sourcecode 2.5.7__tar.gz → 2.5.11__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.7 → sourcecode-2.5.11}/CHANGELOG.md +137 -0
  2. {sourcecode-2.5.7 → sourcecode-2.5.11}/PKG-INFO +1 -1
  3. {sourcecode-2.5.7 → sourcecode-2.5.11}/pyproject.toml +1 -1
  4. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/architecture_analyzer.py +70 -16
  6. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/cli.py +34 -2
  7. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/reconciliation.py +26 -9
  8. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/ris.py +39 -2
  9. {sourcecode-2.5.7 → sourcecode-2.5.11}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-2.5.7 → sourcecode-2.5.11}/.gitignore +0 -0
  11. {sourcecode-2.5.7 → sourcecode-2.5.11}/.ruff.toml +0 -0
  12. {sourcecode-2.5.7 → sourcecode-2.5.11}/CONTRIBUTING.md +0 -0
  13. {sourcecode-2.5.7 → sourcecode-2.5.11}/LICENSE +0 -0
  14. {sourcecode-2.5.7 → sourcecode-2.5.11}/README.md +0 -0
  15. {sourcecode-2.5.7 → sourcecode-2.5.11}/SECURITY.md +0 -0
  16. {sourcecode-2.5.7 → sourcecode-2.5.11}/raw +0 -0
  17. {sourcecode-2.5.7 → sourcecode-2.5.11}/scripts/compare_integration_engines.py +0 -0
  18. {sourcecode-2.5.7 → sourcecode-2.5.11}/scripts/customer_smoke_test.sh +0 -0
  19. {sourcecode-2.5.7 → sourcecode-2.5.11}/scripts/generate_jdk_exports.py +0 -0
  20. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/adaptive_scanner.py +0 -0
  21. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/archetype.py +0 -0
  22. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/architecture_summary.py +0 -0
  23. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/ast_extractor.py +0 -0
  24. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/cache.py +0 -0
  25. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/call_surface.py +0 -0
  26. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/caller_metrics.py +0 -0
  27. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/canonical_ir.py +0 -0
  28. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/cir_graphs.py +0 -0
  29. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/classifier.py +0 -0
  30. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/confidence_analyzer.py +0 -0
  32. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/context_cache.py +0 -0
  33. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/context_graph.py +0 -0
  34. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/context_scorer.py +0 -0
  35. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/context_summarizer.py +0 -0
  36. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/contract_model.py +0 -0
  37. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/contract_pipeline.py +0 -0
  38. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/coverage_parser.py +0 -0
  39. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/dependency_analyzer.py +0 -0
  40. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/__init__.py +0 -0
  41. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/base.py +0 -0
  42. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/csproj_parser.py +0 -0
  43. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/dart.py +0 -0
  44. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/dotnet.py +0 -0
  45. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/elixir.py +0 -0
  46. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/go.py +0 -0
  47. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/heuristic.py +0 -0
  48. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/hybrid.py +0 -0
  49. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/java.py +0 -0
  50. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/jvm_ext.py +0 -0
  51. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/nodejs.py +0 -0
  52. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/parsers.py +0 -0
  53. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/php.py +0 -0
  54. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/project.py +0 -0
  55. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/python.py +0 -0
  56. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/ruby.py +0 -0
  57. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/rust.py +0 -0
  58. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/systems.py +0 -0
  59. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/terraform.py +0 -0
  60. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/detectors/tooling.py +0 -0
  61. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/doc_analyzer.py +0 -0
  62. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/dynamic_argument_surface.py +0 -0
  63. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/endpoint_literals.py +0 -0
  64. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/endpoint_metrics.py +0 -0
  65. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/entrypoint_classifier.py +0 -0
  66. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/env_analyzer.py +0 -0
  67. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/error_schema.py +0 -0
  68. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/evidence_provider.py +0 -0
  69. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/explain.py +0 -0
  70. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/file_chunker.py +0 -0
  71. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/file_classifier.py +0 -0
  72. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/format_contract.py +0 -0
  73. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/fqn_utils.py +0 -0
  74. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/git_analyzer.py +0 -0
  75. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/graph_analyzer.py +0 -0
  76. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/graph_evidence.py +0 -0
  77. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/hibernate_strat.py +0 -0
  78. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/jdk_exports.py +0 -0
  79. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/license.py +0 -0
  80. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/__init__.py +0 -0
  81. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  82. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  83. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  84. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  85. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  86. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/orchestrator.py +0 -0
  87. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/registry.py +0 -0
  88. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/runner.py +0 -0
  89. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp/server.py +0 -0
  90. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/mcp_nudge.py +0 -0
  91. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/metrics_analyzer.py +0 -0
  92. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/migrate_check.py +0 -0
  93. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/openapi_surface.py +0 -0
  94. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/output_budget.py +0 -0
  95. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/path_filters.py +0 -0
  96. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/pr_comment_renderer.py +0 -0
  97. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/pr_impact.py +0 -0
  98. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/prepare_context.py +0 -0
  99. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/progress.py +0 -0
  100. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/ranking_engine.py +0 -0
  101. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/redactor.py +0 -0
  102. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/relevance_scorer.py +0 -0
  103. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/rename_refactor.py +0 -0
  104. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/repo_classifier.py +0 -0
  105. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/repository_ir.py +0 -0
  106. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/__init__.py +0 -0
  107. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/context.py +0 -0
  108. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/errors.py +0 -0
  109. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/executor.py +0 -0
  110. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/planner.py +0 -0
  111. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/query.py +0 -0
  112. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/request.py +0 -0
  113. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/resolution.py +0 -0
  114. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/result.py +0 -0
  115. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/retriever.py +0 -0
  116. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/runtime.py +0 -0
  117. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/steps.py +0 -0
  118. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  119. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/steps_graph.py +0 -0
  120. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/steps_impact.py +0 -0
  121. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/steps_intf.py +0 -0
  122. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/steps_struct.py +0 -0
  123. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  124. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/runtime_classifier.py +0 -0
  125. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/scanner.py +0 -0
  126. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/schema.py +0 -0
  127. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/security_config.py +0 -0
  128. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/security_posture.py +0 -0
  129. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/semantic_analyzer.py +0 -0
  130. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/semantic_impact_engine.py +0 -0
  131. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/semantic_integration_engine.py +0 -0
  132. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/semantic_services.py +0 -0
  133. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/serializer.py +0 -0
  134. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/spring_event_topology.py +0 -0
  135. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/spring_findings.py +0 -0
  136. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/spring_impact.py +0 -0
  137. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/spring_model.py +0 -0
  138. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/spring_security_audit.py +0 -0
  139. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/spring_semantic.py +0 -0
  140. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/spring_tx_analyzer.py +0 -0
  141. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/summarizer.py +0 -0
  142. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/telemetry/__init__.py +0 -0
  143. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/telemetry/config.py +0 -0
  144. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/telemetry/consent.py +0 -0
  145. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/telemetry/events.py +0 -0
  146. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/telemetry/filters.py +0 -0
  147. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/telemetry/transport.py +0 -0
  148. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/tree_utils.py +0 -0
  149. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/type_usage_surface.py +0 -0
  150. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/validation_inference.py +0 -0
  151. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/validation_surface.py +0 -0
  152. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/version_check.py +0 -0
  153. {sourcecode-2.5.7 → sourcecode-2.5.11}/src/sourcecode/workspace.py +0 -0
  154. {sourcecode-2.5.7 → sourcecode-2.5.11}/supabase/functions/README.md +0 -0
  155. {sourcecode-2.5.7 → sourcecode-2.5.11}/supabase/functions/get-license/index.ts +0 -0
  156. {sourcecode-2.5.7 → sourcecode-2.5.11}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  157. {sourcecode-2.5.7 → sourcecode-2.5.11}/supabase/functions/telemetry/index.ts +0 -0
  158. {sourcecode-2.5.7 → sourcecode-2.5.11}/supabase/sql/license_event_ordering.sql +0 -0
  159. {sourcecode-2.5.7 → sourcecode-2.5.11}/supabase/sql/licensing_schema.sql +0 -0
  160. {sourcecode-2.5.7 → sourcecode-2.5.11}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,142 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.11] — 2026-07-17
