sourcecode 2.5.8__py3-none-any.whl → 2.5.12__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 +112 -16
- sourcecode/architecture_summary.py +75 -21
- sourcecode/classifier.py +1 -1
- sourcecode/cli.py +34 -2
- sourcecode/confidence_analyzer.py +14 -0
- sourcecode/prepare_context.py +24 -0
- sourcecode/reconciliation.py +210 -43
- sourcecode/spring_impact.py +35 -4
- sourcecode/summarizer.py +20 -4
- {sourcecode-2.5.8.dist-info → sourcecode-2.5.12.dist-info}/METADATA +1 -1
- {sourcecode-2.5.8.dist-info → sourcecode-2.5.12.dist-info}/RECORD +15 -15
- {sourcecode-2.5.8.dist-info → sourcecode-2.5.12.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.8.dist-info → sourcecode-2.5.12.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.8.dist-info → sourcecode-2.5.12.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -5,9 +5,8 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Literal, Optional
|
|
6
6
|
|
|
7
7
|
from sourcecode.reconciliation import (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
unattested_layers,
|
|
8
|
+
reconcile_architecture_evidence,
|
|
9
|
+
reconcile_code_modules,
|
|
11
10
|
)
|
|
12
11
|
|
|
13
12
|
from sourcecode.schema import (
|
|
@@ -280,6 +279,18 @@ class ArchitectureAnalyzer:
|
|
|
280
279
|
) -> ArchitectureAnalysis:
|
|
281
280
|
limitations: list[str] = []
|
|
282
281
|
evidence: list[dict] = []
|
|
282
|
+
# INV-F1-4: reconciliation rules that refute a layer/pattern must not
|
|
283
|
+
# apply their verdict silently — a pruned layer or a rejected pattern is
|
|
284
|
+
# a positively-detected contradiction the reader is entitled to see, or
|
|
285
|
+
# "no architecture" becomes indistinguishable from "we refuted the
|
|
286
|
+
# architecture claim". `_detect_layers` and its helpers append the
|
|
287
|
+
# findings' sink-neutral detail here; drained into `limitations` below.
|
|
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] = []
|
|
283
294
|
|
|
284
295
|
# Step 1: filter paths
|
|
285
296
|
filtered = self._filter_paths(sm.file_paths)
|
|
@@ -482,6 +493,25 @@ class ArchitectureAnalyzer:
|
|
|
482
493
|
"confidence": "low",
|
|
483
494
|
})
|
|
484
495
|
|
|
496
|
+
# INV-F1-4 sink: surface every refutation that shaped the returned
|
|
497
|
+
# result. Prefix frames it as a refutation; the detail (unchanged) states
|
|
498
|
+
# the contradiction, keeping it sink-neutral (INV-F1-5). Dedup: the
|
|
499
|
+
# layered→modular fallthrough can refute the same directory twice.
|
|
500
|
+
_seen: set[str] = set()
|
|
501
|
+
for _detail in self._refutations:
|
|
502
|
+
if _detail in _seen:
|
|
503
|
+
continue
|
|
504
|
+
_seen.add(_detail)
|
|
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}")
|
|
514
|
+
|
|
485
515
|
return ArchitectureAnalysis(
|
|
486
516
|
requested=True,
|
|
487
517
|
pattern=pattern,
|
|
@@ -627,6 +657,24 @@ class ArchitectureAnalyzer:
|
|
|
627
657
|
return domains
|
|
628
658
|
|
|
629
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
|
+
|
|
630
678
|
# Exclude non-source paths (tests, benchmarks, docs, tooling, vendored assets)
|
|
631
679
|
# from layer scoring. Also exclude anything under a `resources/` segment: in
|
|
632
680
|
# Maven/Gradle layouts `src/main/resources/**` is bundled config/assets — e.g.
|
|
@@ -665,6 +713,13 @@ class ArchitectureAnalyzer:
|
|
|
665
713
|
best_priority = -1
|
|
666
714
|
best_matched: dict[str, list[str]] = {}
|
|
667
715
|
best_layer_files: dict[str, set[int]] = {}
|
|
716
|
+
# INV-F1-4: refutations that fired while assembling each candidate. Only
|
|
717
|
+
# those that shaped the RETURNED result are surfaced (below): the winning
|
|
718
|
+
# pattern's pruned layers, or — when no classical pattern qualifies — the
|
|
719
|
+
# contradictions that removed the contenders. A refutation of a candidate
|
|
720
|
+
# that lost on score anyway is not what the reader is being misled about.
|
|
721
|
+
cand_refutations: dict[str, list[str]] = {}
|
|
722
|
+
contradiction_details: list[str] = []
|
|
668
723
|
|
|
669
724
|
for pattern_name, layer_keys in LAYER_PATTERNS.items():
|
|
670
725
|
matched: dict[str, list[str]] = {}
|
|
@@ -692,16 +747,34 @@ class ArchitectureAnalyzer:
|
|
|
692
747
|
# template/asset-only layers are untouched, so a real Spring-MVC
|
|
693
748
|
# `templates/` view survives.
|
|
694
749
|
_layer_paths = {k: [source_paths[i] for i in v] for k, v in layer_files.items()}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
750
|
+
_refs: list[str] = []
|
|
751
|
+
_split_contradicted = False
|
|
752
|
+
for _f in reconcile_architecture_evidence(
|
|
753
|
+
pattern=pattern_name, layer_files=_layer_paths, repo_files=source_paths
|
|
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
|
|
760
|
+
_refs.append(_f.detail)
|
|
761
|
+
if _f.rule == "composition_runtime_split":
|
|
762
|
+
# A claim that IS a runtime split must show one: JobRunr, a JVM
|
|
763
|
+
# library, read "Fullstack with frontend, backend layers" off a
|
|
764
|
+
# `dashboard/ui/model/` package of 16 Java files. Nothing to
|
|
765
|
+
# prune — the pattern itself is what the evidence contradicts.
|
|
766
|
+
_split_contradicted = True
|
|
767
|
+
else:
|
|
768
|
+
# layer_language_coherence: drop the layers owned by a
|
|
769
|
+
# different runtime; template/asset-only layers are untouched,
|
|
770
|
+
# so a real Spring-MVC `templates/` view survives.
|
|
771
|
+
for _bad in _f.symbols:
|
|
772
|
+
matched.pop(_bad, None)
|
|
773
|
+
layer_files.pop(_bad, None)
|
|
774
|
+
if _split_contradicted:
|
|
775
|
+
contradiction_details.extend(_refs)
|
|
704
776
|
continue
|
|
777
|
+
cand_refutations[pattern_name] = _refs
|
|
705
778
|
|
|
706
779
|
# A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
|
|
707
780
|
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
@@ -717,6 +790,10 @@ class ArchitectureAnalyzer:
|
|
|
717
790
|
best_layer_files = layer_files
|
|
718
791
|
|
|
719
792
|
if best_score >= 2:
|
|
793
|
+
# The reported pattern is the one whose refutations misled the reader
|
|
794
|
+
# if left silent (keycloak's dropped `view` layer). Contradictions of
|
|
795
|
+
# OTHER, losing candidates did not shape this result — omit them.
|
|
796
|
+
self._refutations.extend(cand_refutations.get(best_pattern, []))
|
|
720
797
|
layer_confidence: Literal["high", "medium", "low"] = "medium" if best_score >= 3 else "low"
|
|
721
798
|
layers: list[ArchitectureLayer] = []
|
|
722
799
|
for layer_key, matched_dirs in best_matched.items():
|
|
@@ -729,6 +806,11 @@ class ArchitectureAnalyzer:
|
|
|
729
806
|
))
|
|
730
807
|
return best_pattern, layers
|
|
731
808
|
|
|
809
|
+
# No classical pattern qualified. If a contender was removed by a runtime
|
|
810
|
+
# split contradiction (jobrunr's "fullstack"), that is why — and the
|
|
811
|
+
# eventual fallthrough result would otherwise look like a silent absence.
|
|
812
|
+
self._refutations.extend(contradiction_details)
|
|
813
|
+
|
|
732
814
|
# 2. Spring domain-module detection (petclinic-style: deep common prefix + feature dirs)
|
|
733
815
|
spring_result = self._detect_spring_domain_modules(source_paths)
|
|
734
816
|
if spring_result is not None:
|
|
@@ -872,8 +954,16 @@ class ArchitectureAnalyzer:
|
|
|
872
954
|
# Architecture with orchestration, data layers" rested on ONE directory
|
|
873
955
|
# entry plus a settings.gradle, with 47 Java files visible and ignored.
|
|
874
956
|
# A logical code layer that holds no code of this repo is not one.
|
|
875
|
-
|
|
876
|
-
|
|
957
|
+
# INV-F1-4: this path only runs when no classical pattern won, so a
|
|
958
|
+
# pruning here directly shapes the returned result (fewer layers, or a
|
|
959
|
+
# drop below 2 that falls through to flat/unknown) — surface it.
|
|
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
|
|
964
|
+
for _bad in _f.symbols:
|
|
965
|
+
non_empty.pop(_bad, None)
|
|
966
|
+
self._refutations.append(_f.detail)
|
|
877
967
|
if len(non_empty) >= 2:
|
|
878
968
|
return "layered", [
|
|
879
969
|
ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
|
|
@@ -904,8 +994,14 @@ class ArchitectureAnalyzer:
|
|
|
904
994
|
# count alone let build and asset directories be reported as modules —
|
|
905
995
|
# ofbiz "docker, framework, runtime, gradle", neo4j "packaging",
|
|
906
996
|
# eureka "images" (five PNGs). The real modules survive untouched.
|
|
907
|
-
|
|
908
|
-
|
|
997
|
+
# INV-F1-4: same as the layered path — surface each refuted module.
|
|
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
|
|
1002
|
+
for _bad in _f.symbols:
|
|
1003
|
+
meaningful.pop(_bad, None)
|
|
1004
|
+
self._refutations.append(_f.detail)
|
|
909
1005
|
if len(meaningful) >= 2:
|
|
910
1006
|
return "modular", [
|
|
911
1007
|
ArchitectureLayer(name=k, pattern="modular", files=v, confidence="low")
|
|
@@ -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).
|
sourcecode/cli.py
CHANGED
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Optional, cast
|
|
10
|
+
from typing import Any, NoReturn, Optional, cast
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
|
|
@@ -293,6 +293,30 @@ _OPTIONS_WITH_VALUE: frozenset[str] = frozenset({
|
|
|
293
293
|
})
|
|
294
294
|
|
|
295
295
|
|
|
296
|
+
def _reject_path_before_subcommand(path_token: str, subcommand: str) -> "NoReturn":
|
|
297
|
+
"""Refuse `ask <path> <subcommand>` instead of silently scanning the CWD.
|
|
298
|
+
|
|
299
|
+
`_detected_path` is only read by the root analysis: a subcommand takes its own
|
|
300
|
+
`path` Argument, which defaults to the current directory. So the path token in
|
|
301
|
+
`ask spaghetti-api endpoints` was swallowed here and the subcommand ran against
|
|
302
|
+
the CWD — from a directory of checkouts that is every repository at once.
|
|
303
|
+
Measured: it answered with 1077 endpoints from keycloak, neo4j and eureka
|
|
304
|
+
instead of spaghetti-api's 7, exit code 0, no warning.
|
|
305
|
+
|
|
306
|
+
A wrong answer that looks right is the one failure this tool must not produce,
|
|
307
|
+
and the token order cannot be repaired by guessing: subcommands disagree about
|
|
308
|
+
where a path sits (`impact-chain <symbol> <path>`), so there is no slot to move
|
|
309
|
+
it to. Name the correct command and stop.
|
|
310
|
+
"""
|
|
311
|
+
print(
|
|
312
|
+
f"error: path '{path_token}' comes before the '{subcommand}' subcommand, "
|
|
313
|
+
f"so it would be ignored and the current directory scanned instead.\n"
|
|
314
|
+
f" use: ask {subcommand} {path_token}",
|
|
315
|
+
file=sys.stderr,
|
|
316
|
+
)
|
|
317
|
+
raise SystemExit(2)
|
|
318
|
+
|
|
319
|
+
|
|
296
320
|
def _preprocess_args(args: list[str]) -> list[str]:
|
|
297
321
|
"""Extract a repository path token from an args list and store it in _detected_path.
|
|
298
322
|
|
|
@@ -300,6 +324,8 @@ def _preprocess_args(args: list[str]) -> list[str]:
|
|
|
300
324
|
Correctly skips option values (e.g. ``yaml`` in ``--format yaml``).
|
|
301
325
|
If the first non-flag, non-value positional token is a known subcommand name,
|
|
302
326
|
args are returned unchanged so Click can dispatch the subcommand.
|
|
327
|
+
A path token followed by a subcommand is rejected — see
|
|
328
|
+
``_reject_path_before_subcommand``.
|
|
303
329
|
"""
|
|
304
330
|
result = list(args)
|
|
305
331
|
skip_next = False
|
|
@@ -315,11 +341,17 @@ def _preprocess_args(args: list[str]) -> list[str]:
|
|
|
315
341
|
skip_next = True
|
|
316
342
|
continue
|
|
317
343
|
if arg in _SUBCOMMANDS:
|
|
344
|
+
if _path_index >= 0:
|
|
345
|
+
# A path already claimed a slot, so this subcommand can only be
|
|
346
|
+
# the wrong-order form: the path would be dropped and the CWD
|
|
347
|
+
# scanned in its place.
|
|
348
|
+
_reject_path_before_subcommand(result[_path_index], arg)
|
|
318
349
|
return result # known subcommand — leave for Click to dispatch
|
|
350
|
+
if _path_index >= 0:
|
|
351
|
+
continue # a later positional is the subcommand's own business
|
|
319
352
|
# First genuine positional: treat as repository path
|
|
320
353
|
_set_detected_path(arg)
|
|
321
354
|
_path_index = i
|
|
322
|
-
break
|
|
323
355
|
if _path_index >= 0:
|
|
324
356
|
result.pop(_path_index)
|
|
325
357
|
return result
|
|
@@ -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/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
|
# ---------------------------------------------------------------------------
|
|
@@ -374,21 +430,38 @@ def _rule_composition_runtime_split(
|
|
|
374
430
|
]
|
|
375
431
|
|
|
376
432
|
|
|
433
|
+
def reconcile_code_modules(
|
|
434
|
+
*, layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
435
|
+
) -> list[ReconciliationFinding]:
|
|
436
|
+
"""Run the code-module attestation rule. Never raises.
|
|
437
|
+
|
|
438
|
+
Returns the findings (name AND detail), so a consumer can both prune the
|
|
439
|
+
refuted layers and SURFACE why — INV-F1-4: a rule that is applied silently
|
|
440
|
+
replaces one dishonesty with another.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
layer_files: {claimed module -> its files}.
|
|
444
|
+
repo_files: every source file, supplying the repo's own runtime.
|
|
445
|
+
"""
|
|
446
|
+
def _run() -> list[ReconciliationFinding]:
|
|
447
|
+
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
448
|
+
return _rule_layer_code_attestation(runtime_census(layer_files), repo_runtimes)
|
|
449
|
+
|
|
450
|
+
return _run_rule("layer_code_attestation", _run)
|
|
451
|
+
|
|
452
|
+
|
|
377
453
|
def unattested_layers(
|
|
378
454
|
layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
379
455
|
) -> frozenset[str]:
|
|
380
456
|
"""Layers holding no code of the repo's runtime (convenience view).
|
|
381
457
|
|
|
382
|
-
|
|
458
|
+
Names only; consumers that must also surface the contradiction use
|
|
459
|
+
`reconcile_code_modules` instead. Never raises.
|
|
383
460
|
"""
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
out
|
|
387
|
-
|
|
388
|
-
out.update(f.symbols)
|
|
389
|
-
return frozenset(out)
|
|
390
|
-
except Exception:
|
|
391
|
-
return frozenset()
|
|
461
|
+
out: set[str] = set()
|
|
462
|
+
for f in reconcile_code_modules(layer_files=layer_files, repo_files=repo_files):
|
|
463
|
+
out.update(f.symbols)
|
|
464
|
+
return frozenset(out)
|
|
392
465
|
|
|
393
466
|
|
|
394
467
|
def reconcile_architecture_evidence(
|
|
@@ -405,16 +478,22 @@ def reconcile_architecture_evidence(
|
|
|
405
478
|
repo_files: every source file considered — supplies the repo's own
|
|
406
479
|
runtime, the arbiter of which layers belong to the claim.
|
|
407
480
|
"""
|
|
408
|
-
findings: list[ReconciliationFinding] = []
|
|
409
481
|
try:
|
|
410
482
|
census = runtime_census(layer_files)
|
|
411
483
|
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
)
|
|
418
497
|
return findings
|
|
419
498
|
|
|
420
499
|
|
|
@@ -470,6 +549,19 @@ _STACK_FAMILY: dict[str, str] = {
|
|
|
470
549
|
"elixir": "beam", "erlang": "beam",
|
|
471
550
|
}
|
|
472
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
|
+
|
|
473
565
|
# Project types that are SUPPOSED to span runtimes — their headline names a tier
|
|
474
566
|
# split, so a framework from another runtime is the point, not a contradiction.
|
|
475
567
|
# Same carve-out (and same names) as `_CROSS_RUNTIME_PATTERNS` for layer claims.
|
|
@@ -577,17 +669,13 @@ def reconcile_headline_evidence(
|
|
|
577
669
|
"""
|
|
578
670
|
if project_type in _CROSS_RUNTIME_TYPES:
|
|
579
671
|
return []
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
)
|
|
588
|
-
except Exception:
|
|
589
|
-
pass
|
|
590
|
-
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
|
+
)
|
|
591
679
|
|
|
592
680
|
|
|
593
681
|
def type_claim_unattested(
|
|
@@ -614,6 +702,12 @@ def type_claim_unattested(
|
|
|
614
702
|
return False
|
|
615
703
|
return not any(family in fams for fams in owners.values())
|
|
616
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.
|
|
617
711
|
return False
|
|
618
712
|
|
|
619
713
|
|
|
@@ -629,6 +723,81 @@ def unattested_frameworks(
|
|
|
629
723
|
return frozenset(out)
|
|
630
724
|
|
|
631
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
|
+
|
|
632
801
|
def reconcile_extraction_evidence(
|
|
633
802
|
*,
|
|
634
803
|
type_decl_files: frozenset[str],
|
|
@@ -640,9 +809,7 @@ def reconcile_extraction_evidence(
|
|
|
640
809
|
type_decl_files: files whose masked source declares a Java type.
|
|
641
810
|
extracted_files: files symbol extraction produced ≥1 symbol for.
|
|
642
811
|
"""
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
pass
|
|
648
|
-
return findings
|
|
812
|
+
return _run_rule(
|
|
813
|
+
"parse_coverage",
|
|
814
|
+
lambda: _rule_parse_coverage(type_decl_files, extracted_files),
|
|
815
|
+
)
|
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=OYjcfmD8BAbYO_oviKYCufYRUQmu9cFURHhQC4BEofc,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=
|
|
13
|
-
sourcecode/cli.py,sha256=
|
|
12
|
+
sourcecode/classifier.py,sha256=rkMapdklDuIv3rXrNEwwL65UldkvTn8VuKa73GPoSDo,18849
|
|
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
|
|
@@ -48,10 +48,10 @@ sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9
|
|
|
48
48
|
sourcecode/path_filters.py,sha256=T1y0SJsmUJBju2Hh_-z40kURAowUEq42FpPgWLbOaY0,6931
|
|
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
|
|
@@ -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.12.dist-info/METADATA,sha256=_ZdJavgYcJbSneqocYtK_QZUZ7nsYc9aJkKevISBgO8,10852
|
|
141
|
+
sourcecode-2.5.12.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
142
|
+
sourcecode-2.5.12.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
143
|
+
sourcecode-2.5.12.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
144
|
+
sourcecode-2.5.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|