sourcecode 2.5.11__py3-none-any.whl → 2.5.13__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.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_analyzer.py +42 -0
- sourcecode/architecture_summary.py +75 -21
- sourcecode/classifier.py +1 -1
- sourcecode/confidence_analyzer.py +14 -0
- sourcecode/path_filters.py +48 -0
- sourcecode/prepare_context.py +24 -0
- sourcecode/reconciliation.py +187 -37
- sourcecode/repository_ir.py +32 -1
- sourcecode/spring_impact.py +35 -4
- sourcecode/summarizer.py +20 -4
- {sourcecode-2.5.11.dist-info → sourcecode-2.5.13.dist-info}/METADATA +1 -1
- {sourcecode-2.5.11.dist-info → sourcecode-2.5.13.dist-info}/RECORD +16 -16
- {sourcecode-2.5.11.dist-info → sourcecode-2.5.13.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.11.dist-info → sourcecode-2.5.13.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.11.dist-info → sourcecode-2.5.13.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -286,6 +286,11 @@ class ArchitectureAnalyzer:
|
|
|
286
286
|
# architecture claim". `_detect_layers` and its helpers append the
|
|
287
287
|
# findings' sink-neutral detail here; drained into `limitations` below.
|
|
288
288
|
self._refutations: list[str] = []
|
|
289
|
+
# Gap #2 (fail-open): a reconciliation rule that could not be evaluated
|
|
290
|
+
# is NOT a clean result — surfaced here as its own limitation, never as a
|
|
291
|
+
# refutation and never silently, so an unverified pattern cannot ship as
|
|
292
|
+
# confirmed.
|
|
293
|
+
self._reconciliation_errors: list[str] = []
|
|
289
294
|
|
|
290
295
|
# Step 1: filter paths
|
|
291
296
|
filtered = self._filter_paths(sm.file_paths)
|
|
@@ -498,6 +503,14 @@ class ArchitectureAnalyzer:
|
|
|
498
503
|
continue
|
|
499
504
|
_seen.add(_detail)
|
|
500
505
|
limitations.append(f"Architecture claim refuted on evidence: {_detail}")
|
|
506
|
+
# Fail-open sink: a rule that could not run is surfaced distinctly — the
|
|
507
|
+
# pattern it would have checked is reported UNVERIFIED, never certified.
|
|
508
|
+
_seen_err: set[str] = set()
|
|
509
|
+
for _detail in self._reconciliation_errors:
|
|
510
|
+
if _detail in _seen_err:
|
|
511
|
+
continue
|
|
512
|
+
_seen_err.add(_detail)
|
|
513
|
+
limitations.append(f"Architecture reconciliation could not run: {_detail}")
|
|
501
514
|
|
|
502
515
|
return ArchitectureAnalysis(
|
|
503
516
|
requested=True,
|
|
@@ -644,6 +657,24 @@ class ArchitectureAnalyzer:
|
|
|
644
657
|
return domains
|
|
645
658
|
|
|
646
659
|
def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
|
|
660
|
+
# Drop directory ENTRIES. `flatten_file_tree` emits every node — files AND
|
|
661
|
+
# the directories above them — so `sm.file_paths` carries `src`, `src/main`,
|
|
662
|
+
# `src/main/java/.../model` … A directory is not a file: counting it as a
|
|
663
|
+
# layer/module member makes it read as jvm-less in `runtime_census` (it holds
|
|
664
|
+
# no file OF ITS OWN, only children), and a directory that transitively holds
|
|
665
|
+
# all the Java is then refuted as "no jvm code" — the demonstrated
|
|
666
|
+
# contamination (petclinic phantom `orchestration` = `src/main`; the
|
|
667
|
+
# directory half of `data` = `.../model`). A path that is an ancestor of
|
|
668
|
+
# another path in the set is such an entry; leaf files (incl. template
|
|
669
|
+
# view-layers) are never ancestors and are kept untouched.
|
|
670
|
+
_norm = [p.replace("\\", "/") for p in paths]
|
|
671
|
+
_ancestors: set[str] = set()
|
|
672
|
+
for _p in _norm:
|
|
673
|
+
_segs = _p.split("/")
|
|
674
|
+
for _k in range(1, len(_segs)):
|
|
675
|
+
_ancestors.add("/".join(_segs[:_k]))
|
|
676
|
+
paths = [p for p, n in zip(paths, _norm) if n not in _ancestors]
|
|
677
|
+
|
|
647
678
|
# Exclude non-source paths (tests, benchmarks, docs, tooling, vendored assets)
|
|
648
679
|
# from layer scoring. Also exclude anything under a `resources/` segment: in
|
|
649
680
|
# Maven/Gradle layouts `src/main/resources/**` is bundled config/assets — e.g.
|
|
@@ -721,6 +752,11 @@ class ArchitectureAnalyzer:
|
|
|
721
752
|
for _f in reconcile_architecture_evidence(
|
|
722
753
|
pattern=pattern_name, layer_files=_layer_paths, repo_files=source_paths
|
|
723
754
|
):
|
|
755
|
+
if _f.error:
|
|
756
|
+
# A rule that could not run is surfaced always (not tied to
|
|
757
|
+
# whether this candidate wins) and never as a refutation.
|
|
758
|
+
self._reconciliation_errors.append(_f.detail)
|
|
759
|
+
continue
|
|
724
760
|
_refs.append(_f.detail)
|
|
725
761
|
if _f.rule == "composition_runtime_split":
|
|
726
762
|
# A claim that IS a runtime split must show one: JobRunr, a JVM
|
|
@@ -922,6 +958,9 @@ class ArchitectureAnalyzer:
|
|
|
922
958
|
# pruning here directly shapes the returned result (fewer layers, or a
|
|
923
959
|
# drop below 2 that falls through to flat/unknown) — surface it.
|
|
924
960
|
for _f in reconcile_code_modules(layer_files=non_empty, repo_files=paths):
|
|
961
|
+
if _f.error:
|
|
962
|
+
self._reconciliation_errors.append(_f.detail)
|
|
963
|
+
continue
|
|
925
964
|
for _bad in _f.symbols:
|
|
926
965
|
non_empty.pop(_bad, None)
|
|
927
966
|
self._refutations.append(_f.detail)
|
|
@@ -957,6 +996,9 @@ class ArchitectureAnalyzer:
|
|
|
957
996
|
# eureka "images" (five PNGs). The real modules survive untouched.
|
|
958
997
|
# INV-F1-4: same as the layered path — surface each refuted module.
|
|
959
998
|
for _f in reconcile_code_modules(layer_files=meaningful, repo_files=paths):
|
|
999
|
+
if _f.error:
|
|
1000
|
+
self._reconciliation_errors.append(_f.detail)
|
|
1001
|
+
continue
|
|
960
1002
|
for _bad in _f.symbols:
|
|
961
1003
|
meaningful.pop(_bad, None)
|
|
962
1004
|
self._refutations.append(_f.detail)
|
|
@@ -210,23 +210,38 @@ class ArchitectureSummarizer:
|
|
|
210
210
|
# welds a foreign runtime's framework onto the primary's label. Drop what
|
|
211
211
|
# the claimed stack does not attest, and never let the TYPE label — which
|
|
212
212
|
# those same frameworks drove — survive its own evidence (P2-F).
|
|
213
|
-
from sourcecode.reconciliation import
|
|
213
|
+
from sourcecode.reconciliation import (
|
|
214
|
+
has_execution_error,
|
|
215
|
+
reconcile_headline_evidence,
|
|
216
|
+
type_claim_unattested,
|
|
217
|
+
)
|
|
214
218
|
|
|
215
|
-
|
|
216
|
-
attested = [name for name in fw_names if name not in foreign]
|
|
217
|
-
dropped = [name for name in fw_names if name in foreign]
|
|
218
|
-
if dropped and not attested and type_claim_unattested(
|
|
219
|
+
_headline_findings = reconcile_headline_evidence(
|
|
219
220
|
claimed_stack=primary.stack, project_type=sm.project_type, stacks=sm.stacks
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
)
|
|
222
|
+
if has_execution_error(_headline_findings):
|
|
223
|
+
# Fail-open guard (gap #2): the attestation rule could not run, and
|
|
224
|
+
# this prose surface has no gap channel to warn on. Rather than assert
|
|
225
|
+
# an unverified weld ("C#/.NET … using JAX-RS"), withhold the framework
|
|
226
|
+
# clause — the claim is not made when its check did not execute.
|
|
227
|
+
fw_names = []
|
|
228
|
+
else:
|
|
229
|
+
foreign = frozenset(s for f in _headline_findings for s in f.symbols)
|
|
230
|
+
attested = [name for name in fw_names if name not in foreign]
|
|
231
|
+
dropped = [name for name in fw_names if name in foreign]
|
|
232
|
+
if dropped and not attested and type_claim_unattested(
|
|
233
|
+
claimed_stack=primary.stack, project_type=sm.project_type, stacks=sm.stacks
|
|
234
|
+
):
|
|
235
|
+
return self._describe_foreign_evidence(sm, stack_label, runtime, dropped)
|
|
236
|
+
fw_names = attested
|
|
223
237
|
|
|
224
238
|
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
225
239
|
if runtime:
|
|
226
240
|
# BUG #4: never assert "rest api" in the headline unless the endpoints
|
|
227
|
-
# command actually backs it
|
|
228
|
-
#
|
|
229
|
-
|
|
241
|
+
# command actually backs it. Authority is Java ONLY: extract_java_endpoints
|
|
242
|
+
# reads *.java and is blind to .kt — a Kotlin REST API would otherwise
|
|
243
|
+
# degrade to "only 0 endpoints detected" over routes it cannot see.
|
|
244
|
+
if sm.project_type == "api" and primary.stack == "java":
|
|
230
245
|
total, high = self._endpoint_support()
|
|
231
246
|
if high < _MIN_REST_ENDPOINTS_FOR_LABEL:
|
|
232
247
|
plural = "s" if total != 1 else ""
|
|
@@ -310,23 +325,62 @@ class ArchitectureSummarizer:
|
|
|
310
325
|
|
|
311
326
|
def _qualify_web_pattern(self, arch: Any, arch_line: str, sm: SourceMap) -> str:
|
|
312
327
|
"""BUG #1 (Jenkins field test): the "mvc" pattern is a directory-name
|
|
313
|
-
heuristic (dirs matching controller/model/view keywords). On a
|
|
314
|
-
|
|
328
|
+
heuristic (dirs matching controller/model/view keywords). On a JVM repo,
|
|
329
|
+
do not assert "MVC pattern with ... view layers" in prose when the
|
|
315
330
|
canonical endpoint extractor finds no HTTP controllers in the same run —
|
|
316
331
|
Jenkins (Stapler web framework) matches model/view-ish dirs but declares
|
|
317
|
-
zero Spring MVC controllers.
|
|
318
|
-
|
|
332
|
+
zero Spring MVC controllers.
|
|
333
|
+
|
|
334
|
+
This is a reconciliation rule (layer table × endpoint index), so the
|
|
335
|
+
judgement lives in F-1 (`reconcile_web_pattern_evidence`), not as an
|
|
336
|
+
ad-hoc `if` here (ADR-0005 §6.2/§8.4). The honest predicate is ZERO
|
|
337
|
+
modeled controllers, not `< 5`: with the old threshold, 1–4 real
|
|
338
|
+
controllers still degraded to the categorical "no HTTP controllers
|
|
339
|
+
detected" — a false statement. One controller backs the MVC claim; only
|
|
340
|
+
an empty index contradicts it.
|
|
341
|
+
|
|
342
|
+
**Authority = Java only.** The extractor (`extract_java_endpoints`) reads
|
|
343
|
+
`*.java`; it is blind to `.kt`/`.scala`/`.groovy`. So zero controllers is
|
|
344
|
+
a contradiction ONLY for a Java-primary repo. On Kotlin/Scala/Groovy an
|
|
345
|
+
empty index is the extractor's blindness, not absence of controllers —
|
|
346
|
+
degrading there asserted "no HTTP controllers detected" over real `.kt`
|
|
347
|
+
routes (INV-F1-1: never fire on absence of evidence). Gating the rule on
|
|
348
|
+
the JVM *family* was wrong; the boundary is the extractor's file coverage."""
|
|
349
|
+
from sourcecode.reconciliation import (
|
|
350
|
+
has_execution_error,
|
|
351
|
+
reconcile_web_pattern_evidence,
|
|
352
|
+
)
|
|
353
|
+
|
|
319
354
|
if not arch_line or arch.pattern not in ("mvc", "spring_mvc_layered"):
|
|
320
355
|
return arch_line
|
|
321
356
|
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
322
|
-
if primary is None or primary.stack not in {"java", "kotlin"}:
|
|
323
|
-
return arch_line
|
|
324
357
|
_total, high = self._endpoint_support()
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
358
|
+
# The endpoint extractor reads .java only — it is authoritative for a
|
|
359
|
+
# Java-primary repo, blind elsewhere.
|
|
360
|
+
_authoritative = bool(primary) and primary.stack == "java"
|
|
361
|
+
_findings = reconcile_web_pattern_evidence(
|
|
362
|
+
pattern=arch.pattern,
|
|
363
|
+
endpoint_evidence_authoritative=_authoritative,
|
|
364
|
+
modeled_endpoint_count=high,
|
|
365
|
+
)
|
|
329
366
|
layer_names = [l.name for l in arch.layers[:4] if l.name != "view"] if arch.layers else []
|
|
367
|
+
if has_execution_error(_findings):
|
|
368
|
+
# Fail-open guard (gap #2): the cross-check could not run. Withhold the
|
|
369
|
+
# categorical MVC label AND the "no controllers" claim (both unverified);
|
|
370
|
+
# report only the directory-derived layers, or nothing.
|
|
371
|
+
if layer_names:
|
|
372
|
+
return (
|
|
373
|
+
f"Layered code organization ({', '.join(layer_names)}; "
|
|
374
|
+
"directory-based — endpoint cross-check unavailable)."
|
|
375
|
+
)
|
|
376
|
+
return ""
|
|
377
|
+
if not _findings:
|
|
378
|
+
# Non-Java (extractor blind), not a web pattern, or ≥1 controller backs
|
|
379
|
+
# the claim → keep. On Kotlin/Scala/Groovy the MVC label stays
|
|
380
|
+
# unverified rather than falsely negated.
|
|
381
|
+
return arch_line
|
|
382
|
+
# Contradiction: zero modeled controllers. Degrade — the claim is
|
|
383
|
+
# directory names, not routes; the "no HTTP controllers" wording is TRUE.
|
|
330
384
|
if layer_names:
|
|
331
385
|
return (
|
|
332
386
|
f"Layered code organization ({', '.join(layer_names)}; "
|
sourcecode/classifier.py
CHANGED
|
@@ -207,7 +207,7 @@ class TypeClassifier:
|
|
|
207
207
|
# structured codebase (e.g. JobRunr: core + per-framework adapter modules).
|
|
208
208
|
# This is checked BEFORE the weak `bin/`-directory CLI heuristic so a build
|
|
209
209
|
# output / wrapper `bin/` dir does not mislabel a library as a CLI.
|
|
210
|
-
if stack_names &
|
|
210
|
+
if stack_names & _JVM_STACKS and self._is_multi_module(file_tree):
|
|
211
211
|
return "library"
|
|
212
212
|
|
|
213
213
|
# Weak CLI heuristic: a top-level bin/ directory (only when nothing stronger).
|
|
@@ -120,6 +120,20 @@ class ConfidenceAnalyzer:
|
|
|
120
120
|
project_type=sm.project_type,
|
|
121
121
|
stacks=sm.stacks,
|
|
122
122
|
):
|
|
123
|
+
if _rf.error:
|
|
124
|
+
# Fail-open guard (gap #2): the stack-attestation rule could
|
|
125
|
+
# not run. Never let that read as "primary attests every
|
|
126
|
+
# framework" — record it as an anomaly + gap, not silence.
|
|
127
|
+
anomalies.append(
|
|
128
|
+
"Stack/framework attestation could not be evaluated — "
|
|
129
|
+
"primary-stack attribution is UNVERIFIED"
|
|
130
|
+
)
|
|
131
|
+
gaps.append(AnalysisGap(
|
|
132
|
+
area="framework_stack_attestation",
|
|
133
|
+
reason=_rf.detail,
|
|
134
|
+
impact="medium",
|
|
135
|
+
))
|
|
136
|
+
continue
|
|
123
137
|
anomalies.append(
|
|
124
138
|
f"{', '.join(_rf.symbols)} declared by another runtime, not by the "
|
|
125
139
|
f"{_primary.stack} primary stack"
|
sourcecode/path_filters.py
CHANGED
|
@@ -120,6 +120,54 @@ def is_test_or_fixture_path(path: str) -> bool:
|
|
|
120
120
|
return False
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
# Infrastructure words that, combined with a delimited "test" token in the same
|
|
124
|
+
# directory segment, identify a module whose code is test scaffolding even though
|
|
125
|
+
# it lives under src/main (e.g. "server-test-utils", "foo-testkit"). Kept narrow
|
|
126
|
+
# on purpose: the "test" token must be present in the SAME segment, so packages
|
|
127
|
+
# named "utils"/"support"/"tools" alone never match.
|
|
128
|
+
_TEST_INFRA_TOKENS = frozenset({
|
|
129
|
+
"util", "utils", "fixture", "fixtures", "support",
|
|
130
|
+
"framework", "kit", "harness", "tools", "tool",
|
|
131
|
+
"helper", "helpers", "mock", "mocks", "stub", "stubs", "dummy",
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
# Glued (non-hyphenated) test-fixture module suffixes.
|
|
135
|
+
_TEST_INFRA_SUFFIXES = (
|
|
136
|
+
"testutils", "testutil", "testkit", "testfixtures", "testfixture",
|
|
137
|
+
"testsupport", "testframework", "testharness", "testtools",
|
|
138
|
+
"testhelpers", "testmocks", "teststubs",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def is_test_fixture_module_path(path: str) -> bool:
|
|
143
|
+
"""Return True when *path* belongs to a test-fixture / test-utility MODULE.
|
|
144
|
+
|
|
145
|
+
Structural, name-agnostic: a directory segment must contain a hyphen/underscore
|
|
146
|
+
-delimited ``test`` token together with an infrastructure word (util(s),
|
|
147
|
+
fixture(s), support, framework, kit, harness, tools, helper(s), mock(s),
|
|
148
|
+
stub(s), dummy) — e.g. ``server-test-utils``, ``foo-testkit``,
|
|
149
|
+
``bar-test-support`` — or match a known test-module segment outright.
|
|
150
|
+
|
|
151
|
+
This is deliberately NARROWER than :func:`is_test_or_fixture_path`: it targets
|
|
152
|
+
modules that ship HTTP resources, JAX-RS ``@Path`` fixtures, or dummy
|
|
153
|
+
controllers under ``src/main`` so they are not misreported as production API
|
|
154
|
+
surface. Requiring both tokens in one segment keeps legitimate ``utils`` /
|
|
155
|
+
``support`` / ``tools`` packages from ever matching.
|
|
156
|
+
"""
|
|
157
|
+
norm = path.replace("\\", "/").lower()
|
|
158
|
+
parts = norm.split("/")
|
|
159
|
+
for part in parts[:-1]: # skip the filename
|
|
160
|
+
bare = part.rstrip("/")
|
|
161
|
+
if bare in _TEST_MODULE_SEGMENTS:
|
|
162
|
+
return True
|
|
163
|
+
if bare.endswith(_TEST_INFRA_SUFFIXES):
|
|
164
|
+
return True
|
|
165
|
+
toks = bare.replace("_", "-").split("-")
|
|
166
|
+
if "test" in toks and any(t in _TEST_INFRA_TOKENS for t in toks):
|
|
167
|
+
return True
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
123
171
|
def is_vendor_path(path: str) -> bool:
|
|
124
172
|
"""Return True when *path* is inside a vendored / third-party directory.
|
|
125
173
|
|
sourcecode/prepare_context.py
CHANGED
|
@@ -1249,6 +1249,17 @@ class TaskContextBuilder:
|
|
|
1249
1249
|
[ws.path for ws in workspace_analysis.workspaces],
|
|
1250
1250
|
)
|
|
1251
1251
|
|
|
1252
|
+
# F-1 single-ownership (ADR-0005 §INV-F1-4): run the architecture analysis
|
|
1253
|
+
# ONCE, here, and keep it on sm.architecture — the single owned analysis.
|
|
1254
|
+
# ArchitectureSummarizer/ContextSummarizer reuse it instead of each computing
|
|
1255
|
+
# (and discarding) a private copy, and its refutations are transported into
|
|
1256
|
+
# `limitations` below (surface #3, F1-SURFACE-INTEGRATION-2026-07-18).
|
|
1257
|
+
from sourcecode.architecture_analyzer import ArchitectureAnalyzer as _ArchAnalyzer
|
|
1258
|
+
try:
|
|
1259
|
+
sm.architecture = _ArchAnalyzer().analyze(self.root, sm)
|
|
1260
|
+
except Exception:
|
|
1261
|
+
sm.architecture = None
|
|
1262
|
+
|
|
1252
1263
|
project_summary = ProjectSummarizer(self.root).generate(sm)
|
|
1253
1264
|
architecture_summary = ArchitectureSummarizer(self.root).generate(sm)
|
|
1254
1265
|
|
|
@@ -1306,6 +1317,19 @@ class TaskContextBuilder:
|
|
|
1306
1317
|
key_dependencies: list[dict[str, Any]] = []
|
|
1307
1318
|
limitations: list[str] = []
|
|
1308
1319
|
|
|
1320
|
+
# INV-F1-4 transport: this surface narrates the architecture, so it must
|
|
1321
|
+
# carry the owner's refutations — the same lines surface #1 (serializer)
|
|
1322
|
+
# emits. Only the F-1 refutation / execution-error lines (their fixed
|
|
1323
|
+
# prefixes); pattern-inference notes stay out of the onboard surface. The
|
|
1324
|
+
# analysis is the single owned one (sm.architecture) — no re-run, no re-judge.
|
|
1325
|
+
if sm.architecture is not None:
|
|
1326
|
+
for _arch_lim in (sm.architecture.limitations or []):
|
|
1327
|
+
if _arch_lim.startswith((
|
|
1328
|
+
"Architecture claim refuted on evidence:",
|
|
1329
|
+
"Architecture reconciliation could not run:",
|
|
1330
|
+
)):
|
|
1331
|
+
limitations.append(_arch_lim)
|
|
1332
|
+
|
|
1309
1333
|
if spec.enable_dependencies:
|
|
1310
1334
|
from dataclasses import asdict
|
|
1311
1335
|
from sourcecode.dependency_analyzer import DependencyAnalyzer
|
sourcecode/reconciliation.py
CHANGED
|
@@ -62,17 +62,75 @@ from pathlib import Path
|
|
|
62
62
|
|
|
63
63
|
@dataclass(frozen=True)
|
|
64
64
|
class ReconciliationFinding:
|
|
65
|
-
"""One
|
|
65
|
+
"""One reconciliation outcome that a consumer must not narrate away.
|
|
66
|
+
|
|
67
|
+
Two kinds ride this one type, distinguished by `error`:
|
|
68
|
+
|
|
69
|
+
- a **detected contradiction** (`error=False`, `symbols` non-empty): two
|
|
70
|
+
views positively disagree; the consumer degrades the claim and names it.
|
|
71
|
+
- an **execution error** (`error=True`, `symbols` empty): a rule could not be
|
|
72
|
+
evaluated. It exists so that a rule that raises is NEVER indistinguishable
|
|
73
|
+
from a rule that ran and found nothing — the fail-open that let an
|
|
74
|
+
exception silently certify the very claim the rule was meant to check.
|
|
75
|
+
|
|
76
|
+
A rule that is *not applicable*, or that ran and found the views consistent,
|
|
77
|
+
yields NO finding: both legitimately mean "no contradiction, the claim may
|
|
78
|
+
stand", and no consumer acts differently on them. Only the error case needed
|
|
79
|
+
a physical state of its own (ADR-0005 §8.2).
|
|
80
|
+
"""
|
|
66
81
|
rule: str # stable rule id, e.g. "endpoint_coverage"
|
|
67
82
|
symbols: tuple[str, ...] # symbols the contradiction is anchored to
|
|
68
83
|
detail: str # one-sentence human/agent-readable statement
|
|
84
|
+
error: bool = False # True iff this marks a rule that could not run
|
|
69
85
|
|
|
70
86
|
def to_dict(self) -> dict:
|
|
71
|
-
|
|
87
|
+
d = {
|
|
72
88
|
"rule": self.rule,
|
|
73
89
|
"symbols": list(self.symbols),
|
|
74
90
|
"detail": self.detail,
|
|
75
91
|
}
|
|
92
|
+
# Healthy findings serialize byte-identically to before; the flag appears
|
|
93
|
+
# only when set, so no existing consumer/snapshot shifts.
|
|
94
|
+
if self.error:
|
|
95
|
+
d["error"] = True
|
|
96
|
+
return d
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _execution_error(rule_id: str, exc: Exception) -> ReconciliationFinding:
|
|
100
|
+
"""Build the finding that marks a rule which raised instead of running."""
|
|
101
|
+
return ReconciliationFinding(
|
|
102
|
+
rule=rule_id,
|
|
103
|
+
symbols=(),
|
|
104
|
+
error=True,
|
|
105
|
+
detail=(
|
|
106
|
+
f"the {rule_id} reconciliation rule could not be evaluated "
|
|
107
|
+
f"({type(exc).__name__}) — its consistency check did NOT run, so the "
|
|
108
|
+
"claim it guards is UNVERIFIED, not confirmed."
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _run_rule(rule_id: str, thunk) -> list[ReconciliationFinding]:
|
|
114
|
+
"""Run one rule in isolation. On any exception, return an execution-error
|
|
115
|
+
finding instead of swallowing it — the core of the fail-open fix. Per-rule
|
|
116
|
+
(not per-surface) so one rule raising never blanks a sibling rule's finding.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
return list(thunk())
|
|
120
|
+
except Exception as exc: # noqa: BLE001 — deliberate: convert, never swallow
|
|
121
|
+
return [_execution_error(rule_id, exc)]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def execution_errors(
|
|
125
|
+
findings: "list[ReconciliationFinding]",
|
|
126
|
+
) -> list[ReconciliationFinding]:
|
|
127
|
+
"""The execution-error findings in a batch (empty if every rule ran)."""
|
|
128
|
+
return [f for f in findings if getattr(f, "error", False)]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def has_execution_error(findings: "list[ReconciliationFinding]") -> bool:
|
|
132
|
+
"""True iff any rule in the batch could not be evaluated."""
|
|
133
|
+
return any(getattr(f, "error", False) for f in findings)
|
|
76
134
|
|
|
77
135
|
|
|
78
136
|
def http_handler_classes(cir) -> frozenset[str]:
|
|
@@ -142,14 +200,12 @@ def reconcile_impact_evidence(
|
|
|
142
200
|
modeled_classes: classes the endpoint index has ≥1 route for
|
|
143
201
|
(endpoint_index.controller_fqns).
|
|
144
202
|
"""
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
pass
|
|
152
|
-
return findings
|
|
203
|
+
return _run_rule(
|
|
204
|
+
"endpoint_coverage",
|
|
205
|
+
lambda: _rule_endpoint_coverage(
|
|
206
|
+
chain_classes, handler_classes, modeled_classes
|
|
207
|
+
),
|
|
208
|
+
)
|
|
153
209
|
|
|
154
210
|
|
|
155
211
|
# ---------------------------------------------------------------------------
|
|
@@ -387,11 +443,11 @@ def reconcile_code_modules(
|
|
|
387
443
|
layer_files: {claimed module -> its files}.
|
|
388
444
|
repo_files: every source file, supplying the repo's own runtime.
|
|
389
445
|
"""
|
|
390
|
-
|
|
446
|
+
def _run() -> list[ReconciliationFinding]:
|
|
391
447
|
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
392
448
|
return _rule_layer_code_attestation(runtime_census(layer_files), repo_runtimes)
|
|
393
|
-
|
|
394
|
-
|
|
449
|
+
|
|
450
|
+
return _run_rule("layer_code_attestation", _run)
|
|
395
451
|
|
|
396
452
|
|
|
397
453
|
def unattested_layers(
|
|
@@ -422,16 +478,22 @@ def reconcile_architecture_evidence(
|
|
|
422
478
|
repo_files: every source file considered — supplies the repo's own
|
|
423
479
|
runtime, the arbiter of which layers belong to the claim.
|
|
424
480
|
"""
|
|
425
|
-
findings: list[ReconciliationFinding] = []
|
|
426
481
|
try:
|
|
427
482
|
census = runtime_census(layer_files)
|
|
428
483
|
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
484
|
+
except Exception as exc:
|
|
485
|
+
# Shared view prep failed — neither rule could run. One error finding,
|
|
486
|
+
# attributed to the primary architecture rule, carries that to the sink.
|
|
487
|
+
return [_execution_error("layer_language_coherence", exc)]
|
|
488
|
+
findings: list[ReconciliationFinding] = []
|
|
489
|
+
findings += _run_rule(
|
|
490
|
+
"layer_language_coherence",
|
|
491
|
+
lambda: _rule_layer_language_coherence(pattern, census, repo_runtimes),
|
|
492
|
+
)
|
|
493
|
+
findings += _run_rule(
|
|
494
|
+
"composition_runtime_split",
|
|
495
|
+
lambda: _rule_composition_runtime_split(pattern, census),
|
|
496
|
+
)
|
|
435
497
|
return findings
|
|
436
498
|
|
|
437
499
|
|
|
@@ -487,6 +549,19 @@ _STACK_FAMILY: dict[str, str] = {
|
|
|
487
549
|
"elixir": "beam", "erlang": "beam",
|
|
488
550
|
}
|
|
489
551
|
|
|
552
|
+
|
|
553
|
+
def stack_runtime_family(stack_id: str) -> str | None:
|
|
554
|
+
"""Runtime family of a stack id (``jvm``/``js``/``python``/…), or None.
|
|
555
|
+
|
|
556
|
+
The single source of truth for "which stacks share a runtime". A consumer
|
|
557
|
+
that gates on JVM-ness (a Kotlin, Scala or Groovy repo is the SAME runtime as
|
|
558
|
+
a Java one) reads this instead of hardcoding ``{"java", "kotlin"}`` — which
|
|
559
|
+
silently dropped Scala and Groovy and let an unverified claim ship on them.
|
|
560
|
+
None means "unjudgeable", never a contradiction (INV-F1-1).
|
|
561
|
+
"""
|
|
562
|
+
return _STACK_FAMILY.get(stack_id or "")
|
|
563
|
+
|
|
564
|
+
|
|
490
565
|
# Project types that are SUPPOSED to span runtimes — their headline names a tier
|
|
491
566
|
# split, so a framework from another runtime is the point, not a contradiction.
|
|
492
567
|
# Same carve-out (and same names) as `_CROSS_RUNTIME_PATTERNS` for layer claims.
|
|
@@ -594,17 +669,13 @@ def reconcile_headline_evidence(
|
|
|
594
669
|
"""
|
|
595
670
|
if project_type in _CROSS_RUNTIME_TYPES:
|
|
596
671
|
return []
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
)
|
|
605
|
-
except Exception:
|
|
606
|
-
pass
|
|
607
|
-
return findings
|
|
672
|
+
return _run_rule(
|
|
673
|
+
"framework_stack_attestation",
|
|
674
|
+
lambda: _rule_framework_stack_attestation(
|
|
675
|
+
_STACK_FAMILY.get(claimed_stack or ""),
|
|
676
|
+
framework_owner_families(stacks),
|
|
677
|
+
),
|
|
678
|
+
)
|
|
608
679
|
|
|
609
680
|
|
|
610
681
|
def type_claim_unattested(
|
|
@@ -631,6 +702,12 @@ def type_claim_unattested(
|
|
|
631
702
|
return False
|
|
632
703
|
return not any(family in fams for fams in owners.values())
|
|
633
704
|
except Exception:
|
|
705
|
+
# A bool predicate cannot carry an execution-error finding. It never
|
|
706
|
+
# decides alone: every caller runs `reconcile_headline_evidence` on the
|
|
707
|
+
# SAME stacks first (same `framework_owner_families` call), so an
|
|
708
|
+
# exception here has already surfaced as an execution-error finding
|
|
709
|
+
# there. Returning False adds NO extra clause — it does not certify a
|
|
710
|
+
# claim the paired findings call left unguarded.
|
|
634
711
|
return False
|
|
635
712
|
|
|
636
713
|
|
|
@@ -646,6 +723,81 @@ def unattested_frameworks(
|
|
|
646
723
|
return frozenset(out)
|
|
647
724
|
|
|
648
725
|
|
|
726
|
+
# ---------------------------------------------------------------------------
|
|
727
|
+
# Narrative reconciliation (web-pattern claim × endpoint index)
|
|
728
|
+
# ---------------------------------------------------------------------------
|
|
729
|
+
|
|
730
|
+
# Patterns that assert an HTTP/view tier. The claim is directory-derived (dirs
|
|
731
|
+
# named controller/model/view); the endpoint index is the authoritative
|
|
732
|
+
# controller evidence for the SAME run. They contradict when the pattern names a
|
|
733
|
+
# web tier but the index modeled NO controller at all — Jenkins (Stapler)
|
|
734
|
+
# matched model/view dirs and declared zero Spring MVC controllers.
|
|
735
|
+
_WEB_PATTERNS: frozenset[str] = frozenset({"mvc", "spring_mvc_layered"})
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _rule_web_pattern_attestation(
|
|
739
|
+
pattern: str, endpoint_evidence_authoritative: bool, modeled_endpoint_count: int
|
|
740
|
+
) -> list[ReconciliationFinding]:
|
|
741
|
+
"""A web/MVC pattern claim that the endpoint index does not back at all.
|
|
742
|
+
|
|
743
|
+
Provable ABSENCE, not a threshold: the rule fires only at ZERO modeled
|
|
744
|
+
controllers (`== 0`), never at "few". One controller backs the claim — the
|
|
745
|
+
pattern has real HTTP evidence and is not contradicted. The "< N is not
|
|
746
|
+
really a web app" judgement is a LABEL heuristic and stays in the consumer
|
|
747
|
+
(ADR-0005 limit 2: a rule may not carry a threshold).
|
|
748
|
+
|
|
749
|
+
**Authority gate (INV-F1-1).** Zero controllers is a CONTRADICTION only where
|
|
750
|
+
the endpoint extractor can actually see the code. The canonical extractor
|
|
751
|
+
reads `*.java` only — so on a Kotlin/Scala/Groovy repo an empty index is
|
|
752
|
+
*absence of evidence* (the extractor is blind to `.kt`/`.scala`/`.groovy`),
|
|
753
|
+
NOT a contradiction, and firing there is a false negative (measured: a Kotlin
|
|
754
|
+
`@RestController` with real routes read "no HTTP controllers detected"). The
|
|
755
|
+
caller passes `endpoint_evidence_authoritative`; when false, the rule stays
|
|
756
|
+
silent. The consumer decides authority (it owns the extractor fact), keeping
|
|
757
|
+
this rule free of any language/extractor coupling.
|
|
758
|
+
"""
|
|
759
|
+
if pattern not in _WEB_PATTERNS or not endpoint_evidence_authoritative:
|
|
760
|
+
return []
|
|
761
|
+
if modeled_endpoint_count > 0:
|
|
762
|
+
return []
|
|
763
|
+
return [
|
|
764
|
+
ReconciliationFinding(
|
|
765
|
+
rule="web_pattern_endpoint_attestation",
|
|
766
|
+
symbols=(pattern,),
|
|
767
|
+
detail=(
|
|
768
|
+
f"the '{pattern}' claim names an HTTP/view tier but the endpoint "
|
|
769
|
+
"index modeled no controller — the layers are directory names, "
|
|
770
|
+
"not routes."
|
|
771
|
+
),
|
|
772
|
+
)
|
|
773
|
+
]
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def reconcile_web_pattern_evidence(
|
|
777
|
+
*,
|
|
778
|
+
pattern: str,
|
|
779
|
+
endpoint_evidence_authoritative: bool,
|
|
780
|
+
modeled_endpoint_count: int,
|
|
781
|
+
) -> list[ReconciliationFinding]:
|
|
782
|
+
"""Run the web-pattern attestation rule. Never raises.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
pattern: the claimed architecture pattern.
|
|
786
|
+
endpoint_evidence_authoritative: True iff the endpoint extractor can see
|
|
787
|
+
this repo's controllers (it reads .java
|
|
788
|
+
only — the consumer owns this fact). When
|
|
789
|
+
False, zero endpoints is absence, not a
|
|
790
|
+
contradiction (INV-F1-1), and no finding.
|
|
791
|
+
modeled_endpoint_count: high-confidence controllers modeled.
|
|
792
|
+
"""
|
|
793
|
+
return _run_rule(
|
|
794
|
+
"web_pattern_endpoint_attestation",
|
|
795
|
+
lambda: _rule_web_pattern_attestation(
|
|
796
|
+
pattern, endpoint_evidence_authoritative, modeled_endpoint_count
|
|
797
|
+
),
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
649
801
|
def reconcile_extraction_evidence(
|
|
650
802
|
*,
|
|
651
803
|
type_decl_files: frozenset[str],
|
|
@@ -657,9 +809,7 @@ def reconcile_extraction_evidence(
|
|
|
657
809
|
type_decl_files: files whose masked source declares a Java type.
|
|
658
810
|
extracted_files: files symbol extraction produced ≥1 symbol for.
|
|
659
811
|
"""
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
pass
|
|
665
|
-
return findings
|
|
812
|
+
return _run_rule(
|
|
813
|
+
"parse_coverage",
|
|
814
|
+
lambda: _rule_parse_coverage(type_decl_files, extracted_files),
|
|
815
|
+
)
|
sourcecode/repository_ir.py
CHANGED
|
@@ -5584,7 +5584,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5584
5584
|
"""
|
|
5585
5585
|
import re as _re
|
|
5586
5586
|
from typing import Any as _Any
|
|
5587
|
-
from sourcecode.path_filters import is_test_path
|
|
5587
|
+
from sourcecode.path_filters import is_test_path, is_test_fixture_module_path
|
|
5588
5588
|
|
|
5589
5589
|
_EXTENDS_FROM_SIG = _re.compile(r'\bextends\s+(\w+)')
|
|
5590
5590
|
|
|
@@ -5753,6 +5753,16 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5753
5753
|
endpoint_warnings = _recovery["warnings"]
|
|
5754
5754
|
_openapi_spec_path = _recovery["spec_path"]
|
|
5755
5755
|
|
|
5756
|
+
# Map each declared class/interface FQN to its source file so an endpoint can be
|
|
5757
|
+
# traced back to the MODULE that ships it. Used to flag endpoints that originate
|
|
5758
|
+
# in a test-fixture / test-utility module (dummy web services, JAX-RS @Path
|
|
5759
|
+
# fixtures) living under src/main — these are not production API surface.
|
|
5760
|
+
_fqn_to_file: dict[str, str] = {
|
|
5761
|
+
s.symbol: (s.source_file or s.declaring_file)
|
|
5762
|
+
for s in all_symbols
|
|
5763
|
+
if s.type in ("class", "interface")
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5756
5766
|
endpoints: list[dict] = []
|
|
5757
5767
|
for route in routes:
|
|
5758
5768
|
handler = (
|
|
@@ -5769,6 +5779,9 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5769
5779
|
"handler": handler,
|
|
5770
5780
|
"return_type": route.get("return_type", "void"),
|
|
5771
5781
|
}
|
|
5782
|
+
_src = _fqn_to_file.get(route.get("effective_class", ""), "")
|
|
5783
|
+
if _src and is_test_fixture_module_path(_src):
|
|
5784
|
+
entry["scope"] = "test_util"
|
|
5772
5785
|
# Use security_annotations already extracted by _build_route_surface
|
|
5773
5786
|
# via the canonical _route_security_from_sym extractor.
|
|
5774
5787
|
security_info = route.get("security_annotations")
|
|
@@ -5791,6 +5804,14 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5791
5804
|
)
|
|
5792
5805
|
endpoints = [e for e in endpoints if not _FQN_PATH_RE.search(e.get("path", ""))]
|
|
5793
5806
|
|
|
5807
|
+
# Partition test-fixture/test-utility endpoints out of the production surface.
|
|
5808
|
+
# They ship under src/main of a *-test-utils / *-testkit / test-framework module
|
|
5809
|
+
# (dummy web services, JAX-RS @Path fixtures) and must not be reported as, or
|
|
5810
|
+
# counted with, the real API surface — but they are preserved in their own bucket
|
|
5811
|
+
# so nothing is silently dropped and the exclusion is auditable.
|
|
5812
|
+
_test_util_endpoints = [e for e in endpoints if e.get("scope") == "test_util"]
|
|
5813
|
+
endpoints = [e for e in endpoints if e.get("scope") != "test_util"]
|
|
5814
|
+
|
|
5794
5815
|
# "no_security_signal" = no recognized security annotation at method OR class level.
|
|
5795
5816
|
# Note: repos may use framework-level security (e.g. Keycloak itself) with no
|
|
5796
5817
|
# per-endpoint annotations — this count reflects annotation-based coverage only.
|
|
@@ -5919,6 +5940,16 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5919
5940
|
# Keep legacy field name for backward compat, now means same as no_security_signal
|
|
5920
5941
|
"undocumented": no_security_signal,
|
|
5921
5942
|
}
|
|
5943
|
+
# Surface test-fixture endpoints excluded from the production surface above.
|
|
5944
|
+
# Reported separately (not hidden) so the exclusion is transparent and auditable.
|
|
5945
|
+
if _test_util_endpoints:
|
|
5946
|
+
result["test_util_endpoints"] = _test_util_endpoints
|
|
5947
|
+
result["test_util_excluded"] = len(_test_util_endpoints)
|
|
5948
|
+
result.setdefault("notes", []).append(
|
|
5949
|
+
f"{len(_test_util_endpoints)} endpoint(s) from test-fixture/test-utility "
|
|
5950
|
+
f"module(s) under src/main were excluded from the production surface "
|
|
5951
|
+
f"(see test_util_endpoints)."
|
|
5952
|
+
)
|
|
5922
5953
|
# Surface incomplete-endpoint warnings (interface-defined controllers) only when
|
|
5923
5954
|
# present, to keep output backward-compatible for the common case.
|
|
5924
5955
|
if endpoint_warnings:
|
sourcecode/spring_impact.py
CHANGED
|
@@ -1265,6 +1265,7 @@ class ImpactOrchestrator:
|
|
|
1265
1265
|
# provably declares HTTP handlers but has no modeled route is a
|
|
1266
1266
|
# positively-detected under-report of endpoints_affected.
|
|
1267
1267
|
endpoint_coverage_blind_spot = False
|
|
1268
|
+
reconciliation_error_blind_spot = False
|
|
1268
1269
|
reconciliation_findings: list[dict] = []
|
|
1269
1270
|
try:
|
|
1270
1271
|
_chain_classes = frozenset(
|
|
@@ -1277,9 +1278,23 @@ class ImpactOrchestrator:
|
|
|
1277
1278
|
modeled_classes=frozenset(model.endpoint_index.controller_fqns),
|
|
1278
1279
|
)
|
|
1279
1280
|
reconciliation_findings = [f.to_dict() for f in _rec]
|
|
1281
|
+
# Fail-open guard (gap #2): an execution-error finding means a rule
|
|
1282
|
+
# could not run. It must NEVER be read as "no contradiction" — surface
|
|
1283
|
+
# it as its own blind spot so an unevaluated endpoint check can't pass
|
|
1284
|
+
# for a clean one. (The error finding carries rule="endpoint_coverage"
|
|
1285
|
+
# too, hence the `not _f.error` on the contradiction branch below.)
|
|
1286
|
+
_rec_errors = [f for f in _rec if f.error]
|
|
1287
|
+
if _rec_errors:
|
|
1288
|
+
reconciliation_error_blind_spot = True
|
|
1289
|
+
warnings.append(
|
|
1290
|
+
"Evidence reconciliation could not run for this query — "
|
|
1291
|
+
+ _rec_errors[0].detail
|
|
1292
|
+
+ " The endpoint-coverage check did NOT execute; treat the "
|
|
1293
|
+
"endpoint list as unverified, not confirmed complete."
|
|
1294
|
+
)
|
|
1280
1295
|
_unmapped: list[str] = []
|
|
1281
1296
|
for _f in _rec:
|
|
1282
|
-
if _f.rule == "endpoint_coverage":
|
|
1297
|
+
if _f.rule == "endpoint_coverage" and not _f.error:
|
|
1283
1298
|
endpoint_coverage_blind_spot = True
|
|
1284
1299
|
_unmapped = list(_f.symbols)
|
|
1285
1300
|
if endpoint_coverage_blind_spot:
|
|
@@ -1294,8 +1309,16 @@ class ImpactOrchestrator:
|
|
|
1294
1309
|
"locator chains). Callers remain reliable; verify these classes' "
|
|
1295
1310
|
"exposure before treating the endpoint list as complete."
|
|
1296
1311
|
)
|
|
1297
|
-
except Exception:
|
|
1298
|
-
|
|
1312
|
+
except Exception as _rec_exc:
|
|
1313
|
+
# Even assembling the reconciliation inputs failed (e.g.
|
|
1314
|
+
# http_handler_classes traversing a malformed IR). Same invariant: an
|
|
1315
|
+
# exception is not evidence of consistency — record a blind spot.
|
|
1316
|
+
reconciliation_error_blind_spot = True
|
|
1317
|
+
warnings.append(
|
|
1318
|
+
"Evidence reconciliation could not run for this query "
|
|
1319
|
+
f"({type(_rec_exc).__name__}) — the endpoint-coverage check did "
|
|
1320
|
+
"NOT execute; treat the endpoint list as unverified."
|
|
1321
|
+
)
|
|
1299
1322
|
|
|
1300
1323
|
blind_spots = (
|
|
1301
1324
|
# framework_di stops being a blind spot once CH-007 recovers callers
|
|
@@ -1305,6 +1328,7 @@ class ImpactOrchestrator:
|
|
|
1305
1328
|
+ (["method_scope_unproven"] if method_scope_blind_spot else [])
|
|
1306
1329
|
+ (["parse_coverage"] if parse_coverage_blind_spot else [])
|
|
1307
1330
|
+ (["endpoint_coverage_partial"] if endpoint_coverage_blind_spot else [])
|
|
1331
|
+
+ (["reconciliation_error"] if reconciliation_error_blind_spot else [])
|
|
1308
1332
|
)
|
|
1309
1333
|
|
|
1310
1334
|
confidence: str
|
|
@@ -1322,9 +1346,16 @@ class ImpactOrchestrator:
|
|
|
1322
1346
|
or parse_coverage_blind_spot
|
|
1323
1347
|
):
|
|
1324
1348
|
confidence = "low"
|
|
1325
|
-
elif
|
|
1349
|
+
elif (
|
|
1350
|
+
resolution == "partial"
|
|
1351
|
+
or confidence_reducing
|
|
1352
|
+
or endpoint_coverage_blind_spot
|
|
1353
|
+
or reconciliation_error_blind_spot
|
|
1354
|
+
):
|
|
1326
1355
|
# endpoint_coverage: the callers are still trustworthy — only the
|
|
1327
1356
|
# endpoint axis is provably incomplete, so cap at medium, not low.
|
|
1357
|
+
# reconciliation_error: the endpoint check did not run at all, so we
|
|
1358
|
+
# cannot claim "high" — same medium cap, callers still stand.
|
|
1328
1359
|
confidence = "medium"
|
|
1329
1360
|
else:
|
|
1330
1361
|
confidence = "high"
|
sourcecode/summarizer.py
CHANGED
|
@@ -9,7 +9,10 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from sourcecode.architecture_analyzer import _LAYER_MIN_SHARE, _PATTERN_REQUIRED_KEYS
|
|
12
|
-
from sourcecode.reconciliation import
|
|
12
|
+
from sourcecode.reconciliation import (
|
|
13
|
+
has_execution_error,
|
|
14
|
+
reconcile_architecture_evidence,
|
|
15
|
+
)
|
|
13
16
|
from sourcecode.detectors.parsers import load_json_file, load_toml_file
|
|
14
17
|
from sourcecode.tree_utils import safe_read_text
|
|
15
18
|
from sourcecode.entrypoint_classifier import is_production_entry_point
|
|
@@ -437,9 +440,22 @@ class ProjectSummarizer:
|
|
|
437
440
|
# SIM-3: same coherence rule the architecture prose applies — this
|
|
438
441
|
# table is the headline's copy, and a fix applied to only one of the
|
|
439
442
|
# two duplicated tables leaves the other one lying.
|
|
440
|
-
|
|
441
|
-
layer_files
|
|
442
|
-
|
|
443
|
+
_arch_findings = reconcile_architecture_evidence(
|
|
444
|
+
pattern=pattern_name, layer_files=layer_files, repo_files=file_paths
|
|
445
|
+
)
|
|
446
|
+
# Fail-open guard (gap #2): this headline word has no gap channel. If
|
|
447
|
+
# the coherence/split check could not run, do not headline an
|
|
448
|
+
# unverified pattern — skip it, exactly as a contradiction would.
|
|
449
|
+
if has_execution_error(_arch_findings):
|
|
450
|
+
continue
|
|
451
|
+
_split_contradicted = False
|
|
452
|
+
for _f in _arch_findings:
|
|
453
|
+
if _f.rule == "composition_runtime_split":
|
|
454
|
+
_split_contradicted = True
|
|
455
|
+
else:
|
|
456
|
+
for _bad in _f.symbols:
|
|
457
|
+
layer_files.pop(_bad, None)
|
|
458
|
+
if _split_contradicted:
|
|
443
459
|
continue
|
|
444
460
|
# A pattern must show its defining trait here too, or the headline
|
|
445
461
|
# contradicts the architecture prose that already enforces this.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=M8eYjrmWCXZgccRJ042BhY9QS6VmNX4SDwD_gMX9cvo,309
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/archetype.py,sha256=d7yoN6Tj4OBj-VgSMPEKg4WF9H8FypnZ5A6AkUh5oIE,34484
|
|
4
|
-
sourcecode/architecture_analyzer.py,sha256=
|
|
5
|
-
sourcecode/architecture_summary.py,sha256=
|
|
4
|
+
sourcecode/architecture_analyzer.py,sha256=GFc4ek-s1IHWM7pl-0L32WahZ93AmDgrAMcOuBKA5Dk,61463
|
|
5
|
+
sourcecode/architecture_summary.py,sha256=BVVRHd952cjRhjHnR6CPrvKgaa-tdM16l-pBi1yDCPs,32395
|
|
6
6
|
sourcecode/ast_extractor.py,sha256=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,51351
|
|
7
7
|
sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
|
|
8
8
|
sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
|
|
9
9
|
sourcecode/caller_metrics.py,sha256=HG8RCOkpzzsEGdnMRob6byrwjoj5__t0TEsBj4yR7ks,2574
|
|
10
10
|
sourcecode/canonical_ir.py,sha256=5hLAhhSpXNOdZAvPt-J-rXccJE6M3A6gVhmhU0Tym6Y,29269
|
|
11
11
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
12
|
-
sourcecode/classifier.py,sha256=
|
|
12
|
+
sourcecode/classifier.py,sha256=rkMapdklDuIv3rXrNEwwL65UldkvTn8VuKa73GPoSDo,18849
|
|
13
13
|
sourcecode/cli.py,sha256=mlMgyRukRoHXYdAYHBN4z06LszwdRo0QLdhs0KYtup8,323804
|
|
14
14
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
15
|
-
sourcecode/confidence_analyzer.py,sha256=
|
|
15
|
+
sourcecode/confidence_analyzer.py,sha256=vnbPI-20FnHdjO6STxHW8fbaxmB4A7y58io63ibFZjc,21586
|
|
16
16
|
sourcecode/context_cache.py,sha256=maws79rLKDwyZ7c9Q2LwlIxqZHRFlaKzErtO3mglWoA,25036
|
|
17
17
|
sourcecode/context_graph.py,sha256=WFtUvfxoE8XtQxtRJLvaF4SJuS00ICjNoTk0sH2_V5U,44454
|
|
18
18
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -45,18 +45,18 @@ sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7
|
|
|
45
45
|
sourcecode/migrate_check.py,sha256=HJhvHuvXDGTJSRQU9_AMx0L94h2TImC1DCGs3m3RTu0,108199
|
|
46
46
|
sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
|
|
47
47
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
48
|
-
sourcecode/path_filters.py,sha256=
|
|
48
|
+
sourcecode/path_filters.py,sha256=809P9hj_QChNLg8wk4HuKcFUZmnDKNz0sKIMbmt_FNE,9138
|
|
49
49
|
sourcecode/pr_comment_renderer.py,sha256=KmcjMruhR44gjzMDJwjBSkWP9QEvh8xWBLyxzxoRbj0,14542
|
|
50
50
|
sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
|
|
51
|
-
sourcecode/prepare_context.py,sha256
|
|
51
|
+
sourcecode/prepare_context.py,sha256=-Xu33qCETuqw-OFfT3ukvAtxde7krCjQpn7wR3pbdOg,224290
|
|
52
52
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
53
53
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
54
|
-
sourcecode/reconciliation.py,sha256=
|
|
54
|
+
sourcecode/reconciliation.py,sha256=GU-1PTcVr8zcbtC7BASfpHcZndP9AdboXPNQiBc0fzo,34251
|
|
55
55
|
sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
56
56
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
57
57
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
58
58
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
59
|
-
sourcecode/repository_ir.py,sha256=
|
|
59
|
+
sourcecode/repository_ir.py,sha256=bNSUKkWQEvFQ2wSZPRdn_9cZfx6TRcPzGtM2EgMm49M,311400
|
|
60
60
|
sourcecode/ris.py,sha256=Hw8TakTQ6hku-Abf2k8954NwkrH_sP73_8wVH8x5khc,22079
|
|
61
61
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
62
62
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -70,12 +70,12 @@ sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_p
|
|
|
70
70
|
sourcecode/serializer.py,sha256=FmQo7eojA_z3ptLW-Vgze5mrEJ6FabcZqiwbQWWTgFI,130056
|
|
71
71
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
72
72
|
sourcecode/spring_findings.py,sha256=EX7kLZLN74CFyR9iZPm3CI115BfFKNd4WRPuErNljZM,5729
|
|
73
|
-
sourcecode/spring_impact.py,sha256=
|
|
73
|
+
sourcecode/spring_impact.py,sha256=MkUI27OCOhkjvBoA4f9X1eDBIY0SmyPuLVCWIfZgOcE,72430
|
|
74
74
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
75
75
|
sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
|
|
76
76
|
sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
|
|
77
77
|
sourcecode/spring_tx_analyzer.py,sha256=QMXkxa_hrqDVN3D8WJsd0nT7vfdZCT1E8ZWNoFFbvgU,34692
|
|
78
|
-
sourcecode/summarizer.py,sha256=
|
|
78
|
+
sourcecode/summarizer.py,sha256=SWj-y5BJ_Qw89ljLg-KXaLsvU87usOw-raTckPutCk4,26976
|
|
79
79
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
80
80
|
sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
|
|
81
81
|
sourcecode/validation_inference.py,sha256=UtmYv-d4TEzT1ihGEipAhtXDrwq0XtVtjMpb3i7NsNc,10815
|
|
@@ -137,8 +137,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
|
|
|
137
137
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
138
138
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
139
139
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
140
|
-
sourcecode-2.5.
|
|
141
|
-
sourcecode-2.5.
|
|
142
|
-
sourcecode-2.5.
|
|
143
|
-
sourcecode-2.5.
|
|
144
|
-
sourcecode-2.5.
|
|
140
|
+
sourcecode-2.5.13.dist-info/METADATA,sha256=hAqH0Dxo8V6o4vDdpqalzX3njpQNvHIO8-t5zglFo68,10852
|
|
141
|
+
sourcecode-2.5.13.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
142
|
+
sourcecode-2.5.13.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
143
|
+
sourcecode-2.5.13.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
144
|
+
sourcecode-2.5.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|