4
+
5
+ **Two ways a tool can lie by omission: analyze the wrong repository without
6
+ saying so, and refute a claim without saying so.** Both closed here. Also lands
7
+ ADR-0005, which ratifies the Evidence Reconciliation Layer as infrastructure and
8
+ records — with measurements — which of its "missing pieces" were never needed.
9
+
10
+ ### Fixed
11
+
12
+ - **`ask <path> <subcommand>` silently analyzed the current directory instead
13
+ (P2-G)** (`cli.py`): the argument preprocessor popped the first positional into
14
+ a path that only the top-level `main` reads. Every subcommand declares its own
15
+ `path = Argument(".")`, so a leading path before a subcommand was not
16
+ misparsed — it was **discarded**, and the subcommand fell back to the CWD.
17
+ `ask spaghetti-api endpoints` reported **1077 endpoints from every repo in the
18
+ parent directory** (the real surface was 7), exit 0, no warning — an agent
19
+ consuming that JSON could not tell it was reading someone else's code. Now
20
+ refused: exit 2, empty stdout, naming the correct invocation. Reordering cannot
21
+ be guessed (`impact <symbol> <path>` has no free slot). The false-positive
22
+ guard (an option *value* that happens to match a subcommand name, e.g.
23
+ `--files endpoints`) is covered. The test suite was itself a victim: four tests
24
+ invoked the wrong-order form and passed while analyzing atlas-cli — they
25
+ asserted only "valid JSON", which any repo produces. Corrected to assert an
26
+ artifact only the intended fixture yields.
27
+
28
+ - **Refuted architecture claims were pruned silently (INV-F1-4)**
29
+ (`architecture_analyzer.py`, `reconciliation.py`): three reconciliation rules
30
+ (`layer_language_coherence`, `composition_runtime_split`,
31
+ `layer_code_attestation`) were consumed through name-only projections that
32
+ discarded the finding's explanation. The analyzer dropped a layer or rejected a
33
+ pattern and told the reader nothing — ofbiz showed fewer modules, jobrunr's
34
+ "fullstack" vanished, keycloak's foreign `view` layer disappeared, each without
35
+ a trace. A reader could not distinguish *"this repo has no architecture"* from
36
+ *"we refuted the architecture claim"* — the exact distinction the analyzer's
37
+ degrade-to-N/A discipline exists to preserve. Refutations that shape the
38
+ reported result now surface in `limitations`, prefixed *"Architecture claim
39
+ refuted on evidence: …"*. Scoped to avoid noise: the winning pattern's pruned
40
+ layers, the runtime-split contradiction that removed a contender, and every
41
+ pruning in the fallback paths (which run only when nothing else qualified).
42
+
43
+ ### Added
44
+
45
+ - **ADR-0005 — Evidence Reconciliation Layer (F-1)**
46
+ (`docs/architecture/adr/0005-evidence-reconciliation.md`): the layer's contract,
47
+ invariants and limits. Ratifies the contract as **data** (immutable views +
48
+ pure-function rules + `ReconciliationFinding`) and **rejects** the proposed
49
+ provider protocol — an ABC spanning the six consumers would constrain nothing.
50
+ Documents that the "precursors" slated for migration (the Confidence envelope,
51
+ `coverage_confidence`, `zero_result_reason`, readiness N/A) are single-view
52
+ mechanisms that must stay out of F-1, and that pipelines — not commands — are
53
+ the unit of coverage (`onboard`/`review-pr`/`fix-bug` inherit reconciliation
54
+ from `prepare_context`).
55
+
56
+ ## [2.5.9] — 2026-07-17
57
+
58
+ **A silently dead fast path.** The backlog carried the `entry_points`
59
+ list-vs-dict drift as a schema tidiness item. It was not: the drift had disabled
60
+ a feature, quietly, on exactly the repositories that feature exists for.
61
+
62
+ ### Fixed
63
+
64
+ - **RIS stored `entry_points` in whichever shape it was handed
65
+ (P2-F-RESIDUE(b))** (`ris.py`): `agent_view` emits that key in two shapes under
66
+ one `schema_version` — `{"bootstrap": [path…], "security": […],
67
+ "controllers": {counts}}` when a bootstrap entry point is detected, otherwise
68
+ `[{"path": …, "kind": …}, …]`. RIS copied either one verbatim into
69
+ `structural_map["entrypoints"]`, a key its readers treat as a list of path
70
+ **strings**: they slice it, then guard `isinstance(fp, str)`.
71
+
72
+ Both shapes broke it, measured against the real function:
73
+
74
+ | shape | result |
75
+ |---|---|
76
+ | 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. |
77
+ | list of dicts (everything else) | `isinstance(fp, str)` is false for every item → **zero entry-point files recovered**, silently. |
78
+
79
+ The fast path therefore recovered entry points for **no repository at all**,
80
+ and a bare `except` kept it quiet. `_entrypoint_paths()` now normalizes either
81
+ shape — plus plain path lists and junk — to the paths RIS promises. Only
82
+ `bootstrap`/`security` carry paths, so the `controllers` count summary cannot
83
+ leak into the list.
84
+
85
+ The public shape drift is deliberately untouched: unifying it changes a
86
+ documented `schema_version` and belongs in its own release. RIS's contract now
87
+ holds whichever shape it is handed, which is what its readers depend on.
88
+
89
+ 3 new tests; suite `3466 passed, 5 skipped, 1 deselected` (the deselection is a
90
+ pre-existing `file_chunker` failure unrelated to this work).
91
+
92
+ ## [2.5.8] — 2026-07-17
93
+
94
+ **A count must name what it counted.** 2.5.7 taught the headline to say *"Polyglot
95
+ repository … no single stack owns this repository's architecture."* Three lines
96
+ below it, the same field still said:
97
+
98
+ > 1 project detected.
99
+ > Stack: console.
100
+ > Target: net6.0.
101
+
102
+ on the confluent `examples` monorepo — 20+ subprojects, mostly Java/Python/Go.
103
+
104
+ ### Fixed
105
+
106
+ - **`.NET` summary lines were narrated as claims about the repository
107
+ (P2-F-RESIDUE(c))** (`architecture_summary.py`): every one of those lines
108
+ counts `.csproj` files. They are facts about the .NET stack alone —
109
+ *"1 project detected"* is true of the dotnet stack and false of the repo, and
110
+ it landed directly beside a headline that had just called the repo polyglot.
111
+ Each line now names the stack it measured:
112
+
113
+ > C#/.NET projects: 1.
114
+ > C#/.NET project types: console.
115
+ > C#/.NET target frameworks: net6.0.
116
+
117
+ Naming the stack costs one word and makes each line true on **any** repo, so no
118
+ polyglot detection is needed to decide the wording — a pure .NET solution reads
119
+ *"C#/.NET projects: 1."* just as correctly. The label comes from the same
120
+ `_STACK_LABELS` table the headline uses, never a second literal.
121
+
122
+ Two user-facing Spanish strings in an otherwise English summary went with them:
123
+ *"Patrón detectado: …"* and *"Solución .NET detectada"* now read *"C#/.NET
124
+ solution pattern: …"* and *"C#/.NET solution detected."*
125
+
126
+ ### Corrected
127
+
128
+ - **The 2.5.7 entry overstated its own tests.** It read *"16 new tests; suite
129
+ 3467 passed"*. Both numbers were derived arithmetically from a
130
+ `--collect-only` total — which includes skips — instead of being read off a
131
+ pytest summary line. Measured: **14 new tests**, and the suite reports
132
+ `3462 passed, 5 skipped, 1 deselected`. The entry is fixed above. A changelog
133
+ that inflates its own evidence is the same defect class these releases are
134
+ about; the numbers here are quoted from the summary line, not computed.
135
+
136
+ 1 new test; suite `3463 passed, 5 skipped, 1 deselected` (the deselection is a
137
+ pre-existing `file_chunker` failure unrelated to this work — the real ofbiz file
138
+ shrank below the test's 7500-line assertion).
139
+
3
140
  ## [2.5.7] — 2026-07-17
4
141
 
5
142
  **A framework claim must be attested by its own runtime.** The `examples` repo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.7
3
+ Version: 2.5.11
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.7"
7
+ version = "2.5.11"
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.7"
7
+ __version__ = "2.5.11"
@@ -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,13 @@ 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] = []
283
289
 
