vigil-codeintel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vigil_codeintel-0.1.0.dist-info/METADATA +780 -0
- vigil_codeintel-0.1.0.dist-info/RECORD +131 -0
- vigil_codeintel-0.1.0.dist-info/WHEEL +5 -0
- vigil_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
- vigil_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
- vigil_codeintel-0.1.0.dist-info/top_level.txt +3 -0
- vigil_forensic/__init__.py +224 -0
- vigil_forensic/_git_utils.py +178 -0
- vigil_forensic/_shared.py +510 -0
- vigil_forensic/_stubs.py +156 -0
- vigil_forensic/gate_checks/__init__.py +1 -0
- vigil_forensic/gate_checks/_ast_helpers.py +629 -0
- vigil_forensic/gate_checks/_deployment_detector.py +573 -0
- vigil_forensic/gate_checks/atomic_write_checks.py +1143 -0
- vigil_forensic/gate_checks/authority_checks.py +95 -0
- vigil_forensic/gate_checks/boundary_breach_checks.py +202 -0
- vigil_forensic/gate_checks/broad_except_checks.py +301 -0
- vigil_forensic/gate_checks/broad_except_hidden_sentinel_checks.py +365 -0
- vigil_forensic/gate_checks/common.py +253 -0
- vigil_forensic/gate_checks/config_safety_checks.py +704 -0
- vigil_forensic/gate_checks/config_ssot_checks.py +78 -0
- vigil_forensic/gate_checks/conflict_checks.py +193 -0
- vigil_forensic/gate_checks/context_fallback_checks.py +697 -0
- vigil_forensic/gate_checks/context_health_checks.py +289 -0
- vigil_forensic/gate_checks/contract_shape_drift_checks.py +459 -0
- vigil_forensic/gate_checks/dirty_baseline_check.py +274 -0
- vigil_forensic/gate_checks/duplication_checks.py +387 -0
- vigil_forensic/gate_checks/embedded_string_checks.py +123 -0
- vigil_forensic/gate_checks/empty_output_checks.py +87 -0
- vigil_forensic/gate_checks/encoding_checks.py +847 -0
- vigil_forensic/gate_checks/export_completeness_checks.py +156 -0
- vigil_forensic/gate_checks/fallback_checks.py +41 -0
- vigil_forensic/gate_checks/file_proliferation_checks.py +171 -0
- vigil_forensic/gate_checks/fix_without_test_checks.py +69 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/__init__.py +9 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/_helpers.py +71 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/advanced_checks.py +322 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/core.py +273 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/integrity_checks.py +203 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/quality_checks.py +666 -0
- vigil_forensic/gate_checks/forensic_clusters/__init__.py +193 -0
- vigil_forensic/gate_checks/forensic_clusters/allowlist.py +426 -0
- vigil_forensic/gate_checks/forensic_clusters/allowlist_writer.py +302 -0
- vigil_forensic/gate_checks/forensic_clusters/api_protocol.py +231 -0
- vigil_forensic/gate_checks/forensic_clusters/async_quality.py +1156 -0
- vigil_forensic/gate_checks/forensic_clusters/code_style.py +808 -0
- vigil_forensic/gate_checks/forensic_clusters/core.py +319 -0
- vigil_forensic/gate_checks/forensic_clusters/data_quality.py +763 -0
- vigil_forensic/gate_checks/forensic_clusters/dead_code.py +480 -0
- vigil_forensic/gate_checks/forensic_clusters/edit_mutation.py +842 -0
- vigil_forensic/gate_checks/forensic_clusters/exception_boundary.py +240 -0
- vigil_forensic/gate_checks/forensic_clusters/legacy_debt.py +556 -0
- vigil_forensic/gate_checks/forensic_clusters/static_analysis.py +834 -0
- vigil_forensic/gate_checks/forensic_clusters/structural_quality.py +298 -0
- vigil_forensic/gate_checks/god_object_zones_checks.py +173 -0
- vigil_forensic/gate_checks/hallucination_checks.py +566 -0
- vigil_forensic/gate_checks/hunter_artifact_completeness_check.py +139 -0
- vigil_forensic/gate_checks/implementation_overfit_checks.py +380 -0
- vigil_forensic/gate_checks/import_integrity_checks.py +233 -0
- vigil_forensic/gate_checks/imports_in_function_checks.py +283 -0
- vigil_forensic/gate_checks/ml_checks.py +318 -0
- vigil_forensic/gate_checks/performance_checks.py +106 -0
- vigil_forensic/gate_checks/project_specific_runner.py +691 -0
- vigil_forensic/gate_checks/provider_capability_checks.py +73 -0
- vigil_forensic/gate_checks/refactor_completeness_checks.py +274 -0
- vigil_forensic/gate_checks/reliability_checks.py +389 -0
- vigil_forensic/gate_checks/reporting_checks.py +55 -0
- vigil_forensic/gate_checks/runtime_behavior_checks.py +220 -0
- vigil_forensic/gate_checks/security_injection_checks.py +332 -0
- vigil_forensic/gate_checks/semantic_intent_checks.py +139 -0
- vigil_forensic/gate_checks/size_complexity_checks.py +336 -0
- vigil_forensic/gate_checks/stuck_feature_flag_checks.py +354 -0
- vigil_forensic/gate_checks/syntax_validity_checks.py +217 -0
- vigil_forensic/gate_checks/temporal_freshness_checks.py +79 -0
- vigil_forensic/gate_checks/test_quality_checks.py +946 -0
- vigil_forensic/gate_checks/testing_checks.py +149 -0
- vigil_forensic/gate_checks/toctou_checks.py +367 -0
- vigil_forensic/gate_checks/type_checking_checks.py +316 -0
- vigil_forensic/gate_models.py +392 -0
- vigil_forensic/gate_packs/__init__.py +1 -0
- vigil_forensic/gate_packs/universal.py +179 -0
- vigil_forensic/gate_profile.json +31 -0
- vigil_forensic/gate_registry.py +21 -0
- vigil_forensic/language_profiles.py +219 -0
- vigil_forensic/meta_findings.py +207 -0
- vigil_forensic/self_audit.py +725 -0
- vigil_forensic/source_analysis.py +175 -0
- vigil_mapper/__init__.py +103 -0
- vigil_mapper/_ast_helpers_minimal.py +229 -0
- vigil_mapper/_extract_imports_impl.py +123 -0
- vigil_mapper/_file_count_guard.py +129 -0
- vigil_mapper/_git_utils.py +178 -0
- vigil_mapper/_runtime_ast.py +438 -0
- vigil_mapper/_runtime_dispatch.py +137 -0
- vigil_mapper/_seed_helpers.py +82 -0
- vigil_mapper/authority_builder.py +1102 -0
- vigil_mapper/cli_entry.py +731 -0
- vigil_mapper/conflict_builder.py +818 -0
- vigil_mapper/data_contract_builder.py +446 -0
- vigil_mapper/findings_builder.py +716 -0
- vigil_mapper/fingerprint.py +53 -0
- vigil_mapper/hotspot_builder.py +539 -0
- vigil_mapper/map_common.py +449 -0
- vigil_mapper/map_errors.py +55 -0
- vigil_mapper/map_models.py +431 -0
- vigil_mapper/map_models_ext.py +206 -0
- vigil_mapper/map_models_findings.py +130 -0
- vigil_mapper/map_storage.py +455 -0
- vigil_mapper/parse_cache.py +795 -0
- vigil_mapper/refactor_boundary_builder.py +266 -0
- vigil_mapper/runtime_builder.py +527 -0
- vigil_mapper/runtime_tracer.py +243 -0
- vigil_mapper/runtime_tracer_entry.py +199 -0
- vigil_mapper/semantic_diff.py +71 -0
- vigil_mapper/source_adapters/__init__.py +109 -0
- vigil_mapper/source_adapters/_base.py +264 -0
- vigil_mapper/source_adapters/_ir.py +156 -0
- vigil_mapper/source_adapters/_lexer.py +309 -0
- vigil_mapper/source_adapters/_patterns.py +212 -0
- vigil_mapper/source_adapters/_treesitter.py +182 -0
- vigil_mapper/source_adapters/go.py +553 -0
- vigil_mapper/source_adapters/java.py +541 -0
- vigil_mapper/source_adapters/javascript.py +626 -0
- vigil_mapper/source_adapters/python.py +325 -0
- vigil_mapper/source_adapters/typescript.py +749 -0
- vigil_mapper/structural_builder.py +586 -0
- vigil_mcp/__init__.py +1 -0
- vigil_mcp/_jobs.py +587 -0
- vigil_mcp/_paths.py +93 -0
- vigil_mcp/forensic_server.py +419 -0
- vigil_mcp/map_server.py +452 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Shared source analysis facade for vigil_forensic.
|
|
2
|
+
|
|
3
|
+
Adapted from the Vigil autoforensics source_analysis.
|
|
4
|
+
Key change: uses vigil_mapper.source_adapters (sibling standalone pkg)
|
|
5
|
+
instead of the Vigil autoforensics map_builder.source_adapters.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from vigil_forensic.language_profiles import get_profile_for_extension
|
|
14
|
+
from vigil_mapper.source_adapters import get_adapter_for_file
|
|
15
|
+
import logging
|
|
16
|
+
_log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"FunctionInfo",
|
|
20
|
+
"is_source_file",
|
|
21
|
+
"is_test_file",
|
|
22
|
+
"get_language_id",
|
|
23
|
+
"get_generic_stems",
|
|
24
|
+
"get_shared_families",
|
|
25
|
+
"get_flow_markers",
|
|
26
|
+
"get_exclude_dirs",
|
|
27
|
+
"extract_functions",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class FunctionInfo:
|
|
33
|
+
"""Lightweight descriptor for a function-like region in a source file."""
|
|
34
|
+
name: str
|
|
35
|
+
start_line: int
|
|
36
|
+
end_line: int
|
|
37
|
+
line_count: int
|
|
38
|
+
is_method: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_source_file(path: str) -> bool:
|
|
42
|
+
"""True if *path* has a known source extension (via source_adapters)."""
|
|
43
|
+
try:
|
|
44
|
+
return get_adapter_for_file(Path(path)) is not None
|
|
45
|
+
except Exception:
|
|
46
|
+
_log.debug("is_source_file: adapter lookup failed for %r", path)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_test_file(path: str) -> bool:
|
|
51
|
+
"""True if *path* matches test file patterns for its language."""
|
|
52
|
+
p = Path(path)
|
|
53
|
+
ext = p.suffix.lower()
|
|
54
|
+
profile = get_profile_for_extension(ext)
|
|
55
|
+
if profile is None:
|
|
56
|
+
return False
|
|
57
|
+
name = p.name
|
|
58
|
+
path_str = path.replace("\\", "/")
|
|
59
|
+
return any(
|
|
60
|
+
name.startswith(pat) or name.endswith(pat) or pat in path_str
|
|
61
|
+
for pat in profile.test_file_patterns
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_language_id(path: str) -> str | None:
|
|
66
|
+
"""Return the language_id for *path*, or None if unsupported."""
|
|
67
|
+
ext = Path(path).suffix.lower()
|
|
68
|
+
profile = get_profile_for_extension(ext)
|
|
69
|
+
return profile.language_id if profile else None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_generic_stems(path: str) -> frozenset[str]:
|
|
73
|
+
ext = Path(path).suffix.lower()
|
|
74
|
+
profile = get_profile_for_extension(ext)
|
|
75
|
+
return profile.generic_helper_stems if profile else frozenset()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_shared_families(path: str) -> frozenset[str]:
|
|
79
|
+
ext = Path(path).suffix.lower()
|
|
80
|
+
profile = get_profile_for_extension(ext)
|
|
81
|
+
return profile.shared_layer_families if profile else frozenset()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_flow_markers(path: str) -> tuple[str, ...]:
|
|
85
|
+
ext = Path(path).suffix.lower()
|
|
86
|
+
profile = get_profile_for_extension(ext)
|
|
87
|
+
return profile.flow_marker_patterns if profile else ()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_exclude_dirs(path: str) -> frozenset[str]:
|
|
91
|
+
ext = Path(path).suffix.lower()
|
|
92
|
+
profile = get_profile_for_extension(ext)
|
|
93
|
+
return profile.exclude_dir_hints if profile else frozenset()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def extract_functions(path: str, content: str) -> list[FunctionInfo]:
|
|
97
|
+
"""Extract function-like regions from *content*. Never raises; returns [] on error."""
|
|
98
|
+
lang = get_language_id(path)
|
|
99
|
+
if lang == "python":
|
|
100
|
+
return _extract_python_functions(content)
|
|
101
|
+
if lang in ("javascript", "typescript"):
|
|
102
|
+
return _extract_js_functions(content)
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _extract_python_functions(content: str) -> list[FunctionInfo]:
|
|
107
|
+
try:
|
|
108
|
+
from vigil_forensic.gate_checks.common import extract_python_functions as _ast_extract
|
|
109
|
+
raw = _ast_extract(content)
|
|
110
|
+
return [
|
|
111
|
+
FunctionInfo(name=name, start_line=start, end_line=end, line_count=end - start + 1, is_method=False)
|
|
112
|
+
for name, start, end, snippet in raw
|
|
113
|
+
]
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
_log.debug("_extract_python_functions failed: %s", exc)
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_JS_FUNCTION_RE = re.compile(
|
|
120
|
+
r"""
|
|
121
|
+
(?:^|\s)
|
|
122
|
+
(?:
|
|
123
|
+
(?:async\s+)?function\s+
|
|
124
|
+
(?P<fn_name>[A-Za-z_$][A-Za-z0-9_$]*)
|
|
125
|
+
\s*\(
|
|
126
|
+
|
|
|
127
|
+
(?P<arrow_name>[A-Za-z_$][A-Za-z0-9_$]*)
|
|
128
|
+
\s*=\s*
|
|
129
|
+
(?:async\s+)?
|
|
130
|
+
\(.*?\)\s*=>
|
|
131
|
+
|
|
|
132
|
+
^\s*
|
|
133
|
+
(?P<method_name>[A-Za-z_$][A-Za-z0-9_$]*)
|
|
134
|
+
\s*\(
|
|
135
|
+
)
|
|
136
|
+
""",
|
|
137
|
+
re.VERBOSE | re.MULTILINE,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _extract_js_functions(content: str) -> list[FunctionInfo]:
|
|
142
|
+
try:
|
|
143
|
+
lines = content.splitlines()
|
|
144
|
+
results: list[FunctionInfo] = []
|
|
145
|
+
for i, line in enumerate(lines, start=1):
|
|
146
|
+
stripped = line.strip()
|
|
147
|
+
m = re.match(r"(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(", stripped)
|
|
148
|
+
if m:
|
|
149
|
+
name = m.group(1)
|
|
150
|
+
end_line = min(i + 9, len(lines))
|
|
151
|
+
results.append(FunctionInfo(name=name, start_line=i, end_line=end_line, line_count=end_line - i + 1))
|
|
152
|
+
continue
|
|
153
|
+
m = re.match(
|
|
154
|
+
r"(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(.*?\)\s*=>",
|
|
155
|
+
stripped,
|
|
156
|
+
)
|
|
157
|
+
if m:
|
|
158
|
+
name = m.group(1)
|
|
159
|
+
end_line = min(i + 9, len(lines))
|
|
160
|
+
results.append(FunctionInfo(name=name, start_line=i, end_line=end_line, line_count=end_line - i + 1))
|
|
161
|
+
continue
|
|
162
|
+
m = re.match(r"([A-Za-z_$][A-Za-z0-9_$]*)\s*\(", stripped)
|
|
163
|
+
if m and not re.match(
|
|
164
|
+
r"^(?:if|for|while|switch|catch|return|import|export|class|const|let|var)\b", stripped,
|
|
165
|
+
):
|
|
166
|
+
if line and line[0] in (" ", "\t"):
|
|
167
|
+
name = m.group(1)
|
|
168
|
+
end_line = min(i + 9, len(lines))
|
|
169
|
+
results.append(FunctionInfo(
|
|
170
|
+
name=name, start_line=i, end_line=end_line,
|
|
171
|
+
line_count=end_line - i + 1, is_method=True,
|
|
172
|
+
))
|
|
173
|
+
return results
|
|
174
|
+
except Exception:
|
|
175
|
+
return []
|
vigil_mapper/__init__.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Public API for the map builder subsystem.
|
|
2
|
+
|
|
3
|
+
Deferred (lazy) imports only -- avoids circular import chains during
|
|
4
|
+
early startup before map_models/map_storage are fully initialised.
|
|
5
|
+
|
|
6
|
+
Public surface:
|
|
7
|
+
build_all_maps(project_dir) -- stub, implemented in Phase 7 (cli_entry.py)
|
|
8
|
+
load_repo_maps(project_dir) -- load all 7 maps from disk
|
|
9
|
+
maps_dir(project_dir) -- default output dir: <project_dir>/.cortex/maps/
|
|
10
|
+
seeds_dir(project_dir) -- default seeds dir: <project_dir>/.cortex/map_seeds/
|
|
11
|
+
RepoMaps -- container dataclass
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .map_models import RepoMaps as _RepoMaps
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"build_all_maps",
|
|
23
|
+
"load_repo_maps",
|
|
24
|
+
"maps_dir",
|
|
25
|
+
"run_map_build",
|
|
26
|
+
"seeds_dir",
|
|
27
|
+
"RepoMaps",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_repo_maps(project_dir: Path):
|
|
32
|
+
"""Load all 7 maps from <project_dir>/.cortex/maps/. Deferred import."""
|
|
33
|
+
from .map_storage import load_repo_maps as _load # noqa: PLC0415
|
|
34
|
+
return _load(project_dir)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def maps_dir(project_dir: Path) -> Path:
|
|
38
|
+
"""Default output location: <project_dir>/.cortex/maps/. Deferred import."""
|
|
39
|
+
from .map_storage import maps_dir as _maps_dir # noqa: PLC0415
|
|
40
|
+
return _maps_dir(project_dir)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def seeds_dir(project_dir: Path) -> Path:
|
|
44
|
+
"""Default seed config location: <project_dir>/.cortex/map_seeds/. Deferred import."""
|
|
45
|
+
from .map_storage import seeds_dir as _seeds_dir # noqa: PLC0415
|
|
46
|
+
return _seeds_dir(project_dir)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_all_maps(project_dir: Path) -> None:
|
|
50
|
+
"""Build all maps (stub -- full implementation in Phase 7 cli_entry.py).
|
|
51
|
+
|
|
52
|
+
Currently raises NotImplementedError so callers discover the gap early.
|
|
53
|
+
"""
|
|
54
|
+
raise NotImplementedError(
|
|
55
|
+
"build_all_maps is not implemented yet. "
|
|
56
|
+
"Use run_map_build(project_dir) or the vigil-mapper-mcp server instead."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_map_build(
|
|
61
|
+
project_dir,
|
|
62
|
+
*,
|
|
63
|
+
map: str = "all",
|
|
64
|
+
dry_run: bool = False,
|
|
65
|
+
strict: bool = False,
|
|
66
|
+
timeout_s: int = 300,
|
|
67
|
+
output_dir=None,
|
|
68
|
+
max_file_mb: float = 5.0,
|
|
69
|
+
cancel_event=None,
|
|
70
|
+
) -> int:
|
|
71
|
+
"""Programmatic API for the map build pipeline. Deferred import.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
project_dir: Absolute (or resolvable) path to the target project root.
|
|
75
|
+
map: Map name to build, or "all" for the full pipeline.
|
|
76
|
+
dry_run: If True, build all maps in memory but do not write to disk.
|
|
77
|
+
strict: If True, return exit code 3 on warnings, 4 on new conflicts.
|
|
78
|
+
timeout_s: Timeout in seconds for the tracer (runtime map only).
|
|
79
|
+
output_dir: If given, writes maps here instead of <project_dir>/.cortex/maps/.
|
|
80
|
+
max_file_mb: Files larger than this threshold (MiB) are skipped.
|
|
81
|
+
Default: 5.0 MiB. Pass float('inf') to disable.
|
|
82
|
+
cancel_event: Optional threading.Event; build stops early when set.
|
|
83
|
+
|
|
84
|
+
See ``vigil_mapper.cli_entry.run_map_build`` for full docs.
|
|
85
|
+
"""
|
|
86
|
+
from .cli_entry import run_map_build as _run # noqa: PLC0415
|
|
87
|
+
return _run(
|
|
88
|
+
project_dir,
|
|
89
|
+
map=map,
|
|
90
|
+
dry_run=dry_run,
|
|
91
|
+
strict=strict,
|
|
92
|
+
timeout_s=timeout_s,
|
|
93
|
+
output_dir=output_dir,
|
|
94
|
+
max_file_mb=max_file_mb,
|
|
95
|
+
cancel_event=cancel_event,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def __getattr__(name: str):
|
|
100
|
+
if name == "RepoMaps":
|
|
101
|
+
from .map_models import RepoMaps # noqa: PLC0415
|
|
102
|
+
return RepoMaps
|
|
103
|
+
raise AttributeError("module %r has no attribute %r" % (__name__, name))
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Minimal inlined AST helpers — no cross-cluster imports.
|
|
2
|
+
|
|
3
|
+
Inlined from the originating modules in the parent app (pure stdlib, no external deps):
|
|
4
|
+
- gate_models: GateFinding, EvidenceReference, enums
|
|
5
|
+
- _ast_helpers: parse_python_source_or_emit_finding
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import hashlib
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Callable, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Gate model vocabulary (inlined — pure stdlib enums and dataclasses)
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
class GateVerdict(str, Enum):
|
|
21
|
+
PASS = "pass"
|
|
22
|
+
REVISE = "revise"
|
|
23
|
+
BLOCK = "block"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RepairKind(str, Enum):
|
|
27
|
+
REFACTOR = "refactor"
|
|
28
|
+
CONSOLIDATE = "consolidate"
|
|
29
|
+
ADD_PROOF = "add_proof"
|
|
30
|
+
ADD_TEST = "add_test"
|
|
31
|
+
FIX_CONTRACT = "fix_contract"
|
|
32
|
+
REMOVE_FALLBACK = "remove_fallback"
|
|
33
|
+
SPLIT_MODULE = "split_module"
|
|
34
|
+
EXTRACT_SHARED = "extract_shared"
|
|
35
|
+
VALIDATE_BOUNDARY = "validate_boundary"
|
|
36
|
+
REMOVE_DUPLICATE = "remove_duplicate"
|
|
37
|
+
EDIT_CANONICAL = "edit_canonical_module"
|
|
38
|
+
ADD_BOUNDARY_CHECK = "add_boundary_check"
|
|
39
|
+
REPLACE_WITH_FAIL_LOUD = "replace_fallback_with_fail_loud"
|
|
40
|
+
ADD_MISSING_PROOF = "add_missing_proof"
|
|
41
|
+
ADD_REGRESSION_TEST = "add_regression_test"
|
|
42
|
+
REMOVE_DEAD_SURFACE = "remove_dead_surface"
|
|
43
|
+
NORMALIZE_SHAPE = "normalize_shape"
|
|
44
|
+
FIX_ENCODING = "fix_encoding_safety"
|
|
45
|
+
INVESTIGATE_GATE_FAILURE = "investigate_gate_failure"
|
|
46
|
+
FIX_SYNTAX = "fix_syntax"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GateSeverity(str, Enum):
|
|
50
|
+
INFO = "info"
|
|
51
|
+
LOW = "low"
|
|
52
|
+
MEDIUM = "medium"
|
|
53
|
+
HIGH = "high"
|
|
54
|
+
CRITICAL = "critical"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GateImpact(str, Enum):
|
|
58
|
+
WARN = "warn"
|
|
59
|
+
REVISE = "revise"
|
|
60
|
+
BLOCK = "block"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GateCategory(str, Enum):
|
|
64
|
+
CONTRACT = "contract_integrity"
|
|
65
|
+
TRUTH_BOUNDARY = "truth_boundary"
|
|
66
|
+
DRIFT = "drift"
|
|
67
|
+
FALLBACK = "fallback_hack_workaround"
|
|
68
|
+
DUPLICATION = "duplication_shadow_logic"
|
|
69
|
+
CONFIG_SSOT = "config_ssot"
|
|
70
|
+
RUNTIME_BEHAVIOR = "runtime_behavior"
|
|
71
|
+
PERFORMANCE = "performance"
|
|
72
|
+
SIZE_COMPLEXITY = "size_complexity"
|
|
73
|
+
TESTING = "testing_anti_patterns"
|
|
74
|
+
REPORTING = "reporting_artifact_integrity"
|
|
75
|
+
PIPELINE_CHAIN = "pipeline_chain_integrity"
|
|
76
|
+
SEMANTIC_INTENT = "semantic_intent"
|
|
77
|
+
TEMPORAL_FRESHNESS = "temporal_freshness"
|
|
78
|
+
TOOL_HOOK_COVERAGE = "tool_hook_coverage"
|
|
79
|
+
META = "meta_integrity"
|
|
80
|
+
OBSERVABILITY = "observability"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class EvidenceReference:
|
|
85
|
+
kind: str
|
|
86
|
+
path: str = ""
|
|
87
|
+
detail: str = ""
|
|
88
|
+
ok: bool = True
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict[str, Any]:
|
|
91
|
+
return {"kind": self.kind, "path": self.path, "detail": self.detail, "ok": self.ok}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class GateFinding:
|
|
96
|
+
check_id: str
|
|
97
|
+
category: GateCategory
|
|
98
|
+
title: str
|
|
99
|
+
severity: GateSeverity
|
|
100
|
+
impact: GateImpact
|
|
101
|
+
summary: str
|
|
102
|
+
recommendation: str
|
|
103
|
+
evidence: tuple[EvidenceReference, ...] = field(default_factory=tuple)
|
|
104
|
+
fingerprint: str = ""
|
|
105
|
+
repair_kind: str = ""
|
|
106
|
+
executor_action: str = ""
|
|
107
|
+
proof_required: str = ""
|
|
108
|
+
allowlist_allowed: bool = True
|
|
109
|
+
preferred_fix_shape: str = ""
|
|
110
|
+
confidence: float = 1.0
|
|
111
|
+
applicability: str = "applicable"
|
|
112
|
+
analysis_mode: str = "heuristic"
|
|
113
|
+
applicability_reason: str = ""
|
|
114
|
+
|
|
115
|
+
def __post_init__(self) -> None:
|
|
116
|
+
if not (0.0 <= float(self.confidence) <= 1.0):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"GateFinding.confidence must be in [0.0, 1.0], got {self.confidence!r}"
|
|
119
|
+
)
|
|
120
|
+
if self.applicability not in ("applicable", "not_applicable", "unknown"):
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"GateFinding.applicability must be one of "
|
|
123
|
+
f"{{applicable, not_applicable, unknown}}, got {self.applicability!r}"
|
|
124
|
+
)
|
|
125
|
+
if self.applicability != "applicable" and not (self.applicability_reason or "").strip():
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"GateFinding.applicability_reason is required when applicability != 'applicable' "
|
|
128
|
+
f"(applicability={self.applicability!r}, check_id={self.check_id!r})"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def to_dict(self) -> dict[str, Any]:
|
|
132
|
+
return {
|
|
133
|
+
"check_id": self.check_id,
|
|
134
|
+
"category": self.category.value,
|
|
135
|
+
"title": self.title,
|
|
136
|
+
"severity": self.severity.value,
|
|
137
|
+
"impact": self.impact.value,
|
|
138
|
+
"summary": self.summary,
|
|
139
|
+
"recommendation": self.recommendation,
|
|
140
|
+
"evidence": [e.to_dict() for e in self.evidence],
|
|
141
|
+
"fingerprint": self.fingerprint,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# parse_python_source_or_emit_finding (inlined from _ast_helpers.py)
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
_PYTHON_EXTENSIONS: frozenset[str] = frozenset({".py", ".pyi"})
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _looks_like_python_path(rel_path: str) -> bool:
|
|
153
|
+
if not rel_path:
|
|
154
|
+
return False
|
|
155
|
+
normalized = rel_path.replace("\\", "/").lower()
|
|
156
|
+
dot = normalized.rfind(".")
|
|
157
|
+
if dot < 0:
|
|
158
|
+
return False
|
|
159
|
+
return normalized[dot:] in _PYTHON_EXTENSIONS
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def build_syntax_parse_error_finding(
|
|
163
|
+
*,
|
|
164
|
+
rel_path: str,
|
|
165
|
+
exc: SyntaxError,
|
|
166
|
+
emitting_gate: str = "",
|
|
167
|
+
) -> GateFinding:
|
|
168
|
+
line_info = f"line {exc.lineno}" if exc.lineno else "unknown line"
|
|
169
|
+
msg = str(exc.msg) if exc.msg else "unknown parse error"
|
|
170
|
+
evidence = (
|
|
171
|
+
EvidenceReference(
|
|
172
|
+
kind="syntax_error",
|
|
173
|
+
path=rel_path,
|
|
174
|
+
detail=f"{line_info}: {msg}"[:512],
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
fp_source = f"meta.syntax_parse_error|{rel_path}|{exc.lineno}"
|
|
178
|
+
fingerprint = hashlib.sha256(fp_source.encode("utf-8")).hexdigest()[:16]
|
|
179
|
+
emitter_tag = f" [emitted by {emitting_gate}]" if emitting_gate else ""
|
|
180
|
+
return GateFinding(
|
|
181
|
+
check_id="meta.syntax_parse_error",
|
|
182
|
+
category=GateCategory.META,
|
|
183
|
+
title=f"Python syntax error in {rel_path} ({line_info})",
|
|
184
|
+
severity=GateSeverity.HIGH,
|
|
185
|
+
impact=GateImpact.REVISE,
|
|
186
|
+
summary=(
|
|
187
|
+
f"{rel_path}:{exc.lineno}: {msg}. Autoforensics gate could not "
|
|
188
|
+
f"parse this file and skipped its checks for this path.{emitter_tag}"
|
|
189
|
+
),
|
|
190
|
+
recommendation=(
|
|
191
|
+
"Fix the Python syntax error so gates can parse and audit this "
|
|
192
|
+
"file. A silent skip hides real bugs from the audit."
|
|
193
|
+
),
|
|
194
|
+
evidence=evidence,
|
|
195
|
+
fingerprint=fingerprint,
|
|
196
|
+
repair_kind=RepairKind.FIX_SYNTAX.value,
|
|
197
|
+
executor_action="fix Python syntax error",
|
|
198
|
+
proof_required="ast.parse succeeds on the file",
|
|
199
|
+
allowlist_allowed=False,
|
|
200
|
+
preferred_fix_shape="restore valid Python grammar; do not silence via except",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_python_source_or_emit_finding(
|
|
205
|
+
source: str,
|
|
206
|
+
*,
|
|
207
|
+
rel_path: str,
|
|
208
|
+
emit_finding: Optional[Callable[[GateFinding], None]] = None,
|
|
209
|
+
emitting_gate: str = "",
|
|
210
|
+
filename: str | None = None,
|
|
211
|
+
) -> ast.Module | None:
|
|
212
|
+
"""Parse Python source and return the AST module, or emit a meta finding."""
|
|
213
|
+
if not source:
|
|
214
|
+
return None
|
|
215
|
+
try:
|
|
216
|
+
return ast.parse(source, filename=filename or rel_path or "<unknown>")
|
|
217
|
+
except SyntaxError as exc:
|
|
218
|
+
if emit_finding is not None and _looks_like_python_path(rel_path):
|
|
219
|
+
try:
|
|
220
|
+
emit_finding(
|
|
221
|
+
build_syntax_parse_error_finding(
|
|
222
|
+
rel_path=rel_path,
|
|
223
|
+
exc=exc,
|
|
224
|
+
emitting_gate=emitting_gate,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
return None
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Inlined import extractor — pure stdlib, no cross-cluster imports.
|
|
2
|
+
|
|
3
|
+
Extracted from the prompt_engineer_graph module of the parent app.
|
|
4
|
+
Provides: _extract_imports, ModuleNode, STANDARD_SKIP_DIRS.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Standard skip dirs (from project_exclusions — inlined, pure stdlib)
|
|
13
|
+
STANDARD_SKIP_DIRS: frozenset[str] = frozenset({
|
|
14
|
+
"__pycache__", ".git", ".hg", ".svn",
|
|
15
|
+
"node_modules", ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
16
|
+
".venv", "venv", "env", ".env", ".eggs",
|
|
17
|
+
"dist", "build",
|
|
18
|
+
".cortex", ".a1", ".prompt-engineer", ".claude", ".vendor",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ModuleNode:
|
|
24
|
+
"""A Python module in the project."""
|
|
25
|
+
path: str # relative to project root
|
|
26
|
+
package: str # dotted module name
|
|
27
|
+
imports: list[str] = field(default_factory=list)
|
|
28
|
+
lazy_imports: list[str] = field(default_factory=list)
|
|
29
|
+
dynamic_imports: list[str] = field(default_factory=list)
|
|
30
|
+
re_exports: list[str] = field(default_factory=list)
|
|
31
|
+
all_names: list[str] = field(default_factory=list)
|
|
32
|
+
is_init: bool = False
|
|
33
|
+
is_entry_point: bool = False
|
|
34
|
+
is_test: bool = False
|
|
35
|
+
line_count: int = 0
|
|
36
|
+
mtime: float = 0.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_parent_map(tree: ast.Module) -> dict[int, ast.AST]:
|
|
40
|
+
parent_map: dict[int, ast.AST] = {}
|
|
41
|
+
for parent in ast.walk(tree):
|
|
42
|
+
for child in ast.iter_child_nodes(parent):
|
|
43
|
+
parent_map[id(child)] = parent
|
|
44
|
+
return parent_map
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_in_function(node: ast.AST, parent_map: dict[int, ast.AST]) -> bool:
|
|
48
|
+
current = parent_map.get(id(node))
|
|
49
|
+
while current is not None:
|
|
50
|
+
if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
51
|
+
return True
|
|
52
|
+
current = parent_map.get(id(current))
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _extract_imports(source: str, file_path: str) -> ModuleNode:
|
|
57
|
+
"""Parse a Python file and extract all import information."""
|
|
58
|
+
node = ModuleNode(path=file_path, package="")
|
|
59
|
+
lines = source.splitlines()
|
|
60
|
+
node.line_count = len(lines)
|
|
61
|
+
|
|
62
|
+
basename = Path(file_path).name
|
|
63
|
+
node.is_init = basename == "__init__.py"
|
|
64
|
+
node.is_test = basename.startswith("test_") or basename == "conftest.py"
|
|
65
|
+
node.is_entry_point = False
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
tree = ast.parse(source, filename=file_path)
|
|
69
|
+
except SyntaxError:
|
|
70
|
+
return node
|
|
71
|
+
|
|
72
|
+
parent_map = _build_parent_map(tree)
|
|
73
|
+
re_exports_seen: set[str] = set()
|
|
74
|
+
|
|
75
|
+
for ast_node in ast.walk(tree):
|
|
76
|
+
if isinstance(ast_node, ast.Import):
|
|
77
|
+
for alias in ast_node.names:
|
|
78
|
+
target = node.lazy_imports if _is_in_function(ast_node, parent_map) else node.imports
|
|
79
|
+
target.append(alias.name)
|
|
80
|
+
|
|
81
|
+
elif isinstance(ast_node, ast.ImportFrom):
|
|
82
|
+
module = ast_node.module or ""
|
|
83
|
+
if ast_node.level > 0:
|
|
84
|
+
prefix = "." * ast_node.level
|
|
85
|
+
full = f"{prefix}{module}" if module else prefix
|
|
86
|
+
else:
|
|
87
|
+
full = module
|
|
88
|
+
target = node.lazy_imports if _is_in_function(ast_node, parent_map) else node.imports
|
|
89
|
+
target.append(full)
|
|
90
|
+
|
|
91
|
+
if node.is_init and not _is_in_function(ast_node, parent_map):
|
|
92
|
+
for alias in ast_node.names:
|
|
93
|
+
key = f"{full}.{alias.name}"
|
|
94
|
+
if key not in re_exports_seen:
|
|
95
|
+
re_exports_seen.add(key)
|
|
96
|
+
node.re_exports.append(key)
|
|
97
|
+
|
|
98
|
+
elif isinstance(ast_node, ast.Call):
|
|
99
|
+
func = ast_node.func
|
|
100
|
+
if isinstance(func, ast.Attribute) and func.attr == "import_module":
|
|
101
|
+
if ast_node.args and isinstance(ast_node.args[0], ast.Constant):
|
|
102
|
+
node.dynamic_imports.append(str(ast_node.args[0].value))
|
|
103
|
+
elif isinstance(func, ast.Name) and func.id == "__import__":
|
|
104
|
+
if ast_node.args and isinstance(ast_node.args[0], ast.Constant):
|
|
105
|
+
node.dynamic_imports.append(str(ast_node.args[0].value))
|
|
106
|
+
|
|
107
|
+
elif isinstance(ast_node, ast.Assign):
|
|
108
|
+
for target in ast_node.targets:
|
|
109
|
+
if isinstance(target, ast.Name) and target.id == "__all__":
|
|
110
|
+
if isinstance(ast_node.value, (ast.List, ast.Tuple)):
|
|
111
|
+
for elt in ast_node.value.elts:
|
|
112
|
+
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
113
|
+
node.all_names.append(elt.value)
|
|
114
|
+
|
|
115
|
+
elif isinstance(ast_node, ast.If):
|
|
116
|
+
test = ast_node.test
|
|
117
|
+
if (isinstance(test, ast.Compare)
|
|
118
|
+
and isinstance(test.left, ast.Name) and test.left.id == "__name__"
|
|
119
|
+
and any(isinstance(c, ast.Constant) and c.value == "__main__"
|
|
120
|
+
for c in test.comparators)):
|
|
121
|
+
node.is_entry_point = True
|
|
122
|
+
|
|
123
|
+
return node
|