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.
Files changed (131) hide show
  1. vigil_codeintel-0.1.0.dist-info/METADATA +780 -0
  2. vigil_codeintel-0.1.0.dist-info/RECORD +131 -0
  3. vigil_codeintel-0.1.0.dist-info/WHEEL +5 -0
  4. vigil_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
  5. vigil_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. vigil_codeintel-0.1.0.dist-info/top_level.txt +3 -0
  7. vigil_forensic/__init__.py +224 -0
  8. vigil_forensic/_git_utils.py +178 -0
  9. vigil_forensic/_shared.py +510 -0
  10. vigil_forensic/_stubs.py +156 -0
  11. vigil_forensic/gate_checks/__init__.py +1 -0
  12. vigil_forensic/gate_checks/_ast_helpers.py +629 -0
  13. vigil_forensic/gate_checks/_deployment_detector.py +573 -0
  14. vigil_forensic/gate_checks/atomic_write_checks.py +1143 -0
  15. vigil_forensic/gate_checks/authority_checks.py +95 -0
  16. vigil_forensic/gate_checks/boundary_breach_checks.py +202 -0
  17. vigil_forensic/gate_checks/broad_except_checks.py +301 -0
  18. vigil_forensic/gate_checks/broad_except_hidden_sentinel_checks.py +365 -0
  19. vigil_forensic/gate_checks/common.py +253 -0
  20. vigil_forensic/gate_checks/config_safety_checks.py +704 -0
  21. vigil_forensic/gate_checks/config_ssot_checks.py +78 -0
  22. vigil_forensic/gate_checks/conflict_checks.py +193 -0
  23. vigil_forensic/gate_checks/context_fallback_checks.py +697 -0
  24. vigil_forensic/gate_checks/context_health_checks.py +289 -0
  25. vigil_forensic/gate_checks/contract_shape_drift_checks.py +459 -0
  26. vigil_forensic/gate_checks/dirty_baseline_check.py +274 -0
  27. vigil_forensic/gate_checks/duplication_checks.py +387 -0
  28. vigil_forensic/gate_checks/embedded_string_checks.py +123 -0
  29. vigil_forensic/gate_checks/empty_output_checks.py +87 -0
  30. vigil_forensic/gate_checks/encoding_checks.py +847 -0
  31. vigil_forensic/gate_checks/export_completeness_checks.py +156 -0
  32. vigil_forensic/gate_checks/fallback_checks.py +41 -0
  33. vigil_forensic/gate_checks/file_proliferation_checks.py +171 -0
  34. vigil_forensic/gate_checks/fix_without_test_checks.py +69 -0
  35. vigil_forensic/gate_checks/forensic_cluster_runners/__init__.py +9 -0
  36. vigil_forensic/gate_checks/forensic_cluster_runners/_helpers.py +71 -0
  37. vigil_forensic/gate_checks/forensic_cluster_runners/advanced_checks.py +322 -0
  38. vigil_forensic/gate_checks/forensic_cluster_runners/core.py +273 -0
  39. vigil_forensic/gate_checks/forensic_cluster_runners/integrity_checks.py +203 -0
  40. vigil_forensic/gate_checks/forensic_cluster_runners/quality_checks.py +666 -0
  41. vigil_forensic/gate_checks/forensic_clusters/__init__.py +193 -0
  42. vigil_forensic/gate_checks/forensic_clusters/allowlist.py +426 -0
  43. vigil_forensic/gate_checks/forensic_clusters/allowlist_writer.py +302 -0
  44. vigil_forensic/gate_checks/forensic_clusters/api_protocol.py +231 -0
  45. vigil_forensic/gate_checks/forensic_clusters/async_quality.py +1156 -0
  46. vigil_forensic/gate_checks/forensic_clusters/code_style.py +808 -0
  47. vigil_forensic/gate_checks/forensic_clusters/core.py +319 -0
  48. vigil_forensic/gate_checks/forensic_clusters/data_quality.py +763 -0
  49. vigil_forensic/gate_checks/forensic_clusters/dead_code.py +480 -0
  50. vigil_forensic/gate_checks/forensic_clusters/edit_mutation.py +842 -0
  51. vigil_forensic/gate_checks/forensic_clusters/exception_boundary.py +240 -0
  52. vigil_forensic/gate_checks/forensic_clusters/legacy_debt.py +556 -0
  53. vigil_forensic/gate_checks/forensic_clusters/static_analysis.py +834 -0
  54. vigil_forensic/gate_checks/forensic_clusters/structural_quality.py +298 -0
  55. vigil_forensic/gate_checks/god_object_zones_checks.py +173 -0
  56. vigil_forensic/gate_checks/hallucination_checks.py +566 -0
  57. vigil_forensic/gate_checks/hunter_artifact_completeness_check.py +139 -0
  58. vigil_forensic/gate_checks/implementation_overfit_checks.py +380 -0
  59. vigil_forensic/gate_checks/import_integrity_checks.py +233 -0
  60. vigil_forensic/gate_checks/imports_in_function_checks.py +283 -0
  61. vigil_forensic/gate_checks/ml_checks.py +318 -0
  62. vigil_forensic/gate_checks/performance_checks.py +106 -0
  63. vigil_forensic/gate_checks/project_specific_runner.py +691 -0
  64. vigil_forensic/gate_checks/provider_capability_checks.py +73 -0
  65. vigil_forensic/gate_checks/refactor_completeness_checks.py +274 -0
  66. vigil_forensic/gate_checks/reliability_checks.py +389 -0
  67. vigil_forensic/gate_checks/reporting_checks.py +55 -0
  68. vigil_forensic/gate_checks/runtime_behavior_checks.py +220 -0
  69. vigil_forensic/gate_checks/security_injection_checks.py +332 -0
  70. vigil_forensic/gate_checks/semantic_intent_checks.py +139 -0
  71. vigil_forensic/gate_checks/size_complexity_checks.py +336 -0
  72. vigil_forensic/gate_checks/stuck_feature_flag_checks.py +354 -0
  73. vigil_forensic/gate_checks/syntax_validity_checks.py +217 -0
  74. vigil_forensic/gate_checks/temporal_freshness_checks.py +79 -0
  75. vigil_forensic/gate_checks/test_quality_checks.py +946 -0
  76. vigil_forensic/gate_checks/testing_checks.py +149 -0
  77. vigil_forensic/gate_checks/toctou_checks.py +367 -0
  78. vigil_forensic/gate_checks/type_checking_checks.py +316 -0
  79. vigil_forensic/gate_models.py +392 -0
  80. vigil_forensic/gate_packs/__init__.py +1 -0
  81. vigil_forensic/gate_packs/universal.py +179 -0
  82. vigil_forensic/gate_profile.json +31 -0
  83. vigil_forensic/gate_registry.py +21 -0
  84. vigil_forensic/language_profiles.py +219 -0
  85. vigil_forensic/meta_findings.py +207 -0
  86. vigil_forensic/self_audit.py +725 -0
  87. vigil_forensic/source_analysis.py +175 -0
  88. vigil_mapper/__init__.py +103 -0
  89. vigil_mapper/_ast_helpers_minimal.py +229 -0
  90. vigil_mapper/_extract_imports_impl.py +123 -0
  91. vigil_mapper/_file_count_guard.py +129 -0
  92. vigil_mapper/_git_utils.py +178 -0
  93. vigil_mapper/_runtime_ast.py +438 -0
  94. vigil_mapper/_runtime_dispatch.py +137 -0
  95. vigil_mapper/_seed_helpers.py +82 -0
  96. vigil_mapper/authority_builder.py +1102 -0
  97. vigil_mapper/cli_entry.py +731 -0
  98. vigil_mapper/conflict_builder.py +818 -0
  99. vigil_mapper/data_contract_builder.py +446 -0
  100. vigil_mapper/findings_builder.py +716 -0
  101. vigil_mapper/fingerprint.py +53 -0
  102. vigil_mapper/hotspot_builder.py +539 -0
  103. vigil_mapper/map_common.py +449 -0
  104. vigil_mapper/map_errors.py +55 -0
  105. vigil_mapper/map_models.py +431 -0
  106. vigil_mapper/map_models_ext.py +206 -0
  107. vigil_mapper/map_models_findings.py +130 -0
  108. vigil_mapper/map_storage.py +455 -0
  109. vigil_mapper/parse_cache.py +795 -0
  110. vigil_mapper/refactor_boundary_builder.py +266 -0
  111. vigil_mapper/runtime_builder.py +527 -0
  112. vigil_mapper/runtime_tracer.py +243 -0
  113. vigil_mapper/runtime_tracer_entry.py +199 -0
  114. vigil_mapper/semantic_diff.py +71 -0
  115. vigil_mapper/source_adapters/__init__.py +109 -0
  116. vigil_mapper/source_adapters/_base.py +264 -0
  117. vigil_mapper/source_adapters/_ir.py +156 -0
  118. vigil_mapper/source_adapters/_lexer.py +309 -0
  119. vigil_mapper/source_adapters/_patterns.py +212 -0
  120. vigil_mapper/source_adapters/_treesitter.py +182 -0
  121. vigil_mapper/source_adapters/go.py +553 -0
  122. vigil_mapper/source_adapters/java.py +541 -0
  123. vigil_mapper/source_adapters/javascript.py +626 -0
  124. vigil_mapper/source_adapters/python.py +325 -0
  125. vigil_mapper/source_adapters/typescript.py +749 -0
  126. vigil_mapper/structural_builder.py +586 -0
  127. vigil_mcp/__init__.py +1 -0
  128. vigil_mcp/_jobs.py +587 -0
  129. vigil_mcp/_paths.py +93 -0
  130. vigil_mcp/forensic_server.py +419 -0
  131. 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 []
@@ -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