284
290
  # Step 1: filter paths
285
291
  filtered = self._filter_paths(sm.file_paths)
@@ -482,6 +488,17 @@ class ArchitectureAnalyzer:
482
488
  "confidence": "low",
483
489
  })
484
490
 
491
+ # INV-F1-4 sink: surface every refutation that shaped the returned
492
+ # result. Prefix frames it as a refutation; the detail (unchanged) states
493
+ # the contradiction, keeping it sink-neutral (INV-F1-5). Dedup: the
494
+ # layered→modular fallthrough can refute the same directory twice.
495
+ _seen: set[str] = set()
496
+ for _detail in self._refutations:
497
+ if _detail in _seen:
498
+ continue
499
+ _seen.add(_detail)
500
+ limitations.append(f"Architecture claim refuted on evidence: {_detail}")
501
+
485
502
  return ArchitectureAnalysis(
486
503
  requested=True,
487
504
  pattern=pattern,
@@ -665,6 +682,13 @@ class ArchitectureAnalyzer:
665
682
  best_priority = -1
666
683
  best_matched: dict[str, list[str]] = {}
667
684
  best_layer_files: dict[str, set[int]] = {}
685
+ # INV-F1-4: refutations that fired while assembling each candidate. Only
686
+ # those that shaped the RETURNED result are surfaced (below): the winning
687
+ # pattern's pruned layers, or — when no classical pattern qualifies — the
688
+ # contradictions that removed the contenders. A refutation of a candidate
689
+ # that lost on score anyway is not what the reader is being misled about.
690
+ cand_refutations: dict[str, list[str]] = {}
691
+ contradiction_details: list[str] = []
668
692
 
669
693
  for pattern_name, layer_keys in LAYER_PATTERNS.items():
670
694
  matched: dict[str, list[str]] = {}
@@ -692,16 +716,29 @@ class ArchitectureAnalyzer:
692
716
  # template/asset-only layers are untouched, so a real Spring-MVC
693
717
  # `templates/` view survives.
694
718
  _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):
