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,666 @@
|
|
|
1
|
+
"""Quality cluster wrappers -- clusters 10-31.
|
|
2
|
+
|
|
3
|
+
Covers: edit consistency, mutation verified, security patterns, test quality,
|
|
4
|
+
import cycles, roundtrip consistency, shared mutable state, dependency
|
|
5
|
+
vulnerabilities, secrets in code, dead code, unused imports, magic numbers,
|
|
6
|
+
error message quality, naming consistency, todo debt, log level quality,
|
|
7
|
+
encoding consistency, embedded code syntax, response shape drift, HTTP method
|
|
8
|
+
consistency, JS surface coverage, exception swallowing.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from ...source_analysis import is_source_file, get_language_id
|
|
16
|
+
from ...gate_models import GateFinding, PostExecGateContext
|
|
17
|
+
from .._ast_helpers import collect_string_constant_line_ranges
|
|
18
|
+
from ..forensic_clusters import (
|
|
19
|
+
DeadCodeItem,
|
|
20
|
+
assess_dead_code,
|
|
21
|
+
assess_dependency_vulnerabilities,
|
|
22
|
+
assess_edit_consistency,
|
|
23
|
+
assess_embedded_code_syntax,
|
|
24
|
+
assess_encoding_consistency,
|
|
25
|
+
assess_error_message_quality,
|
|
26
|
+
assess_exception_swallowing,
|
|
27
|
+
assess_http_method_consistency,
|
|
28
|
+
assess_import_cycles,
|
|
29
|
+
assess_js_surface_coverage,
|
|
30
|
+
assess_log_level_quality,
|
|
31
|
+
assess_magic_numbers,
|
|
32
|
+
assess_mutation_verified,
|
|
33
|
+
assess_naming_consistency,
|
|
34
|
+
assess_response_shape_drift,
|
|
35
|
+
assess_secrets_in_code,
|
|
36
|
+
assess_security_patterns,
|
|
37
|
+
assess_shared_mutable_state,
|
|
38
|
+
assess_test_quality,
|
|
39
|
+
assess_todo_debt,
|
|
40
|
+
assess_unused_imports,
|
|
41
|
+
classify_dead_code_item,
|
|
42
|
+
)
|
|
43
|
+
from ._helpers import _MAX_FINDINGS_PER_CLUSTER
|
|
44
|
+
import logging
|
|
45
|
+
_log = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Cluster 10: Edit Consistency
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _check_edit_consistency(ctx: PostExecGateContext) -> list[GateFinding]:
|
|
54
|
+
"""Cluster 10: Check that repeated patterns are consistent across touched files."""
|
|
55
|
+
snapshots = ctx.file_snapshots or {}
|
|
56
|
+
if not snapshots:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
operator_files = {
|
|
60
|
+
path: snap for path, snap in snapshots.items()
|
|
61
|
+
if "operator_api" in path and hasattr(snap, "text")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if not operator_files:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
instances: dict[str, str] = {}
|
|
68
|
+
for path, snap in operator_files.items():
|
|
69
|
+
text = snap.text or ""
|
|
70
|
+
for match in re.finditer(r"def (handle_\w+)\(", text):
|
|
71
|
+
func_name = match.group(1)
|
|
72
|
+
start = match.start()
|
|
73
|
+
next_def = text.find("\ndef ", start + 1)
|
|
74
|
+
body = text[start:next_def] if next_def != -1 else text[start:]
|
|
75
|
+
|
|
76
|
+
if "_current_project_id(" in body:
|
|
77
|
+
instances[f"{path}:{func_name}"] = "_current_project_id"
|
|
78
|
+
elif "_bound_project_id(" in body:
|
|
79
|
+
instances[f"{path}:{func_name}"] = "_bound_project_id"
|
|
80
|
+
|
|
81
|
+
if not instances:
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
return assess_edit_consistency(instances, r"_bound_project_id")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Cluster 11: Mutation Without Verification
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_mutation_verified(ctx: PostExecGateContext) -> list[GateFinding]:
|
|
93
|
+
"""Cluster 11: Verify that file snapshots match actual disk content."""
|
|
94
|
+
import hashlib
|
|
95
|
+
|
|
96
|
+
snapshots = ctx.file_snapshots or {}
|
|
97
|
+
if not snapshots:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
findings: list[GateFinding] = []
|
|
101
|
+
sample_paths = sorted(snapshots.keys())[:10]
|
|
102
|
+
|
|
103
|
+
for path in sample_paths:
|
|
104
|
+
snap = snapshots[path]
|
|
105
|
+
if not hasattr(snap, "text") or not snap.text:
|
|
106
|
+
continue
|
|
107
|
+
expected_hash = hashlib.sha256(snap.text.encode("utf-8")).hexdigest()
|
|
108
|
+
findings.extend(
|
|
109
|
+
assess_mutation_verified(
|
|
110
|
+
path,
|
|
111
|
+
expected_hash,
|
|
112
|
+
project_dir=getattr(ctx, "project_dir", None),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return findings
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Cluster 12: Security Patterns
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _check_security_patterns(ctx) -> list[GateFinding]:
|
|
125
|
+
snapshots = ctx.file_snapshots or {}
|
|
126
|
+
findings: list[GateFinding] = []
|
|
127
|
+
for path, snap in snapshots.items():
|
|
128
|
+
if not hasattr(snap, "text") or not snap.text:
|
|
129
|
+
continue
|
|
130
|
+
findings.extend(assess_security_patterns(path, snap.text))
|
|
131
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
132
|
+
break
|
|
133
|
+
return findings
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Cluster 13: Test Quality
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check_test_quality(ctx) -> list[GateFinding]:
|
|
142
|
+
snapshots = ctx.file_snapshots or {}
|
|
143
|
+
findings: list[GateFinding] = []
|
|
144
|
+
for path, snap in snapshots.items():
|
|
145
|
+
if not path.replace("\\", "/").split("/")[-1].startswith("test_"):
|
|
146
|
+
continue
|
|
147
|
+
if not hasattr(snap, "text") or not snap.text:
|
|
148
|
+
continue
|
|
149
|
+
findings.extend(assess_test_quality(path, snap.text))
|
|
150
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
151
|
+
break
|
|
152
|
+
return findings
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Cluster 14: Import Cycles
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _check_import_cycles(ctx) -> list[GateFinding]:
|
|
161
|
+
import re as _local_re
|
|
162
|
+
snapshots = ctx.file_snapshots or {}
|
|
163
|
+
module_imports = {}
|
|
164
|
+
for path, snap in snapshots.items():
|
|
165
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
166
|
+
continue
|
|
167
|
+
if get_language_id(path) != "python":
|
|
168
|
+
continue
|
|
169
|
+
mod_name = path.replace("/", ".").replace("\\", ".").removesuffix(".py")
|
|
170
|
+
imports = []
|
|
171
|
+
for line in snap.text.splitlines():
|
|
172
|
+
m = _local_re.match(r"from\s+\.(\w+)", line)
|
|
173
|
+
if m:
|
|
174
|
+
parent = ".".join(mod_name.split(".")[:-1])
|
|
175
|
+
imports.append(f"{parent}.{m.group(1)}")
|
|
176
|
+
continue
|
|
177
|
+
m = _local_re.match(r"from\s+([\w.]+)\s+import", line)
|
|
178
|
+
if m:
|
|
179
|
+
imports.append(m.group(1))
|
|
180
|
+
if imports:
|
|
181
|
+
module_imports[mod_name] = imports
|
|
182
|
+
return assess_import_cycles(module_imports)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Cluster 15: Roundtrip Consistency
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _check_roundtrip_consistency(ctx) -> list[GateFinding]:
|
|
191
|
+
# standalone: project_export roundtrip check not applicable
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# Cluster 16: Shared Mutable State
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _check_shared_mutable_state(ctx) -> list[GateFinding]:
|
|
201
|
+
snapshots = ctx.file_snapshots or {}
|
|
202
|
+
findings: list[GateFinding] = []
|
|
203
|
+
for path, snap in snapshots.items():
|
|
204
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
205
|
+
continue
|
|
206
|
+
if get_language_id(path) != "python":
|
|
207
|
+
continue
|
|
208
|
+
if path.replace("\\", "/").split("/")[-1].startswith("test_"):
|
|
209
|
+
continue
|
|
210
|
+
findings.extend(assess_shared_mutable_state(path, snap.text))
|
|
211
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
212
|
+
break
|
|
213
|
+
return findings
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Cluster 17: Dependency Vulnerabilities
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _check_dependency_vulnerabilities(ctx) -> list[GateFinding]:
|
|
222
|
+
"""Cluster 17: Run pip audit and parse results."""
|
|
223
|
+
import logging as _logging
|
|
224
|
+
_log = _logging.getLogger(__name__)
|
|
225
|
+
if os.environ.get("AI_HOST_SKIP_PIP_AUDIT") == "1":
|
|
226
|
+
_log.debug("_check_dependency_vulnerabilities: skipped (AI_HOST_SKIP_PIP_AUDIT=1)")
|
|
227
|
+
return []
|
|
228
|
+
import subprocess as _sp
|
|
229
|
+
try:
|
|
230
|
+
result = _sp.run(
|
|
231
|
+
["pip", "audit", "--format=json"],
|
|
232
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
233
|
+
errors="replace", timeout=60,
|
|
234
|
+
)
|
|
235
|
+
return assess_dependency_vulnerabilities(result.stdout, "pip")
|
|
236
|
+
except (FileNotFoundError, Exception):
|
|
237
|
+
return [] # NOT_APPLICABLE -- pip audit unavailable
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# Cluster 25: Secrets in Code
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _check_secrets_in_code(ctx) -> list[GateFinding]:
|
|
246
|
+
"""Cluster 25: Scan touched files for hardcoded secrets."""
|
|
247
|
+
snapshots = ctx.file_snapshots or {}
|
|
248
|
+
findings: list[GateFinding] = []
|
|
249
|
+
for path, snap in snapshots.items():
|
|
250
|
+
if not hasattr(snap, "text") or not snap.text:
|
|
251
|
+
continue
|
|
252
|
+
basename = path.replace("\\", "/").split("/")[-1]
|
|
253
|
+
if basename.startswith("test_") or basename.endswith(".example"):
|
|
254
|
+
continue
|
|
255
|
+
findings.extend(assess_secrets_in_code(path, snap.text))
|
|
256
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
257
|
+
break
|
|
258
|
+
return findings
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Cluster 20: Dead Code
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _check_dead_code(ctx) -> list[GateFinding]:
|
|
267
|
+
"""Cluster 20: Find dead/forgotten code in touched files."""
|
|
268
|
+
import re as _re
|
|
269
|
+
snapshots = ctx.file_snapshots or {}
|
|
270
|
+
if not snapshots:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
definitions: list[tuple[str, str, int, str]] = []
|
|
274
|
+
all_content = ""
|
|
275
|
+
module_alls: dict[str, set] = {}
|
|
276
|
+
|
|
277
|
+
for path, snap in snapshots.items():
|
|
278
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
279
|
+
continue
|
|
280
|
+
if get_language_id(path) != "python":
|
|
281
|
+
continue
|
|
282
|
+
text = snap.text
|
|
283
|
+
all_content += text + "\n"
|
|
284
|
+
|
|
285
|
+
all_match = _re.search(r"__all__\s*=\s*[\[\(](.*?)[\]\)]", text, _re.DOTALL)
|
|
286
|
+
if all_match:
|
|
287
|
+
names = set(_re.findall(r"['\"](\w+)['\"]", all_match.group(1)))
|
|
288
|
+
module_alls[path] = names
|
|
289
|
+
|
|
290
|
+
# F14a: pre-compute string-literal line ranges so we skip `def foo`
|
|
291
|
+
# matches that actually live inside a triple-quoted string fixture.
|
|
292
|
+
file_string_lines = collect_string_constant_line_ranges(text)
|
|
293
|
+
for m in _re.finditer(r"^def (\w+)\s*\(", text, _re.MULTILINE):
|
|
294
|
+
line_start = text.rfind("\n", 0, m.start()) + 1
|
|
295
|
+
if text[line_start:m.start()].strip():
|
|
296
|
+
continue
|
|
297
|
+
line = text[:m.start()].count("\n") + 1
|
|
298
|
+
if line in file_string_lines:
|
|
299
|
+
# Definition is inside a string literal (test fixture, docstring
|
|
300
|
+
# example, embedded snippet). Not a real top-level function.
|
|
301
|
+
continue
|
|
302
|
+
definitions.append((m.group(1), path, line, "function"))
|
|
303
|
+
|
|
304
|
+
if not definitions:
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
items: list[DeadCodeItem] = []
|
|
308
|
+
for name, file_path, line, kind in definitions:
|
|
309
|
+
# Skip dunders (framework hooks like __init__/__repr__, normally called
|
|
310
|
+
# implicitly) and test functions. Single-underscore private functions
|
|
311
|
+
# ARE candidates: classify_dead_code_item flags a private symbol only
|
|
312
|
+
# when it is unreferenced anywhere (a `self._x` method call sets
|
|
313
|
+
# is_referenced and is skipped), and a public unreferenced symbol is
|
|
314
|
+
# treated as possible external API (not flagged) -- so private+unref is
|
|
315
|
+
# the precise dead-code signal.
|
|
316
|
+
if name.startswith("__") or name.startswith("test_"):
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
is_in_all = name in module_alls.get(file_path, set())
|
|
320
|
+
ref_pattern = _re.compile(rf"\b{_re.escape(name)}\b")
|
|
321
|
+
all_refs = list(ref_pattern.finditer(all_content))
|
|
322
|
+
ref_count = len(all_refs) - 1
|
|
323
|
+
|
|
324
|
+
method_pattern = _re.compile(rf"\.{_re.escape(name)}\b")
|
|
325
|
+
method_refs = list(method_pattern.finditer(all_content))
|
|
326
|
+
is_referenced = ref_count > 0 or len(method_refs) > 0
|
|
327
|
+
item = classify_dead_code_item(
|
|
328
|
+
name=name, file_path=file_path, line=line, kind=kind,
|
|
329
|
+
is_referenced_anywhere=is_referenced,
|
|
330
|
+
is_in_all=is_in_all,
|
|
331
|
+
is_recent_commit=False,
|
|
332
|
+
has_adjacent_caller_file=False,
|
|
333
|
+
)
|
|
334
|
+
if item.classification != "standalone_utility":
|
|
335
|
+
items.append(item)
|
|
336
|
+
|
|
337
|
+
return assess_dead_code(items)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
# Cluster 23: Unused Imports
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _check_unused_imports(ctx) -> list[GateFinding]:
|
|
346
|
+
"""Cluster 23: Find unused imports in touched files."""
|
|
347
|
+
snapshots = ctx.file_snapshots or {}
|
|
348
|
+
findings: list[GateFinding] = []
|
|
349
|
+
for path, snap in snapshots.items():
|
|
350
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
351
|
+
continue
|
|
352
|
+
if get_language_id(path) != "python":
|
|
353
|
+
continue
|
|
354
|
+
findings.extend(assess_unused_imports(path, snap.text))
|
|
355
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
356
|
+
break
|
|
357
|
+
return findings
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
# Cluster 21: Magic Numbers
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _check_magic_numbers(ctx) -> list[GateFinding]:
|
|
366
|
+
snapshots = ctx.file_snapshots or {}
|
|
367
|
+
findings: list[GateFinding] = []
|
|
368
|
+
for path, snap in snapshots.items():
|
|
369
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
370
|
+
continue
|
|
371
|
+
if get_language_id(path) != "python":
|
|
372
|
+
continue
|
|
373
|
+
findings.extend(assess_magic_numbers(path, snap.text))
|
|
374
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
375
|
+
break
|
|
376
|
+
return findings
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
# Cluster 22: Error Message Quality
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _check_error_message_quality(ctx) -> list[GateFinding]:
|
|
385
|
+
snapshots = ctx.file_snapshots or {}
|
|
386
|
+
findings: list[GateFinding] = []
|
|
387
|
+
for path, snap in snapshots.items():
|
|
388
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
389
|
+
continue
|
|
390
|
+
findings.extend(assess_error_message_quality(path, snap.text))
|
|
391
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
392
|
+
break
|
|
393
|
+
return findings
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ---------------------------------------------------------------------------
|
|
397
|
+
# Cluster 24: Naming Consistency
|
|
398
|
+
# ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _check_naming_consistency(ctx) -> list[GateFinding]:
|
|
402
|
+
snapshots = ctx.file_snapshots or {}
|
|
403
|
+
findings: list[GateFinding] = []
|
|
404
|
+
for path, snap in snapshots.items():
|
|
405
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
406
|
+
continue
|
|
407
|
+
findings.extend(assess_naming_consistency(path, snap.text))
|
|
408
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
409
|
+
break
|
|
410
|
+
return findings
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
# Cluster 26: TODO Debt
|
|
415
|
+
# ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _check_todo_debt(ctx) -> list[GateFinding]:
|
|
419
|
+
snapshots = ctx.file_snapshots or {}
|
|
420
|
+
findings: list[GateFinding] = []
|
|
421
|
+
for path, snap in snapshots.items():
|
|
422
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
423
|
+
continue
|
|
424
|
+
findings.extend(assess_todo_debt(path, snap.text))
|
|
425
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
426
|
+
break
|
|
427
|
+
return findings
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ---------------------------------------------------------------------------
|
|
431
|
+
# Cluster 28: Log Level Quality
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _check_log_level_quality(ctx) -> list[GateFinding]:
|
|
436
|
+
snapshots = ctx.file_snapshots or {}
|
|
437
|
+
findings: list[GateFinding] = []
|
|
438
|
+
for path, snap in snapshots.items():
|
|
439
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
440
|
+
continue
|
|
441
|
+
findings.extend(assess_log_level_quality(path, snap.text))
|
|
442
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
443
|
+
break
|
|
444
|
+
return findings
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
# Cluster 29: Encoding Consistency
|
|
449
|
+
# ---------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _check_encoding_consistency(ctx) -> list[GateFinding]:
|
|
453
|
+
snapshots = ctx.file_snapshots or {}
|
|
454
|
+
findings: list[GateFinding] = []
|
|
455
|
+
for path, snap in snapshots.items():
|
|
456
|
+
if not is_source_file(path):
|
|
457
|
+
continue
|
|
458
|
+
from pathlib import Path as _P
|
|
459
|
+
p = _P(path)
|
|
460
|
+
if not p.exists():
|
|
461
|
+
continue
|
|
462
|
+
try:
|
|
463
|
+
raw = p.read_bytes()
|
|
464
|
+
except OSError:
|
|
465
|
+
continue
|
|
466
|
+
findings.extend(assess_encoding_consistency(path, raw))
|
|
467
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
468
|
+
break
|
|
469
|
+
return findings
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
# Phase 8 sub-runners (JS/API contract drift)
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _check_embedded_code_syntax(ctx) -> list[GateFinding]:
|
|
478
|
+
snapshots = ctx.file_snapshots or {}
|
|
479
|
+
findings: list[GateFinding] = []
|
|
480
|
+
for path, snap in snapshots.items():
|
|
481
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
482
|
+
continue
|
|
483
|
+
findings.extend(assess_embedded_code_syntax(path, snap.text))
|
|
484
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
485
|
+
break
|
|
486
|
+
return findings
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _check_response_shape_drift(ctx) -> list[GateFinding]:
|
|
490
|
+
snapshots = ctx.file_snapshots or {}
|
|
491
|
+
backend = {p: s.text for p, s in snapshots.items()
|
|
492
|
+
if is_source_file(p) and hasattr(s, "text") and s.text and "_send_json" in s.text}
|
|
493
|
+
frontend = {p: s.text for p, s in snapshots.items()
|
|
494
|
+
if is_source_file(p) and hasattr(s, "text") and s.text
|
|
495
|
+
and any(kw in s.text for kw in ("fetch(", "addEventListener", "document.getElementById"))}
|
|
496
|
+
return assess_response_shape_drift(backend, frontend)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _check_http_method_consistency(ctx) -> list[GateFinding]:
|
|
500
|
+
import re as _re
|
|
501
|
+
snapshots = ctx.file_snapshots or {}
|
|
502
|
+
|
|
503
|
+
route_methods = {}
|
|
504
|
+
for path, snap in snapshots.items():
|
|
505
|
+
if "dashboard_extension" not in path:
|
|
506
|
+
continue
|
|
507
|
+
if not hasattr(snap, "text") or not snap.text:
|
|
508
|
+
continue
|
|
509
|
+
in_get = False
|
|
510
|
+
in_post = False
|
|
511
|
+
for line in snap.text.splitlines():
|
|
512
|
+
if "def do_GET" in line:
|
|
513
|
+
in_get = True; in_post = False
|
|
514
|
+
elif "def do_POST" in line:
|
|
515
|
+
in_post = True; in_get = False
|
|
516
|
+
elif line.strip().startswith("def "):
|
|
517
|
+
in_get = False; in_post = False
|
|
518
|
+
|
|
519
|
+
m = _re.search(r'path\s*==\s*["\']([^"\']+)["\']', line)
|
|
520
|
+
if m:
|
|
521
|
+
method = "GET" if in_get else "POST" if in_post else ""
|
|
522
|
+
if method:
|
|
523
|
+
route_methods[m.group(1)] = method
|
|
524
|
+
|
|
525
|
+
js_fetches = _extract_js_fetches_multiline(snapshots)
|
|
526
|
+
|
|
527
|
+
return assess_http_method_consistency(route_methods, js_fetches)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _extract_js_fetches_multiline(snapshots) -> list[tuple[str, str, str]]:
|
|
531
|
+
"""Extract ``fetch(url, {options})`` call sites, honouring multi-line options.
|
|
532
|
+
|
|
533
|
+
The legacy implementation looked for ``method: 'POST'`` on the SAME line as
|
|
534
|
+
the ``fetch(`` call -- which broke for idiomatic multi-line literals like::
|
|
535
|
+
|
|
536
|
+
fetch('/api/x', {
|
|
537
|
+
method: 'POST',
|
|
538
|
+
body: JSON.stringify({...}),
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
... causing the gate to default to ``GET`` and flag a mismatch even though
|
|
542
|
+
the JS was fine. This version collects the full options object by
|
|
543
|
+
bracket-balanced scan across lines, then searches ``method: ...`` inside
|
|
544
|
+
the collected block.
|
|
545
|
+
|
|
546
|
+
Returns list of ``(url, METHOD, "<path>:<line>")`` tuples.
|
|
547
|
+
"""
|
|
548
|
+
import re as _re
|
|
549
|
+
|
|
550
|
+
fetch_re = _re.compile(r"fetch\s*\(\s*['\"](/[^'\"]+)['\"]\s*(,\s*\{)?")
|
|
551
|
+
method_re = _re.compile(r"""['\"]?method['\"]?\s*:\s*['\"](\w+)['\"]""")
|
|
552
|
+
|
|
553
|
+
js_fetches: list[tuple[str, str, str]] = []
|
|
554
|
+
for path, snap in snapshots.items():
|
|
555
|
+
text = getattr(snap, "text", None)
|
|
556
|
+
if not text:
|
|
557
|
+
continue
|
|
558
|
+
lines = text.splitlines()
|
|
559
|
+
for i, line in enumerate(lines, 1):
|
|
560
|
+
for m in fetch_re.finditer(line):
|
|
561
|
+
url = m.group(1)
|
|
562
|
+
has_opts = m.group(2) is not None
|
|
563
|
+
method = "GET"
|
|
564
|
+
if has_opts:
|
|
565
|
+
# Collect bracket-balanced options block starting from the
|
|
566
|
+
# position of the opening '{' within `line`.
|
|
567
|
+
opts_block, _end = _collect_balanced_block(lines, i - 1, m.end() - 1)
|
|
568
|
+
if opts_block:
|
|
569
|
+
mm = method_re.search(opts_block)
|
|
570
|
+
if mm:
|
|
571
|
+
method = mm.group(1)
|
|
572
|
+
js_fetches.append((url, method, f"{path}:{i}"))
|
|
573
|
+
return js_fetches
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _collect_balanced_block(
|
|
577
|
+
lines: list[str],
|
|
578
|
+
start_line_idx: int,
|
|
579
|
+
start_col: int,
|
|
580
|
+
max_lines: int = 80,
|
|
581
|
+
) -> tuple[str, int]:
|
|
582
|
+
"""Collect a ``{ ... }``-balanced block starting at ``lines[start_line_idx][start_col]``.
|
|
583
|
+
|
|
584
|
+
Returns ``(block_text, last_line_idx)``. If no opening brace at the
|
|
585
|
+
starting position or block not closed within ``max_lines``, returns
|
|
586
|
+
``("", start_line_idx)``.
|
|
587
|
+
"""
|
|
588
|
+
if start_line_idx >= len(lines):
|
|
589
|
+
return "", start_line_idx
|
|
590
|
+
first_line = lines[start_line_idx]
|
|
591
|
+
# Find the opening brace at or after start_col.
|
|
592
|
+
open_pos = first_line.find("{", start_col)
|
|
593
|
+
if open_pos == -1:
|
|
594
|
+
return "", start_line_idx
|
|
595
|
+
|
|
596
|
+
depth = 0
|
|
597
|
+
collected: list[str] = []
|
|
598
|
+
end_line = start_line_idx
|
|
599
|
+
in_string: str | None = None # quote char or None
|
|
600
|
+
|
|
601
|
+
for line_idx in range(start_line_idx, min(start_line_idx + max_lines, len(lines))):
|
|
602
|
+
line = lines[line_idx]
|
|
603
|
+
col_start = open_pos if line_idx == start_line_idx else 0
|
|
604
|
+
for col in range(col_start, len(line)):
|
|
605
|
+
ch = line[col]
|
|
606
|
+
if in_string is not None:
|
|
607
|
+
if ch == in_string and line[col - 1: col] != "\\":
|
|
608
|
+
in_string = None
|
|
609
|
+
continue
|
|
610
|
+
if ch in ("'", '"', "`"):
|
|
611
|
+
in_string = ch
|
|
612
|
+
continue
|
|
613
|
+
if ch == "{":
|
|
614
|
+
depth += 1
|
|
615
|
+
elif ch == "}":
|
|
616
|
+
depth -= 1
|
|
617
|
+
if depth == 0:
|
|
618
|
+
collected.append(line[col_start: col + 1])
|
|
619
|
+
return "".join(collected), line_idx
|
|
620
|
+
collected.append(line[col_start:] + "\n")
|
|
621
|
+
end_line = line_idx
|
|
622
|
+
return "".join(collected), end_line
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _check_js_surface_coverage(ctx) -> list[GateFinding]:
|
|
626
|
+
import re as _re
|
|
627
|
+
snapshots = ctx.file_snapshots or {}
|
|
628
|
+
|
|
629
|
+
all_js = set()
|
|
630
|
+
for path, snap in snapshots.items():
|
|
631
|
+
if not hasattr(snap, "text") or not snap.text:
|
|
632
|
+
continue
|
|
633
|
+
for m in _re.finditer(r'\b(_?[A-Z][A-Z_0-9]*JS)\b', snap.text):
|
|
634
|
+
name = m.group(1)
|
|
635
|
+
if name.endswith("_JS"):
|
|
636
|
+
all_js.add(name)
|
|
637
|
+
|
|
638
|
+
checked = {
|
|
639
|
+
"OPERATOR_JS",
|
|
640
|
+
"OPERATOR_FILES_JS",
|
|
641
|
+
"LAUNCHER_JS",
|
|
642
|
+
"OPERATOR_SSH_JS",
|
|
643
|
+
"BACK_NAV_JS",
|
|
644
|
+
"_SIDEBAR_JS",
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return assess_js_surface_coverage(sorted(all_js), sorted(checked))
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# ---------------------------------------------------------------------------
|
|
651
|
+
# Cluster 31: Exception Swallowing
|
|
652
|
+
# ---------------------------------------------------------------------------
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _check_exception_swallowing(ctx) -> list[GateFinding]:
|
|
656
|
+
snapshots = ctx.file_snapshots or {}
|
|
657
|
+
findings: list[GateFinding] = []
|
|
658
|
+
for path, snap in snapshots.items():
|
|
659
|
+
if not is_source_file(path) or not hasattr(snap, "text") or not snap.text:
|
|
660
|
+
continue
|
|
661
|
+
if get_language_id(path) != "python":
|
|
662
|
+
continue
|
|
663
|
+
findings.extend(assess_exception_swallowing(path, snap.text))
|
|
664
|
+
if len(findings) >= _MAX_FINDINGS_PER_CLUSTER:
|
|
665
|
+
break
|
|
666
|
+
return findings
|