sourcecode 2.5.8__tar.gz → 2.5.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. {sourcecode-2.5.8 → sourcecode-2.5.12}/CHANGELOG.md +138 -0
  2. {sourcecode-2.5.8 → sourcecode-2.5.12}/PKG-INFO +1 -1
  3. {sourcecode-2.5.8 → sourcecode-2.5.12}/pyproject.toml +1 -1
  4. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/architecture_analyzer.py +112 -16
  6. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/architecture_summary.py +75 -21
  7. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/classifier.py +1 -1
  8. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/cli.py +34 -2
  9. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/confidence_analyzer.py +14 -0
  10. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/prepare_context.py +24 -0
  11. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/reconciliation.py +210 -43
  12. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_impact.py +35 -4
  13. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/summarizer.py +20 -4
  14. {sourcecode-2.5.8 → sourcecode-2.5.12}/.github/workflows/build-windows.yml +0 -0
  15. {sourcecode-2.5.8 → sourcecode-2.5.12}/.gitignore +0 -0
  16. {sourcecode-2.5.8 → sourcecode-2.5.12}/.ruff.toml +0 -0
  17. {sourcecode-2.5.8 → sourcecode-2.5.12}/CONTRIBUTING.md +0 -0
  18. {sourcecode-2.5.8 → sourcecode-2.5.12}/LICENSE +0 -0
  19. {sourcecode-2.5.8 → sourcecode-2.5.12}/README.md +0 -0
  20. {sourcecode-2.5.8 → sourcecode-2.5.12}/SECURITY.md +0 -0
  21. {sourcecode-2.5.8 → sourcecode-2.5.12}/raw +0 -0
  22. {sourcecode-2.5.8 → sourcecode-2.5.12}/scripts/compare_integration_engines.py +0 -0
  23. {sourcecode-2.5.8 → sourcecode-2.5.12}/scripts/customer_smoke_test.sh +0 -0
  24. {sourcecode-2.5.8 → sourcecode-2.5.12}/scripts/generate_jdk_exports.py +0 -0
  25. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/adaptive_scanner.py +0 -0
  26. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/archetype.py +0 -0
  27. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/ast_extractor.py +0 -0
  28. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/cache.py +0 -0
  29. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/call_surface.py +0 -0
  30. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/caller_metrics.py +0 -0
  31. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/canonical_ir.py +0 -0
  32. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/cir_graphs.py +0 -0
  33. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/code_notes_analyzer.py +0 -0
  34. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/context_cache.py +0 -0
  35. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/context_graph.py +0 -0
  36. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/context_scorer.py +0 -0
  37. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/context_summarizer.py +0 -0
  38. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/contract_model.py +0 -0
  39. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/contract_pipeline.py +0 -0
  40. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/coverage_parser.py +0 -0
  41. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/dependency_analyzer.py +0 -0
  42. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/__init__.py +0 -0
  43. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/base.py +0 -0
  44. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/csproj_parser.py +0 -0
  45. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/dart.py +0 -0
  46. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/dotnet.py +0 -0
  47. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/elixir.py +0 -0
  48. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/go.py +0 -0
  49. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/heuristic.py +0 -0
  50. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/hybrid.py +0 -0
  51. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/java.py +0 -0
  52. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/jvm_ext.py +0 -0
  53. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/nodejs.py +0 -0
  54. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/parsers.py +0 -0
  55. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/php.py +0 -0
  56. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/project.py +0 -0
  57. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/python.py +0 -0
  58. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/ruby.py +0 -0
  59. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/rust.py +0 -0
  60. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/systems.py +0 -0
  61. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/terraform.py +0 -0
  62. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/detectors/tooling.py +0 -0
  63. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/doc_analyzer.py +0 -0
  64. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/dynamic_argument_surface.py +0 -0
  65. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/endpoint_literals.py +0 -0
  66. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/endpoint_metrics.py +0 -0
  67. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/entrypoint_classifier.py +0 -0
  68. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/env_analyzer.py +0 -0
  69. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/error_schema.py +0 -0
  70. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/evidence_provider.py +0 -0
  71. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/explain.py +0 -0
  72. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/file_chunker.py +0 -0
  73. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/file_classifier.py +0 -0
  74. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/format_contract.py +0 -0
  75. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/fqn_utils.py +0 -0
  76. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/git_analyzer.py +0 -0
  77. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/graph_analyzer.py +0 -0
  78. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/graph_evidence.py +0 -0
  79. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/hibernate_strat.py +0 -0
  80. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/jdk_exports.py +0 -0
  81. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/license.py +0 -0
  82. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/__init__.py +0 -0
  83. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  84. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  85. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  86. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  87. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  88. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/orchestrator.py +0 -0
  89. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/registry.py +0 -0
  90. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/runner.py +0 -0
  91. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp/server.py +0 -0
  92. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/mcp_nudge.py +0 -0
  93. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/metrics_analyzer.py +0 -0
  94. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/migrate_check.py +0 -0
  95. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/openapi_surface.py +0 -0
  96. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/output_budget.py +0 -0
  97. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/path_filters.py +0 -0
  98. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/pr_comment_renderer.py +0 -0
  99. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/pr_impact.py +0 -0
  100. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/progress.py +0 -0
  101. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/ranking_engine.py +0 -0
  102. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/redactor.py +0 -0
  103. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/relevance_scorer.py +0 -0
  104. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/rename_refactor.py +0 -0
  105. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/repo_classifier.py +0 -0
  106. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/repository_ir.py +0 -0
  107. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/__init__.py +0 -0
  108. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/context.py +0 -0
  109. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/errors.py +0 -0
  110. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/executor.py +0 -0
  111. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/planner.py +0 -0
  112. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/query.py +0 -0
  113. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/request.py +0 -0
  114. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/resolution.py +0 -0
  115. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/result.py +0 -0
  116. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/retriever.py +0 -0
  117. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/runtime.py +0 -0
  118. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps.py +0 -0
  119. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  120. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_graph.py +0 -0
  121. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_impact.py +0 -0
  122. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_intf.py +0 -0
  123. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_struct.py +0 -0
  124. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  125. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/ris.py +0 -0
  126. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/runtime_classifier.py +0 -0
  127. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/scanner.py +0 -0
  128. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/schema.py +0 -0
  129. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/security_config.py +0 -0
  130. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/security_posture.py +0 -0
  131. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/semantic_analyzer.py +0 -0
  132. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/semantic_impact_engine.py +0 -0
  133. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/semantic_integration_engine.py +0 -0
  134. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/semantic_services.py +0 -0
  135. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/serializer.py +0 -0
  136. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_event_topology.py +0 -0
  137. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_findings.py +0 -0
  138. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_model.py +0 -0
  139. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_security_audit.py +0 -0
  140. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_semantic.py +0 -0
  141. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/spring_tx_analyzer.py +0 -0
  142. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/__init__.py +0 -0
  143. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/config.py +0 -0
  144. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/consent.py +0 -0
  145. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/events.py +0 -0
  146. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/filters.py +0 -0
  147. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/telemetry/transport.py +0 -0
  148. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/tree_utils.py +0 -0
  149. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/type_usage_surface.py +0 -0
  150. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/validation_inference.py +0 -0
  151. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/validation_surface.py +0 -0
  152. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/version_check.py +0 -0
  153. {sourcecode-2.5.8 → sourcecode-2.5.12}/src/sourcecode/workspace.py +0 -0
  154. {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/functions/README.md +0 -0
  155. {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/functions/get-license/index.ts +0 -0
  156. {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  157. {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/functions/telemetry/index.ts +0 -0
  158. {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/sql/license_event_ordering.sql +0 -0
  159. {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/sql/licensing_schema.sql +0 -0
  160. {sourcecode-2.5.8 → sourcecode-2.5.12}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,143 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.12] — 2026-07-18
4
+
5
+ **F-1 (the Evidence Reconciliation Layer) is closed and its rule core frozen.** This release
6
+ lands the reconciliation gap-fixes accumulated since 2.5.11 and completes INV-F1-4 across
7
+ every surface that narrates an architectural result — the layer's last open honesty debt.
8
+ No new rule, no widened heuristic; the frozen core (`reconciliation.py`) was not touched.
9
+
10
+ ### Fixed
11
+
12
+ - **INV-F1-4 closed across the narrating surfaces** (`architecture_analyzer.py`,
13
+ `prepare_context.py`): `036eaa9` made the producer record every architecture refutation on
14
+ `arch.limitations`, but only the serializer surface transported them — `prepare-context`/
15
+ onboard computed the analysis and threw the refutations away, so ofbiz's pruned modules and
16
+ a refuted layered claim vanished silently there. onboard now runs the analysis once (a
17
+ single owned `sm.architecture`) and carries the refutation lines into `limitations`, the
18
+ same sink the serializer uses. A reader can distinguish *"no architecture"* from *"claim
19
+ refuted"* on every narrating surface. One ownership model governs which surface shows,
20
+ degrades, or only transports a refutation (ADR-0005 §6.5).
21
+
22
+ - **Directory contamination that manufactured phantom refutations** (`architecture_analyzer.py`):
23
+ `flatten_file_tree` emits directory nodes, so `sm.file_paths` carried entries like
24
+ `src/main` that `_detect_layers` counted as jvm-less module members — a directory holding
25
+ all the Java transitively read as "no jvm code" and was refuted (petclinic phantom
26
+ `orchestration`; the directory half of `data`). The layer census now drops ancestor
27
+ (directory) paths; leaf files, including template view-layers, are untouched. Side effect:
28
+ pattern detection on the previously-polluted input is corrected (petclinic → `mvc`).
29
+
30
+ - **The web-pattern cross-check is now an F-1 rule (gap #4)** (`reconciliation.py`,
31
+ `architecture_summary.py`): `_qualify_web_pattern` was an ad-hoc `if`; it is now the seventh
32
+ rule, `web_pattern_endpoint_attestation`. The migration fixed a latent lie — the gate
33
+ degraded at `high < 5`, so a repo with 1–4 real controllers reported *"no HTTP controllers
34
+ detected"*. The honest predicate is `high == 0` (provable absence), and the authority is
35
+ Java-file coverage (`stack == "java"`), not the JVM family.
36
+
37
+ - **A reconciliation rule that raises can no longer read as "no contradiction" (gap #2)**
38
+ (`reconciliation.py` and all six consumers): each entry point wraps rules per-rule; an
39
+ exception yields an execution-error finding routed to a surfaced channel, never silence.
40
+
41
+ - **JVM checks gate on the runtime family, reconciled to one source (gap #3)**
42
+ (`reconciliation.py`, `classifier.py`): `stack_runtime_family` is the single owner; the two
43
+ incomplete `{java,kotlin}` copies are gone. The mvc/rest-api authority gate is separately
44
+ corrected to Java-file coverage, fixing a Kotlin/Scala/Groovy false-negation (INV-F1-1).
45
+
46
+ ### Documentation
47
+
48
+ - **ADR-0005 marked Accepted & FROZEN.** Seven rules, six invariants closed, ownership model
49
+ ratified (§6.5), freeze & re-open conditions recorded (§10). Stale "six rules" / "INV-F1-4
50
+ violated" text removed. Closure report and surface-integration map in `.planning/`.
51
+
52
+ ## [2.5.11] — 2026-07-17
53
+
54
+ **Two ways a tool can lie by omission: analyze the wrong repository without
55
+ saying so, and refute a claim without saying so.** Both closed here. Also lands
56
+ ADR-0005, which ratifies the Evidence Reconciliation Layer as infrastructure and
57
+ records — with measurements — which of its "missing pieces" were never needed.
58
+
59
+ ### Fixed
60
+
61
+ - **`ask <path> <subcommand>` silently analyzed the current directory instead
62
+ (P2-G)** (`cli.py`): the argument preprocessor popped the first positional into
63
+ a path that only the top-level `main` reads. Every subcommand declares its own
64
+ `path = Argument(".")`, so a leading path before a subcommand was not
65
+ misparsed — it was **discarded**, and the subcommand fell back to the CWD.
66
+ `ask spaghetti-api endpoints` reported **1077 endpoints from every repo in the
67
+ parent directory** (the real surface was 7), exit 0, no warning — an agent
68
+ consuming that JSON could not tell it was reading someone else's code. Now
69
+ refused: exit 2, empty stdout, naming the correct invocation. Reordering cannot
70
+ be guessed (`impact <symbol> <path>` has no free slot). The false-positive
71
+ guard (an option *value* that happens to match a subcommand name, e.g.
72
+ `--files endpoints`) is covered. The test suite was itself a victim: four tests
73
+ invoked the wrong-order form and passed while analyzing atlas-cli — they
74
+ asserted only "valid JSON", which any repo produces. Corrected to assert an
75
+ artifact only the intended fixture yields.
76
+
77
+ - **Refuted architecture claims were pruned silently (INV-F1-4)**
78
+ (`architecture_analyzer.py`, `reconciliation.py`): three reconciliation rules
79
+ (`layer_language_coherence`, `composition_runtime_split`,
80
+ `layer_code_attestation`) were consumed through name-only projections that
81
+ discarded the finding's explanation. The analyzer dropped a layer or rejected a
82
+ pattern and told the reader nothing — ofbiz showed fewer modules, jobrunr's
83
+ "fullstack" vanished, keycloak's foreign `view` layer disappeared, each without
84
+ a trace. A reader could not distinguish *"this repo has no architecture"* from
85
+ *"we refuted the architecture claim"* — the exact distinction the analyzer's
86
+ degrade-to-N/A discipline exists to preserve. Refutations that shape the
87
+ reported result now surface in `limitations`, prefixed *"Architecture claim
88
+ refuted on evidence: …"*. Scoped to avoid noise: the winning pattern's pruned
89
+ layers, the runtime-split contradiction that removed a contender, and every
90
+ pruning in the fallback paths (which run only when nothing else qualified).
91
+
92
+ ### Added
93
+
94
+ - **ADR-0005 — Evidence Reconciliation Layer (F-1)**
95
+ (`docs/architecture/adr/0005-evidence-reconciliation.md`): the layer's contract,
96
+ invariants and limits. Ratifies the contract as **data** (immutable views +
97
+ pure-function rules + `ReconciliationFinding`) and **rejects** the proposed
98
+ provider protocol — an ABC spanning the six consumers would constrain nothing.
99
+ Documents that the "precursors" slated for migration (the Confidence envelope,
100
+ `coverage_confidence`, `zero_result_reason`, readiness N/A) are single-view
101
+ mechanisms that must stay out of F-1, and that pipelines — not commands — are
102
+ the unit of coverage (`onboard`/`review-pr`/`fix-bug` inherit reconciliation
103
+ from `prepare_context`).
104
+
105
+ ## [2.5.9] — 2026-07-17
106
+
107
+ **A silently dead fast path.** The backlog carried the `entry_points`
108
+ list-vs-dict drift as a schema tidiness item. It was not: the drift had disabled
109
+ a feature, quietly, on exactly the repositories that feature exists for.
110
+
111
+ ### Fixed
112
+
113
+ - **RIS stored `entry_points` in whichever shape it was handed
114
+ (P2-F-RESIDUE(b))** (`ris.py`): `agent_view` emits that key in two shapes under
115
+ one `schema_version` — `{"bootstrap": [path…], "security": […],
116
+ "controllers": {counts}}` when a bootstrap entry point is detected, otherwise
117
+ `[{"path": …, "kind": …}, …]`. RIS copied either one verbatim into
118
+ `structural_map["entrypoints"]`, a key its readers treat as a list of path
119
+ **strings**: they slice it, then guard `isinstance(fp, str)`.
120
+
121
+ Both shapes broke it, measured against the real function:
122
+
123
+ | shape | result |
124
+ |---|---|
125
+ | dict (Spring, …) | `entrypoints[:10]` → `KeyError: slice(None, 10, None)`. `prepare_context._try_ris_fast_path` wraps its whole body in `except Exception: return None`, so **the RIS fast path was silently dead on Java/Spring repos** — falling back to full analysis with no warning. |
126
+ | list of dicts (everything else) | `isinstance(fp, str)` is false for every item → **zero entry-point files recovered**, silently. |
127
+
128
+ The fast path therefore recovered entry points for **no repository at all**,
129
+ and a bare `except` kept it quiet. `_entrypoint_paths()` now normalizes either
130
+ shape — plus plain path lists and junk — to the paths RIS promises. Only
131
+ `bootstrap`/`security` carry paths, so the `controllers` count summary cannot
132
+ leak into the list.
133
+
134
+ The public shape drift is deliberately untouched: unifying it changes a
135
+ documented `schema_version` and belongs in its own release. RIS's contract now
136
+ holds whichever shape it is handed, which is what its readers depend on.
137
+
138
+ 3 new tests; suite `3466 passed, 5 skipped, 1 deselected` (the deselection is a
139
+ pre-existing `file_chunker` failure unrelated to this work).
140
+
3
141
  ## [2.5.8] — 2026-07-17
4
142
 
5
143
  **A count must name what it counted.** 2.5.7 taught the headline to say *"Polyglot
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.8
3
+ Version: 2.5.12
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "2.5.8"
7
+ version = "2.5.12"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.5.8"
7
+ __version__ = "2.5.12"
@@ -5,9 +5,8 @@ from pathlib import Path
5
5
  from typing import Literal, Optional
6
6
 
7
7
  from sourcecode.reconciliation import (
8
- incoherent_layers,
9
- pattern_contradicted,
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
- for _bad in incoherent_layers(pattern_name, _layer_paths, source_paths):
696
- matched.pop(_bad, None)
697
- layer_files.pop(_bad, None)
698
-
699
- # A claim that IS a runtime split must show one: JobRunr, a JVM
700
- # library, read "Fullstack with frontend, backend layers" off a
701
- # `dashboard/ui/model/` package of 16 Java files. Nothing to prune
702
- # the pattern itself is what the evidence contradicts.
703
- if pattern_contradicted(pattern_name, _layer_paths, source_paths):
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
- for _bad in unattested_layers(non_empty, paths):
876
- non_empty.pop(_bad, None)
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
- for _bad in unattested_layers(meaningful, paths):
908
- meaningful.pop(_bad, None)
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 type_claim_unattested, unattested_frameworks
213
+ from sourcecode.reconciliation import (
214
+ has_execution_error,
215
+ reconcile_headline_evidence,
216
+ type_claim_unattested,
217
+ )
214
218
 
215
- foreign = unattested_frameworks(primary.stack, sm.project_type, sm.stacks)
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
- return self._describe_foreign_evidence(sm, stack_label, runtime, dropped)
222
- fw_names = attested
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 (Java/Kotlin only — that is where we have an
228
- # authoritative extractor). Degrade to a qualified, consistent phrasing.
229
- if sm.project_type == "api" and primary.stack in {"java", "kotlin"}:
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 Java/Kotlin
314
- repo, do not assert "MVC pattern with ... view layers" in prose when the
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. Cross-check against `endpoints` and degrade to
318
- a consistent, non-committal phrasing instead of a categorical MVC claim."""
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
- if high >= _MIN_REST_ENDPOINTS_FOR_LABEL:
326
- return arch_line
327
- # No controller backing → the MVC/view claim is unverified. Report the
328
- # directory-derived layers without the framework label or the "view" claim.
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)}; "
@@ -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 & {"java", "kotlin", "scala"} and self._is_multi_module(file_tree):
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).
@@ -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"
@@ -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