719
+ _refs: list[str] = []
720
+ _split_contradicted = False
721
+ for _f in reconcile_architecture_evidence(
722
+ pattern=pattern_name, layer_files=_layer_paths, repo_files=source_paths
723
+ ):
724
+ _refs.append(_f.detail)
725
+ if _f.rule == "composition_runtime_split":
726
+ # A claim that IS a runtime split must show one: JobRunr, a JVM
727
+ # library, read "Fullstack with frontend, backend layers" off a
728
+ # `dashboard/ui/model/` package of 16 Java files. Nothing to
729
+ # prune — the pattern itself is what the evidence contradicts.
730
+ _split_contradicted = True
731
+ else:
732
+ # layer_language_coherence: drop the layers owned by a
733
+ # different runtime; template/asset-only layers are untouched,
734
+ # so a real Spring-MVC `templates/` view survives.
735
+ for _bad in _f.symbols:
736
+ matched.pop(_bad, None)
737
+ layer_files.pop(_bad, None)
738
+ if _split_contradicted:
739
+ contradiction_details.extend(_refs)
704
740
  continue
741
+ cand_refutations[pattern_name] = _refs
705
742
 
706
743
  # A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
707
744
  required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
@@ -717,6 +754,10 @@ class ArchitectureAnalyzer:
717
754
  best_layer_files = layer_files
