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,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