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,336 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from vigil_forensic._shared import EvidenceReference, GateCategory, GateImpact, GateSeverity, RepairKind
|
|
6
|
+
from vigil_forensic.gate_models import PostExecGateContext
|
|
7
|
+
from ..source_analysis import extract_functions, get_language_id, is_source_file
|
|
8
|
+
from .common import build_check_result, build_finding, is_generated_file, iter_touched_snapshots, max_nesting_depth, normalize_path
|
|
9
|
+
|
|
10
|
+
_log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _suppress_for_data_module(
|
|
14
|
+
snapshot,
|
|
15
|
+
file_warn: int,
|
|
16
|
+
file_revise: int,
|
|
17
|
+
fn_warn: int,
|
|
18
|
+
fn_revise: int,
|
|
19
|
+
nest_warn: int,
|
|
20
|
+
nest_revise: int,
|
|
21
|
+
) -> bool:
|
|
22
|
+
"""Return True iff legacy thresholds would emit ANY size_complexity finding.
|
|
23
|
+
|
|
24
|
+
Used by the Sprint B4 data_module branch to decide whether a single
|
|
25
|
+
``applicability=not_applicable`` summary finding should be surfaced.
|
|
26
|
+
If legacy thresholds would not flag the file anyway, skip silently
|
|
27
|
+
(no NA finding inflation).
|
|
28
|
+
"""
|
|
29
|
+
if snapshot.line_count >= file_warn:
|
|
30
|
+
return True
|
|
31
|
+
if is_source_file(snapshot.path):
|
|
32
|
+
try:
|
|
33
|
+
for fi in extract_functions(snapshot.path, snapshot.text):
|
|
34
|
+
if fi.line_count >= fn_warn:
|
|
35
|
+
return True
|
|
36
|
+
except Exception: # pragma: no cover -- fail-open
|
|
37
|
+
return False
|
|
38
|
+
if get_language_id(snapshot.path) == "python":
|
|
39
|
+
if max_nesting_depth(snapshot.text) >= nest_warn:
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
# NOTE: the size_complexity.zone_overload sub-check was REMOVED (FP fix).
|
|
44
|
+
# It inferred "responsibility zones" from function-name prefixes — the exact
|
|
45
|
+
# same name-prefix heuristic as god_object_zones — and double-reported every
|
|
46
|
+
# file that god_object_zones already flagged (e.g. 7 + 5 findings on the same
|
|
47
|
+
# filelock files). The zone heuristic now has a single home in the opt-in
|
|
48
|
+
# god_object_zones gate (see self_audit._NOISY_OPT_IN_GATES). size_complexity
|
|
49
|
+
# keeps only its objective size / function-length / nesting budget checks.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_size_complexity_checks(ctx: PostExecGateContext):
|
|
53
|
+
findings = []
|
|
54
|
+
profile = ctx.repo_profile
|
|
55
|
+
thresholds = (profile.size_thresholds if profile is not None else {}) or {}
|
|
56
|
+
file_warn = int(thresholds.get("file_warn", 600))
|
|
57
|
+
file_revise = int(thresholds.get("file_revise", 800))
|
|
58
|
+
fn_warn = int(thresholds.get("function_warn", 80))
|
|
59
|
+
fn_revise = int(thresholds.get("function_revise", 120))
|
|
60
|
+
nest_warn = int(thresholds.get("nesting_warn", 4))
|
|
61
|
+
nest_revise = int(thresholds.get("nesting_revise", 6))
|
|
62
|
+
# Sprint B4: optional per-file role map (ProjectContext.file_roles).
|
|
63
|
+
# None when ctx.project_context is absent (legacy path) or when the
|
|
64
|
+
# light-tier build is in use. Gate behavior degrades gracefully: when
|
|
65
|
+
# file_roles is None or role.kind is in {"code_module", "generated",
|
|
66
|
+
# "unknown", "test_module"}, legacy threshold + F16d marker + allowlist
|
|
67
|
+
# logic applies unchanged. Only role.kind == "data_module" takes the
|
|
68
|
+
# new suppression branch.
|
|
69
|
+
file_roles = getattr(getattr(ctx, "project_context", None), "file_roles", None)
|
|
70
|
+
|
|
71
|
+
# Sprint C2 (2026-04-23): prefer TestTopology.is_test_path for test-path
|
|
72
|
+
# skip. Legacy basename check preserved as fallback.
|
|
73
|
+
topology = getattr(getattr(ctx, "project_context", None), "test_topology", None)
|
|
74
|
+
|
|
75
|
+
for snapshot in iter_touched_snapshots(ctx):
|
|
76
|
+
if not snapshot.exists:
|
|
77
|
+
continue
|
|
78
|
+
norm_path = snapshot.path.replace("\\", "/")
|
|
79
|
+
if topology is not None:
|
|
80
|
+
if topology.is_test_path(norm_path):
|
|
81
|
+
continue
|
|
82
|
+
elif norm_path.split("/")[-1].startswith("test_"):
|
|
83
|
+
continue
|
|
84
|
+
if profile and snapshot.path in profile.allowlisted_large_files:
|
|
85
|
+
continue
|
|
86
|
+
# F16d: skip auto-generated files and sanctioned asset bundles.
|
|
87
|
+
if is_generated_file(snapshot.text):
|
|
88
|
+
_log.debug(
|
|
89
|
+
"size_complexity: skipping generated/sanctioned file %s",
|
|
90
|
+
snapshot.path,
|
|
91
|
+
)
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Sprint B4: density-based suppression for data-dominant modules.
|
|
95
|
+
# Catalog/registry files (e.g. GATE_SPECS = (...)) are measured by
|
|
96
|
+
# thresholds designed for code; surface a single not_applicable
|
|
97
|
+
# finding when legacy thresholds would have flagged, then skip the
|
|
98
|
+
# rest of the per-file checks.
|
|
99
|
+
if file_roles is not None:
|
|
100
|
+
role = file_roles.role(snapshot.path)
|
|
101
|
+
if role.kind == "data_module":
|
|
102
|
+
if _suppress_for_data_module(
|
|
103
|
+
snapshot,
|
|
104
|
+
file_warn,
|
|
105
|
+
file_revise,
|
|
106
|
+
fn_warn,
|
|
107
|
+
fn_revise,
|
|
108
|
+
nest_warn,
|
|
109
|
+
nest_revise,
|
|
110
|
+
):
|
|
111
|
+
pct = int(round(role.metrics.data_density_ratio * 100))
|
|
112
|
+
findings.append(
|
|
113
|
+
build_finding(
|
|
114
|
+
check_id="size.applicability_suppressed",
|
|
115
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
116
|
+
title="Size/complexity thresholds not applicable to data module",
|
|
117
|
+
severity=GateSeverity.LOW,
|
|
118
|
+
impact=GateImpact.WARN,
|
|
119
|
+
summary=(
|
|
120
|
+
f"{snapshot.path} is a data-dominant module "
|
|
121
|
+
f"({pct}% literal content); size thresholds "
|
|
122
|
+
f"designed for code modules do not apply."
|
|
123
|
+
),
|
|
124
|
+
recommendation=(
|
|
125
|
+
"Treat catalog/registry files as data, not "
|
|
126
|
+
"code: exempt from LOC/function/nesting "
|
|
127
|
+
"budgets."
|
|
128
|
+
),
|
|
129
|
+
evidence=[EvidenceReference(kind="file", path=snapshot.path)],
|
|
130
|
+
repair_kind="",
|
|
131
|
+
executor_action="",
|
|
132
|
+
proof_required="",
|
|
133
|
+
allowlist_allowed=True,
|
|
134
|
+
applicability="not_applicable",
|
|
135
|
+
applicability_reason=role.reason,
|
|
136
|
+
analysis_mode="role_map",
|
|
137
|
+
confidence=0.9,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
_log.debug(
|
|
141
|
+
"size_complexity: skipping data_module %s (data_density=%.2f code_density=%.2f)",
|
|
142
|
+
snapshot.path,
|
|
143
|
+
role.metrics.data_density_ratio,
|
|
144
|
+
role.metrics.code_density_ratio,
|
|
145
|
+
)
|
|
146
|
+
continue
|
|
147
|
+
# Other role kinds fall through to legacy path.
|
|
148
|
+
if snapshot.line_count >= file_revise:
|
|
149
|
+
findings.append(
|
|
150
|
+
build_finding(
|
|
151
|
+
check_id="size.file_too_large",
|
|
152
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
153
|
+
title="Touched file exceeds the revise threshold",
|
|
154
|
+
severity=GateSeverity.HIGH,
|
|
155
|
+
impact=GateImpact.REVISE,
|
|
156
|
+
summary=f"{snapshot.path} is {snapshot.line_count} lines; profile revise threshold is {file_revise}.",
|
|
157
|
+
recommendation="Split responsibilities or move new logic into smaller modules.",
|
|
158
|
+
evidence=[EvidenceReference(kind="file", path=snapshot.path)],
|
|
159
|
+
repair_kind=RepairKind.SPLIT_MODULE.value,
|
|
160
|
+
executor_action=f"Split {snapshot.path} — {snapshot.line_count} lines exceeds {file_revise}-line threshold; extract each responsibility into a focused module",
|
|
161
|
+
proof_required="file below threshold after split; grep confirms no logic removed",
|
|
162
|
+
allowlist_allowed=False,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
elif snapshot.line_count >= file_warn:
|
|
166
|
+
findings.append(
|
|
167
|
+
build_finding(
|
|
168
|
+
check_id="size.file_warn",
|
|
169
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
170
|
+
title="Touched file exceeds the warning threshold",
|
|
171
|
+
severity=GateSeverity.MEDIUM,
|
|
172
|
+
impact=GateImpact.REVISE,
|
|
173
|
+
summary=f"{snapshot.path} is {snapshot.line_count} lines; profile warning threshold is {file_warn}.",
|
|
174
|
+
recommendation="Keep the file from becoming a new god-file.",
|
|
175
|
+
evidence=[EvidenceReference(kind="file", path=snapshot.path)],
|
|
176
|
+
repair_kind=RepairKind.SPLIT_MODULE.value,
|
|
177
|
+
executor_action=f"Watch {snapshot.path} — {snapshot.line_count} lines approaching {file_revise}-line revise threshold",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
if is_source_file(snapshot.path):
|
|
181
|
+
for fi in extract_functions(snapshot.path, snapshot.text):
|
|
182
|
+
if fi.line_count >= fn_revise:
|
|
183
|
+
findings.append(
|
|
184
|
+
build_finding(
|
|
185
|
+
check_id="size.function_too_large",
|
|
186
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
187
|
+
title="Touched function exceeds the revise threshold",
|
|
188
|
+
severity=GateSeverity.HIGH,
|
|
189
|
+
impact=GateImpact.REVISE,
|
|
190
|
+
summary=f"{snapshot.path}::{fi.name} is {fi.line_count} lines; profile revise threshold is {fn_revise}.",
|
|
191
|
+
recommendation="Split orchestration, logic, and rendering into smaller helpers.",
|
|
192
|
+
evidence=[EvidenceReference(kind="file", path=snapshot.path, detail=fi.name)],
|
|
193
|
+
repair_kind=RepairKind.REFACTOR.value,
|
|
194
|
+
executor_action=f"Refactor {snapshot.path}::{fi.name} — {fi.line_count} lines; extract sub-steps into named helpers",
|
|
195
|
+
proof_required="function below threshold; tests still pass",
|
|
196
|
+
allowlist_allowed=False,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
break
|
|
200
|
+
if fi.line_count >= fn_warn:
|
|
201
|
+
findings.append(
|
|
202
|
+
build_finding(
|
|
203
|
+
check_id="size.function_warn",
|
|
204
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
205
|
+
title="Touched function exceeds the warning threshold",
|
|
206
|
+
severity=GateSeverity.MEDIUM,
|
|
207
|
+
impact=GateImpact.REVISE,
|
|
208
|
+
summary=f"{snapshot.path}::{fi.name} is {fi.line_count} lines; profile warning threshold is {fn_warn}.",
|
|
209
|
+
recommendation="Watch for mixed responsibility growth.",
|
|
210
|
+
evidence=[EvidenceReference(kind="file", path=snapshot.path, detail=fi.name)],
|
|
211
|
+
repair_kind=RepairKind.REFACTOR.value,
|
|
212
|
+
executor_action=f"Watch {snapshot.path}::{fi.name} — {fi.line_count} lines approaching revise threshold",
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
break
|
|
216
|
+
if get_language_id(snapshot.path) == "python":
|
|
217
|
+
nesting = max_nesting_depth(snapshot.text)
|
|
218
|
+
if nesting >= nest_revise:
|
|
219
|
+
findings.append(
|
|
220
|
+
build_finding(
|
|
221
|
+
check_id="size.nesting_too_high",
|
|
222
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
223
|
+
title="Touched code exceeds nesting threshold",
|
|
224
|
+
severity=GateSeverity.HIGH,
|
|
225
|
+
impact=GateImpact.REVISE,
|
|
226
|
+
summary=f"{snapshot.path} reaches nesting depth {nesting}; profile revise threshold is {nest_revise}.",
|
|
227
|
+
recommendation="Flatten control flow or extract helpers.",
|
|
228
|
+
evidence=[EvidenceReference(kind="file", path=snapshot.path)],
|
|
229
|
+
repair_kind=RepairKind.REFACTOR.value,
|
|
230
|
+
executor_action=f"Flatten {snapshot.path} — nesting depth {nesting} exceeds {nest_revise}; use early returns",
|
|
231
|
+
proof_required="nesting depth below threshold; tests still pass",
|
|
232
|
+
allowlist_allowed=False,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
elif nesting >= nest_warn:
|
|
236
|
+
findings.append(
|
|
237
|
+
build_finding(
|
|
238
|
+
check_id="size.nesting_warn",
|
|
239
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
240
|
+
title="Touched code exceeds warning nesting threshold",
|
|
241
|
+
severity=GateSeverity.MEDIUM,
|
|
242
|
+
impact=GateImpact.REVISE,
|
|
243
|
+
summary=f"{snapshot.path} reaches nesting depth {nesting}; profile warning threshold is {nest_warn}.",
|
|
244
|
+
recommendation="Prefer early exits and smaller helpers.",
|
|
245
|
+
evidence=[EvidenceReference(kind="file", path=snapshot.path)],
|
|
246
|
+
repair_kind=RepairKind.REFACTOR.value,
|
|
247
|
+
executor_action=f"Watch {snapshot.path} — nesting depth {nesting} approaching {nest_revise} revise threshold",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
# size_complexity.zone_overload removed (FP fix): name-prefix zone
|
|
251
|
+
# inference now lives only in the opt-in god_object_zones gate.
|
|
252
|
+
return build_check_result(check_id="size_complexity", category=GateCategory.SIZE_COMPLEXITY, findings=findings)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
# hotspot_inflation gate
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
_MODE_DO_NOT_TOUCH = "do_not_touch_without_runtime_trace"
|
|
260
|
+
_MODE_FORENSIC_FIRST = "forensic_first"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def run_hotspot_inflation_checks(ctx: PostExecGateContext):
|
|
264
|
+
"""Emit a finding for every touched file that is a high-risk hotspot in ctx.maps.
|
|
265
|
+
|
|
266
|
+
Modes that trigger findings:
|
|
267
|
+
- do_not_touch_without_runtime_trace -> HIGH severity
|
|
268
|
+
- forensic_first -> MEDIUM severity
|
|
269
|
+
|
|
270
|
+
No file-fallback: hotspot data is only available via ctx.maps.
|
|
271
|
+
When ctx.maps is absent or missing, return empty findings (fail-open).
|
|
272
|
+
"""
|
|
273
|
+
if ctx.maps is None or getattr(ctx.maps, "missing", False):
|
|
274
|
+
_log.debug("hotspot_inflation: ctx.maps not available -- skipping")
|
|
275
|
+
return build_check_result(
|
|
276
|
+
check_id="hotspot_inflation",
|
|
277
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
278
|
+
notes=("maps not available -- skipping",),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
hotspots = getattr(ctx.maps, "hotspot", ()) or ()
|
|
282
|
+
hotspot_by_target: dict[str, object] = {
|
|
283
|
+
normalize_path(h.target): h for h in hotspots
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
findings = []
|
|
287
|
+
for raw_path in (ctx.touched_files or ()):
|
|
288
|
+
normalized = normalize_path(raw_path)
|
|
289
|
+
h = hotspot_by_target.get(normalized)
|
|
290
|
+
if h is None:
|
|
291
|
+
continue
|
|
292
|
+
mode = (getattr(h, "recommended_mode", "") or "").lower()
|
|
293
|
+
if mode == _MODE_DO_NOT_TOUCH:
|
|
294
|
+
severity = GateSeverity.HIGH
|
|
295
|
+
impact = GateImpact.REVISE
|
|
296
|
+
recommendation = (
|
|
297
|
+
"Capture startup trace + regression tests before merging. "
|
|
298
|
+
"This file must not be modified without a runtime trace per map_builder policy."
|
|
299
|
+
)
|
|
300
|
+
elif mode == _MODE_FORENSIC_FIRST:
|
|
301
|
+
severity = GateSeverity.MEDIUM
|
|
302
|
+
impact = GateImpact.REVISE
|
|
303
|
+
recommendation = (
|
|
304
|
+
"Run forensic gates + authority check before refactoring. "
|
|
305
|
+
"This file is flagged forensic_first in the hotspot map."
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
_log.debug("hotspot_inflation: %s mode=%r -- not actionable, skipping", normalized, mode)
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
hotspot_score = getattr(h, "hotspot_score", 0)
|
|
312
|
+
findings.append(
|
|
313
|
+
build_finding(
|
|
314
|
+
check_id="hotspot_inflation",
|
|
315
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
316
|
+
title=f"Touched high-risk hotspot ({mode})",
|
|
317
|
+
severity=severity,
|
|
318
|
+
impact=impact,
|
|
319
|
+
summary=(
|
|
320
|
+
f"{normalized}: hotspot_score={hotspot_score}, mode={mode}. "
|
|
321
|
+
f"Modifying this file requires pre-change forensic trace per map_builder policy."
|
|
322
|
+
),
|
|
323
|
+
recommendation=recommendation,
|
|
324
|
+
evidence=(EvidenceReference(kind="file", path=normalized),),
|
|
325
|
+
repair_kind=RepairKind.REFACTOR.value,
|
|
326
|
+
executor_action="Address finding details",
|
|
327
|
+
proof_required="Performance acceptable",
|
|
328
|
+
allowlist_allowed=False,
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return build_check_result(
|
|
333
|
+
check_id="hotspot_inflation",
|
|
334
|
+
category=GateCategory.SIZE_COMPLEXITY,
|
|
335
|
+
findings=findings,
|
|
336
|
+
)
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Stuck feature flag forensic gate.
|
|
2
|
+
|
|
3
|
+
Detects module-level UPPER_SNAKE_CASE constants assigned to ``False`` that:
|
|
4
|
+
1. are referenced inside an ``if`` test somewhere in the touched files,
|
|
5
|
+
2. are never reassigned to a non-False value anywhere in the touched files,
|
|
6
|
+
3. are not part of a re-export chain (only False-default literals count).
|
|
7
|
+
|
|
8
|
+
The result is a "stuck flag": code is permanently gated off and forgotten.
|
|
9
|
+
Real-world example from this codebase: ``plan_review_ran = False`` hardcoded
|
|
10
|
+
with the actual review code never running.
|
|
11
|
+
|
|
12
|
+
Project-agnostic — works for any Python codebase. Operates strictly on
|
|
13
|
+
``ctx.touched_files`` (only ``.py``); files outside ``ctx.project_dir`` are
|
|
14
|
+
skipped.
|
|
15
|
+
|
|
16
|
+
Algorithm:
|
|
17
|
+
Pass 1: collect module-level ``Assign(target=Name(UPPER), value=Constant(False))``
|
|
18
|
+
(excluding TypeVar-style and dunder names).
|
|
19
|
+
Pass 2: across ALL touched .py files, locate any other assignment to the
|
|
20
|
+
same name whose RHS is NOT ``Constant(False)`` — anything else
|
|
21
|
+
(True, function call, attribute, name reference) marks the name as
|
|
22
|
+
"dynamic" and disqualifies it.
|
|
23
|
+
Pass 3: across ALL touched .py files, scan ``If.test`` for usage of the
|
|
24
|
+
candidate name (as bare ``Name``, ``not NAME``, inside ``BoolOp``,
|
|
25
|
+
or as left side of ``Compare``).
|
|
26
|
+
|
|
27
|
+
A finding is emitted for each name with at least one If usage AND no dynamic
|
|
28
|
+
assignment.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import ast
|
|
33
|
+
import logging
|
|
34
|
+
import re
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Iterable
|
|
37
|
+
|
|
38
|
+
from vigil_forensic.gate_models import PostExecGateContext
|
|
39
|
+
from vigil_forensic.meta_findings import emit_meta_finding
|
|
40
|
+
from vigil_forensic._shared import (
|
|
41
|
+
EvidenceReference,
|
|
42
|
+
GateCategory,
|
|
43
|
+
GateCheckResult,
|
|
44
|
+
GateImpact,
|
|
45
|
+
GateSeverity,
|
|
46
|
+
RepairKind,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from .common import build_check_result, build_finding, normalize_path
|
|
50
|
+
|
|
51
|
+
_log = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
_CATEGORY = GateCategory.DRIFT
|
|
54
|
+
_CHECK_ID = "stuck_feature_flag"
|
|
55
|
+
|
|
56
|
+
# UPPER_SNAKE_CASE: leading underscore allowed (private module constants).
|
|
57
|
+
# At least one alpha char and one underscore-or-alphanum to avoid lone-letter
|
|
58
|
+
# matches like ``X = False`` (which is almost never a feature flag).
|
|
59
|
+
_UPPER_SNAKE_RE = re.compile(r"^_?[A-Z][A-Z0-9_]*[A-Z0-9]$")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_false_constant(node: ast.AST) -> bool:
|
|
63
|
+
return (
|
|
64
|
+
isinstance(node, ast.Constant)
|
|
65
|
+
and isinstance(node.value, bool)
|
|
66
|
+
and node.value is False
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _module_level_false_assign_target(node: ast.AST) -> str | None:
|
|
71
|
+
"""Return the constant name iff *node* is ``NAME = False`` at module top.
|
|
72
|
+
|
|
73
|
+
Caller is responsible for ensuring *node* is a direct child of the module
|
|
74
|
+
(not inside a function/class body).
|
|
75
|
+
"""
|
|
76
|
+
if not isinstance(node, ast.Assign):
|
|
77
|
+
return None
|
|
78
|
+
if len(node.targets) != 1:
|
|
79
|
+
return None
|
|
80
|
+
target = node.targets[0]
|
|
81
|
+
if not isinstance(target, ast.Name):
|
|
82
|
+
return None
|
|
83
|
+
name = target.id
|
|
84
|
+
if not _UPPER_SNAKE_RE.match(name):
|
|
85
|
+
return None
|
|
86
|
+
if name.startswith("__") and name.endswith("__"):
|
|
87
|
+
return None
|
|
88
|
+
if not _is_false_constant(node.value):
|
|
89
|
+
return None
|
|
90
|
+
return name
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _module_level_assign_targets(node: ast.AST) -> list[tuple[str, ast.AST]]:
|
|
94
|
+
"""Return (name, value) pairs for any module-level ``NAME = <expr>``
|
|
95
|
+
assignment whose target is a single Name. Used by the dynamic-assignment
|
|
96
|
+
pass to detect "assigned to non-False elsewhere".
|
|
97
|
+
"""
|
|
98
|
+
out: list[tuple[str, ast.AST]] = []
|
|
99
|
+
if isinstance(node, ast.Assign):
|
|
100
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
101
|
+
out.append((node.targets[0].id, node.value))
|
|
102
|
+
elif isinstance(node, ast.AugAssign) and isinstance(node.target, ast.Name):
|
|
103
|
+
# ``X |= True`` etc. — counts as dynamic.
|
|
104
|
+
out.append((node.target.id, node.value))
|
|
105
|
+
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
106
|
+
if node.value is not None:
|
|
107
|
+
out.append((node.target.id, node.value))
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _walk_assigns_anywhere(tree: ast.AST) -> Iterable[tuple[str, ast.AST]]:
|
|
112
|
+
"""Yield (name, value) for every ``Name = <expr>`` assignment found
|
|
113
|
+
anywhere in the tree (module-level OR inside functions/classes). Used
|
|
114
|
+
to detect cross-scope dynamic reassignment of a candidate flag.
|
|
115
|
+
"""
|
|
116
|
+
for node in ast.walk(tree):
|
|
117
|
+
if isinstance(node, ast.Assign):
|
|
118
|
+
for tgt in node.targets:
|
|
119
|
+
if isinstance(tgt, ast.Name):
|
|
120
|
+
yield (tgt.id, node.value)
|
|
121
|
+
elif isinstance(tgt, (ast.Tuple, ast.List)):
|
|
122
|
+
for elt in tgt.elts:
|
|
123
|
+
if isinstance(elt, ast.Name):
|
|
124
|
+
yield (elt.id, node.value)
|
|
125
|
+
elif isinstance(node, ast.AugAssign) and isinstance(node.target, ast.Name):
|
|
126
|
+
yield (node.target.id, node.value)
|
|
127
|
+
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
128
|
+
if node.value is not None:
|
|
129
|
+
yield (node.target.id, node.value)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _name_is_used_in_if_test(test: ast.AST, name: str) -> bool:
|
|
133
|
+
"""Return True if *test* references *name* in any of the supported forms.
|
|
134
|
+
|
|
135
|
+
Supported:
|
|
136
|
+
* ``Name(id=NAME)`` — bare reference
|
|
137
|
+
* ``UnaryOp(op=Not, operand=Name(id=NAME))`` — ``if not NAME``
|
|
138
|
+
* ``BoolOp(values=[..., Name(id=NAME), ...])`` — ``if NAME and X``
|
|
139
|
+
* ``Compare(left=Name(id=NAME), ...)`` — ``if NAME == X``
|
|
140
|
+
"""
|
|
141
|
+
if isinstance(test, ast.Name) and test.id == name:
|
|
142
|
+
return True
|
|
143
|
+
if (
|
|
144
|
+
isinstance(test, ast.UnaryOp)
|
|
145
|
+
and isinstance(test.op, ast.Not)
|
|
146
|
+
and isinstance(test.operand, ast.Name)
|
|
147
|
+
and test.operand.id == name
|
|
148
|
+
):
|
|
149
|
+
return True
|
|
150
|
+
if isinstance(test, ast.BoolOp):
|
|
151
|
+
for v in test.values:
|
|
152
|
+
if _name_is_used_in_if_test(v, name):
|
|
153
|
+
return True
|
|
154
|
+
if isinstance(test, ast.Compare):
|
|
155
|
+
if isinstance(test.left, ast.Name) and test.left.id == name:
|
|
156
|
+
return True
|
|
157
|
+
for cmp_node in test.comparators:
|
|
158
|
+
if isinstance(cmp_node, ast.Name) and cmp_node.id == name:
|
|
159
|
+
return True
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _collect_if_usage_sites(tree: ast.AST, name: str) -> list[int]:
|
|
164
|
+
"""Return 1-based line numbers of every ``If`` whose test references *name*."""
|
|
165
|
+
lines: list[int] = []
|
|
166
|
+
for node in ast.walk(tree):
|
|
167
|
+
if isinstance(node, ast.If) and _name_is_used_in_if_test(node.test, name):
|
|
168
|
+
line = getattr(node, "lineno", 0) or 0
|
|
169
|
+
if line:
|
|
170
|
+
lines.append(int(line))
|
|
171
|
+
return lines
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _resolve_target_path(project_dir: Path, raw_path: str) -> Path | None:
|
|
175
|
+
"""Resolve *raw_path* to an absolute Path inside *project_dir*. Return
|
|
176
|
+
``None`` for files outside the project, missing files, or non-``.py``
|
|
177
|
+
files.
|
|
178
|
+
"""
|
|
179
|
+
rel = normalize_path(raw_path)
|
|
180
|
+
if not rel.lower().endswith(".py"):
|
|
181
|
+
return None
|
|
182
|
+
candidate = (project_dir / rel).resolve()
|
|
183
|
+
try:
|
|
184
|
+
project_resolved = project_dir.resolve()
|
|
185
|
+
except OSError:
|
|
186
|
+
return None
|
|
187
|
+
try:
|
|
188
|
+
candidate.relative_to(project_resolved)
|
|
189
|
+
except ValueError:
|
|
190
|
+
return None
|
|
191
|
+
if not candidate.exists() or not candidate.is_file():
|
|
192
|
+
return None
|
|
193
|
+
return candidate
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def run_stuck_feature_flag_checks(ctx: PostExecGateContext) -> GateCheckResult:
|
|
197
|
+
"""Detect stuck feature flags in ``ctx.touched_files`` (Python only).
|
|
198
|
+
|
|
199
|
+
Returns a GateCheckResult with one finding per stuck flag (advisory WARN).
|
|
200
|
+
"""
|
|
201
|
+
project_dir = ctx.project_dir
|
|
202
|
+
touched = tuple(ctx.touched_files or ())
|
|
203
|
+
if not touched:
|
|
204
|
+
return build_check_result(
|
|
205
|
+
check_id=_CHECK_ID,
|
|
206
|
+
category=_CATEGORY,
|
|
207
|
+
notes=[f"{_CHECK_ID}: no touched files"],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Maps name -> (rel_path, line). Uses first-seen False assignment;
|
|
211
|
+
# subsequent False assignments to the same name are tolerated (they are
|
|
212
|
+
# still "False" — not dynamic). The recorded site is the canonical
|
|
213
|
+
# evidence pointer in the finding.
|
|
214
|
+
false_assignments: dict[str, tuple[str, int]] = {}
|
|
215
|
+
# Maps name -> list of (rel_path, line) where it is reassigned to non-False
|
|
216
|
+
# (anywhere — module level, function body, class body). Presence
|
|
217
|
+
# disqualifies the name.
|
|
218
|
+
dynamic_assignments: dict[str, list[tuple[str, int]]] = {}
|
|
219
|
+
# Maps name -> list of (rel_path, line) for if-test usage sites.
|
|
220
|
+
if_usage_sites: dict[str, list[tuple[str, int]]] = {}
|
|
221
|
+
|
|
222
|
+
for raw_path in touched:
|
|
223
|
+
abs_path = _resolve_target_path(project_dir, raw_path)
|
|
224
|
+
if abs_path is None:
|
|
225
|
+
continue
|
|
226
|
+
rel_path = normalize_path(raw_path)
|
|
227
|
+
try:
|
|
228
|
+
source = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
229
|
+
except OSError as exc:
|
|
230
|
+
emit_meta_finding(
|
|
231
|
+
"meta.file_unreadable",
|
|
232
|
+
path=rel_path,
|
|
233
|
+
detail=f"{type(exc).__name__}: {exc}",
|
|
234
|
+
)
|
|
235
|
+
continue
|
|
236
|
+
try:
|
|
237
|
+
tree = ast.parse(source, filename=str(abs_path))
|
|
238
|
+
except SyntaxError as exc:
|
|
239
|
+
emit_meta_finding(
|
|
240
|
+
"meta.syntax_parse_error",
|
|
241
|
+
path=rel_path,
|
|
242
|
+
detail=f"line {exc.lineno}: {exc.msg}",
|
|
243
|
+
)
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
# Pass 1 — module-level NAME = False
|
|
247
|
+
for node in ast.iter_child_nodes(tree):
|
|
248
|
+
name = _module_level_false_assign_target(node)
|
|
249
|
+
if name is None:
|
|
250
|
+
continue
|
|
251
|
+
line = int(getattr(node, "lineno", 0) or 0)
|
|
252
|
+
false_assignments.setdefault(name, (rel_path, line))
|
|
253
|
+
|
|
254
|
+
# Pass 2 — any assignment anywhere whose RHS is NOT Constant(False)
|
|
255
|
+
for name, value in _walk_assigns_anywhere(tree):
|
|
256
|
+
if not _UPPER_SNAKE_RE.match(name):
|
|
257
|
+
continue
|
|
258
|
+
if name.startswith("__") and name.endswith("__"):
|
|
259
|
+
continue
|
|
260
|
+
if _is_false_constant(value):
|
|
261
|
+
continue
|
|
262
|
+
line = int(getattr(value, "lineno", 0) or 0)
|
|
263
|
+
dynamic_assignments.setdefault(name, []).append((rel_path, line))
|
|
264
|
+
|
|
265
|
+
# Pass 3 — if-test usage scan. Only do this for names that survived
|
|
266
|
+
# pass 1 + pass 2 (cheap optimisation; correctness is unchanged).
|
|
267
|
+
candidates = {
|
|
268
|
+
name for name in false_assignments if name not in dynamic_assignments
|
|
269
|
+
}
|
|
270
|
+
if not candidates:
|
|
271
|
+
return build_check_result(
|
|
272
|
+
check_id=_CHECK_ID,
|
|
273
|
+
category=_CATEGORY,
|
|
274
|
+
notes=[f"{_CHECK_ID}: no candidate False-default constants found"],
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
for raw_path in touched:
|
|
278
|
+
abs_path = _resolve_target_path(project_dir, raw_path)
|
|
279
|
+
if abs_path is None:
|
|
280
|
+
continue
|
|
281
|
+
rel_path = normalize_path(raw_path)
|
|
282
|
+
try:
|
|
283
|
+
source = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
284
|
+
except OSError:
|
|
285
|
+
continue
|
|
286
|
+
try:
|
|
287
|
+
tree = ast.parse(source, filename=str(abs_path))
|
|
288
|
+
except SyntaxError:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
for name in candidates:
|
|
292
|
+
for line in _collect_if_usage_sites(tree, name):
|
|
293
|
+
if_usage_sites.setdefault(name, []).append((rel_path, line))
|
|
294
|
+
|
|
295
|
+
# Emit findings.
|
|
296
|
+
findings = []
|
|
297
|
+
for name in sorted(candidates):
|
|
298
|
+
usages = if_usage_sites.get(name, [])
|
|
299
|
+
if not usages:
|
|
300
|
+
continue
|
|
301
|
+
decl_path, decl_line = false_assignments[name]
|
|
302
|
+
evidence: list[EvidenceReference] = [
|
|
303
|
+
EvidenceReference(
|
|
304
|
+
kind="false_default_assignment",
|
|
305
|
+
path=decl_path,
|
|
306
|
+
detail=f"{decl_path}:{decl_line}: {name} = False",
|
|
307
|
+
)
|
|
308
|
+
]
|
|
309
|
+
for use_path, use_line in usages:
|
|
310
|
+
evidence.append(
|
|
311
|
+
EvidenceReference(
|
|
312
|
+
kind="if_test_usage",
|
|
313
|
+
path=use_path,
|
|
314
|
+
detail=f"{use_path}:{use_line}: if-test references {name}",
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
findings.append(
|
|
318
|
+
build_finding(
|
|
319
|
+
check_id=_CHECK_ID,
|
|
320
|
+
category=_CATEGORY,
|
|
321
|
+
title=f"Stuck feature flag: {name}",
|
|
322
|
+
severity=GateSeverity.MEDIUM,
|
|
323
|
+
impact=GateImpact.WARN,
|
|
324
|
+
summary=(
|
|
325
|
+
f"Module constant {name}=False at {decl_path}:{decl_line} "
|
|
326
|
+
f"is used in {len(usages)} conditional(s) but never "
|
|
327
|
+
f"reassigned anywhere in the project. Likely dead/disabled "
|
|
328
|
+
f"feature."
|
|
329
|
+
),
|
|
330
|
+
recommendation=(
|
|
331
|
+
"Either remove the flag and the gated code, or wire up a "
|
|
332
|
+
"path that sets it to True."
|
|
333
|
+
),
|
|
334
|
+
evidence=evidence,
|
|
335
|
+
repair_kind=RepairKind.REMOVE_DEAD_SURFACE.value,
|
|
336
|
+
executor_action="Resolve stuck feature flag (remove or wire)",
|
|
337
|
+
proof_required="Flag is either deleted (with its gated code) or set to True via a real code path.",
|
|
338
|
+
allowlist_allowed=True,
|
|
339
|
+
preferred_fix_shape="delete the flag + the dead branch, OR add the missing assignment path",
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
notes: list[str] = []
|
|
344
|
+
if not findings:
|
|
345
|
+
notes.append(
|
|
346
|
+
f"{_CHECK_ID}: {len(candidates)} False-default candidate(s) "
|
|
347
|
+
f"found, none with if-test usage"
|
|
348
|
+
)
|
|
349
|
+
return build_check_result(
|
|
350
|
+
check_id=_CHECK_ID,
|
|
351
|
+
category=_CATEGORY,
|
|
352
|
+
findings=findings,
|
|
353
|
+
notes=notes,
|
|
354
|
+
)
|