718
755
 
719
756
  if best_score >= 2:
757
+ # The reported pattern is the one whose refutations misled the reader
758
+ # if left silent (keycloak's dropped `view` layer). Contradictions of
759
+ # OTHER, losing candidates did not shape this result — omit them.
760
+ self._refutations.extend(cand_refutations.get(best_pattern, []))
720
761
  layer_confidence: Literal["high", "medium", "low"] = "medium" if best_score >= 3 else "low"
721
762
  layers: list[ArchitectureLayer] = []
722
763
  for layer_key, matched_dirs in best_matched.items():
@@ -729,6 +770,11 @@ class ArchitectureAnalyzer:
729
770
  ))
730
771
  return best_pattern, layers
731
772
 
773
+ # No classical pattern qualified. If a contender was removed by a runtime
774
+ # split contradiction (jobrunr's "fullstack"), that is why — and the
775
+ # eventual fallthrough result would otherwise look like a silent absence.
776
+ self._refutations.extend(contradiction_details)
777
+
732
778
  # 2. Spring domain-module detection (petclinic-style: deep common prefix + feature dirs)
733
779
  spring_result = self._detect_spring_domain_modules(source_paths)
734
780
  if spring_result is not None:
@@ -872,8 +918,13 @@ class ArchitectureAnalyzer:
872
918
  # Architecture with orchestration, data layers" rested on ONE directory
873
919
  # entry plus a settings.gradle, with 47 Java files visible and ignored.
874
920
  # 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)
921
+ # INV-F1-4: this path only runs when no classical pattern won, so a
922
+ # pruning here directly shapes the returned result (fewer layers, or a
923
+ # drop below 2 that falls through to flat/unknown) — surface it.
924
+ for _f in reconcile_code_modules(layer_files=non_empty, repo_files=paths):
925
+ for _bad in _f.symbols:
926
+ non_empty.pop(_bad, None)
927
+ self._refutations.append(_f.detail)
877
928
  if len(non_empty) >= 2:
878
929
  return "layered", [
879
930
  ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
@@ -904,8 +955,11 @@ class ArchitectureAnalyzer:
904
955
  # count alone let build and asset directories be reported as modules —
905
956
  # ofbiz "docker, framework, runtime, gradle", neo4j "packaging",
906
957
  # eureka "images" (five PNGs). The real modules survive untouched.
907
- for _bad in unattested_layers(meaningful, paths):
908
- meaningful.pop(_bad, None)
958
+ # INV-F1-4: same as the layered path — surface each refuted module.
959
+ for _f in reconcile_code_modules(layer_files=meaningful, repo_files=paths):
960
+ for _bad in _f.symbols:
961
+ meaningful.pop(_bad, None)
962
+ self._refutations.append(_f.detail)
909
963
  if len(meaningful) >= 2:
910
964
  return "modular", [
911
965
  ArchitectureLayer(name=k, pattern="modular", files=v, confidence="low")
@@ -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
@@ -374,21 +374,38 @@ def _rule_composition_runtime_split(
374
374
  ]
375
375
 
376
376
 
377
+ def reconcile_code_modules(
378
+ *, layer_files: dict[str, list[str]], repo_files: "list[str]"
379
+ ) -> list[ReconciliationFinding]:
380
+ """Run the code-module attestation rule. Never raises.
381
+
382
+ Returns the findings (name AND detail), so a consumer can both prune the
383
+ refuted layers and SURFACE why — INV-F1-4: a rule that is applied silently
384
+ replaces one dishonesty with another.
385
+
386
+ Args:
387
+ layer_files: {claimed module -> its files}.
388
+ repo_files: every source file, supplying the repo's own runtime.
389
+ """
390
+ try:
391
+ repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
392
+ return _rule_layer_code_attestation(runtime_census(layer_files), repo_runtimes)
393
+ except Exception:
394
+ return []
395
+
396
+
377
397
  def unattested_layers(
378
398
  layer_files: dict[str, list[str]], repo_files: "list[str]"
379
399
  ) -> frozenset[str]:
380
400
  """Layers holding no code of the repo's runtime (convenience view).
381
401
 
382
- For consumers claiming code modules / logical code layers. Never raises.
402
+ Names only; consumers that must also surface the contradiction use
403
+ `reconcile_code_modules` instead. Never raises.
383
404
  """
384
- try:
385
- repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
386
- out: set[str] = set()
387
- for f in _rule_layer_code_attestation(runtime_census(layer_files), repo_runtimes):
388
- out.update(f.symbols)
389
- return frozenset(out)
390
- except Exception:
391
- return frozenset()
405
+ out: set[str] = set()
406
+ for f in reconcile_code_modules(layer_files=layer_files, repo_files=repo_files):
407
+ out.update(f.symbols)
408
+ return frozenset(out)
392
409
 
393
410
 
394
411
  def reconcile_architecture_evidence(
@@ -29,7 +29,7 @@ import subprocess
29
29
  from dataclasses import asdict, dataclass
30
30
  from datetime import datetime, timezone
31
31
  from pathlib import Path
32
- from typing import Optional
32
+ from typing import Any, Optional
33
33
 
34
34
  RIS_SCHEMA_VERSION: str = "1.0"
35
35
  _RIS_FILENAME: str = "ris.json.gz"
@@ -107,9 +107,46 @@ def save_ris(repo_root: Path, ris: RepositoryIntelligenceSnapshot) -> None:
107
107
  # Data extraction from L1 core_dict
108
108
  # ---------------------------------------------------------------------------
109
109
 
110
+ def _entrypoint_paths(raw: Any) -> list[str]:
111
+ """Normalize agent_view's `entry_points` to the file paths RIS promises.
112
+
113
+ `structural_map["entrypoints"]` is consumed as a list of path STRINGS (it is
114
+ sliced, and every reader guards with `isinstance(fp, str)`), but agent_view
115
+ emits that key in two different shapes under one schema_version:
116
+
117
+ {"bootstrap": [path, …], "security": […], "controllers": {counts}}
118
+ when a bootstrap/security entry point was detected (Spring, …)
119
+ [{"path": …, "kind": …}, …]
120
+ otherwise
121
+
122
+ Storing either verbatim broke BOTH readers: the dict raised `KeyError` on
123
+ `entrypoints[:10]` — swallowed by a bare `except Exception`, silently
124
+ disabling the RIS fast path on exactly the Java/Spring repos it exists for —
125
+ and the list of dicts recovered zero paths, because a dict is not a `str`.
126
+
127
+ The shape drift itself is a separate schema question; normalizing here keeps
128
+ RIS's own contract true whichever shape it is handed, including plain paths.
129
+ """
130
+ if isinstance(raw, dict):
131
+ # Only bootstrap/security carry paths; `controllers` is a count summary.
132
+ out: list[str] = []
133
+ for key in ("bootstrap", "security"):
134
+ out.extend(p for p in (raw.get(key) or []) if isinstance(p, str))
135
+ return out
136
+ if isinstance(raw, list):
137
+ out = []
138
+ for item in raw:
139
+ if isinstance(item, str):
140
+ out.append(item)
141
+ elif isinstance(item, dict) and isinstance(item.get("path"), str):
142
+ out.append(item["path"])
143
+ return out
144
+ return []
145
+
146
+
110
147
  def _extract_structural_map(agent_data: dict) -> dict:
111
148
  """Extract structural map from agent_view output."""
112
- entry_points = agent_data.get("entry_points", [])
149
+ entry_points = _entrypoint_paths(agent_data.get("entry_points", []))
113
150
 
114
151
  layers = []
115
152
  domains = []
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes