sourcecode 2.5.6__tar.gz → 2.5.8__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.6 → sourcecode-2.5.8}/CHANGELOG.md +126 -0
  2. {sourcecode-2.5.6 → sourcecode-2.5.8}/PKG-INFO +1 -1
  3. {sourcecode-2.5.6 → sourcecode-2.5.8}/pyproject.toml +1 -1
  4. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/architecture_summary.py +55 -6
  6. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/confidence_analyzer.py +39 -0
  7. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/reconciliation.py +191 -0
  8. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/ris.py +39 -2
  9. {sourcecode-2.5.6 → sourcecode-2.5.8}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-2.5.6 → sourcecode-2.5.8}/.gitignore +0 -0
  11. {sourcecode-2.5.6 → sourcecode-2.5.8}/.ruff.toml +0 -0
  12. {sourcecode-2.5.6 → sourcecode-2.5.8}/CONTRIBUTING.md +0 -0
  13. {sourcecode-2.5.6 → sourcecode-2.5.8}/LICENSE +0 -0
  14. {sourcecode-2.5.6 → sourcecode-2.5.8}/README.md +0 -0
  15. {sourcecode-2.5.6 → sourcecode-2.5.8}/SECURITY.md +0 -0
  16. {sourcecode-2.5.6 → sourcecode-2.5.8}/raw +0 -0
  17. {sourcecode-2.5.6 → sourcecode-2.5.8}/scripts/compare_integration_engines.py +0 -0
  18. {sourcecode-2.5.6 → sourcecode-2.5.8}/scripts/customer_smoke_test.sh +0 -0
  19. {sourcecode-2.5.6 → sourcecode-2.5.8}/scripts/generate_jdk_exports.py +0 -0
  20. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/adaptive_scanner.py +0 -0
  21. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/archetype.py +0 -0
  22. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/architecture_analyzer.py +0 -0
  23. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/ast_extractor.py +0 -0
  24. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/cache.py +0 -0
  25. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/call_surface.py +0 -0
  26. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/caller_metrics.py +0 -0
  27. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/canonical_ir.py +0 -0
  28. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/cir_graphs.py +0 -0
  29. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/classifier.py +0 -0
  30. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/cli.py +0 -0
  31. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/code_notes_analyzer.py +0 -0
  32. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/context_cache.py +0 -0
  33. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/context_graph.py +0 -0
  34. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/context_scorer.py +0 -0
  35. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/context_summarizer.py +0 -0
  36. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/contract_model.py +0 -0
  37. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/contract_pipeline.py +0 -0
  38. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/coverage_parser.py +0 -0
  39. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/dependency_analyzer.py +0 -0
  40. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/__init__.py +0 -0
  41. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/base.py +0 -0
  42. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/csproj_parser.py +0 -0
  43. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/dart.py +0 -0
  44. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/dotnet.py +0 -0
  45. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/elixir.py +0 -0
  46. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/go.py +0 -0
  47. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/heuristic.py +0 -0
  48. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/hybrid.py +0 -0
  49. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/java.py +0 -0
  50. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/jvm_ext.py +0 -0
  51. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/nodejs.py +0 -0
  52. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/parsers.py +0 -0
  53. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/php.py +0 -0
  54. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/project.py +0 -0
  55. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/python.py +0 -0
  56. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/ruby.py +0 -0
  57. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/rust.py +0 -0
  58. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/systems.py +0 -0
  59. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/terraform.py +0 -0
  60. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/detectors/tooling.py +0 -0
  61. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/doc_analyzer.py +0 -0
  62. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/dynamic_argument_surface.py +0 -0
  63. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/endpoint_literals.py +0 -0
  64. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/endpoint_metrics.py +0 -0
  65. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/entrypoint_classifier.py +0 -0
  66. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/env_analyzer.py +0 -0
  67. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/error_schema.py +0 -0
  68. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/evidence_provider.py +0 -0
  69. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/explain.py +0 -0
  70. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/file_chunker.py +0 -0
  71. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/file_classifier.py +0 -0
  72. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/format_contract.py +0 -0
  73. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/fqn_utils.py +0 -0
  74. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/git_analyzer.py +0 -0
  75. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/graph_analyzer.py +0 -0
  76. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/graph_evidence.py +0 -0
  77. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/hibernate_strat.py +0 -0
  78. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/jdk_exports.py +0 -0
  79. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/license.py +0 -0
  80. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/__init__.py +0 -0
  81. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  82. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  83. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  84. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  85. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  86. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/orchestrator.py +0 -0
  87. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/registry.py +0 -0
  88. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/runner.py +0 -0
  89. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp/server.py +0 -0
  90. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/mcp_nudge.py +0 -0
  91. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/metrics_analyzer.py +0 -0
  92. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/migrate_check.py +0 -0
  93. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/openapi_surface.py +0 -0
  94. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/output_budget.py +0 -0
  95. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/path_filters.py +0 -0
  96. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/pr_comment_renderer.py +0 -0
  97. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/pr_impact.py +0 -0
  98. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/prepare_context.py +0 -0
  99. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/progress.py +0 -0
  100. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/ranking_engine.py +0 -0
  101. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/redactor.py +0 -0
  102. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/relevance_scorer.py +0 -0
  103. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/rename_refactor.py +0 -0
  104. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/repo_classifier.py +0 -0
  105. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/repository_ir.py +0 -0
  106. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/__init__.py +0 -0
  107. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/context.py +0 -0
  108. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/errors.py +0 -0
  109. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/executor.py +0 -0
  110. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/planner.py +0 -0
  111. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/query.py +0 -0
  112. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/request.py +0 -0
  113. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/resolution.py +0 -0
  114. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/result.py +0 -0
  115. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/retriever.py +0 -0
  116. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/runtime.py +0 -0
  117. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps.py +0 -0
  118. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  119. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_graph.py +0 -0
  120. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_impact.py +0 -0
  121. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_intf.py +0 -0
  122. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_struct.py +0 -0
  123. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  124. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/runtime_classifier.py +0 -0
  125. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/scanner.py +0 -0
  126. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/schema.py +0 -0
  127. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/security_config.py +0 -0
  128. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/security_posture.py +0 -0
  129. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/semantic_analyzer.py +0 -0
  130. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/semantic_impact_engine.py +0 -0
  131. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/semantic_integration_engine.py +0 -0
  132. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/semantic_services.py +0 -0
  133. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/serializer.py +0 -0
  134. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_event_topology.py +0 -0
  135. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_findings.py +0 -0
  136. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_impact.py +0 -0
  137. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_model.py +0 -0
  138. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_security_audit.py +0 -0
  139. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_semantic.py +0 -0
  140. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/spring_tx_analyzer.py +0 -0
  141. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/summarizer.py +0 -0
  142. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/__init__.py +0 -0
  143. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/config.py +0 -0
  144. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/consent.py +0 -0
  145. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/events.py +0 -0
  146. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/filters.py +0 -0
  147. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/telemetry/transport.py +0 -0
  148. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/tree_utils.py +0 -0
  149. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/type_usage_surface.py +0 -0
  150. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/validation_inference.py +0 -0
  151. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/validation_surface.py +0 -0
  152. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/version_check.py +0 -0
  153. {sourcecode-2.5.6 → sourcecode-2.5.8}/src/sourcecode/workspace.py +0 -0
  154. {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/functions/README.md +0 -0
  155. {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/functions/get-license/index.ts +0 -0
  156. {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  157. {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/functions/telemetry/index.ts +0 -0
  158. {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/sql/license_event_ordering.sql +0 -0
  159. {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/sql/licensing_schema.sql +0 -0
  160. {sourcecode-2.5.6 → sourcecode-2.5.8}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,131 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.8] — 2026-07-17
4
+
5
+ **A count must name what it counted.** 2.5.7 taught the headline to say *"Polyglot
6
+ repository … no single stack owns this repository's architecture."* Three lines
7
+ below it, the same field still said:
8
+
9
+ > 1 project detected.
10
+ > Stack: console.
11
+ > Target: net6.0.
12
+
13
+ on the confluent `examples` monorepo — 20+ subprojects, mostly Java/Python/Go.
14
+
15
+ ### Fixed
16
+
17
+ - **`.NET` summary lines were narrated as claims about the repository
18
+ (P2-F-RESIDUE(c))** (`architecture_summary.py`): every one of those lines
19
+ counts `.csproj` files. They are facts about the .NET stack alone —
20
+ *"1 project detected"* is true of the dotnet stack and false of the repo, and
21
+ it landed directly beside a headline that had just called the repo polyglot.
22
+ Each line now names the stack it measured:
23
+
24
+ > C#/.NET projects: 1.
25
+ > C#/.NET project types: console.
26
+ > C#/.NET target frameworks: net6.0.
27
+
28
+ Naming the stack costs one word and makes each line true on **any** repo, so no
29
+ polyglot detection is needed to decide the wording — a pure .NET solution reads
30
+ *"C#/.NET projects: 1."* just as correctly. The label comes from the same
31
+ `_STACK_LABELS` table the headline uses, never a second literal.
32
+
33
+ Two user-facing Spanish strings in an otherwise English summary went with them:
34
+ *"Patrón detectado: …"* and *"Solución .NET detectada"* now read *"C#/.NET
35
+ solution pattern: …"* and *"C#/.NET solution detected."*
36
+
37
+ ### Corrected
38
+
39
+ - **The 2.5.7 entry overstated its own tests.** It read *"16 new tests; suite
40
+ 3467 passed"*. Both numbers were derived arithmetically from a
41
+ `--collect-only` total — which includes skips — instead of being read off a
42
+ pytest summary line. Measured: **14 new tests**, and the suite reports
43
+ `3462 passed, 5 skipped, 1 deselected`. The entry is fixed above. A changelog
44
+ that inflates its own evidence is the same defect class these releases are
45
+ about; the numbers here are quoted from the summary line, not computed.
46
+
47
+ 1 new test; suite `3463 passed, 5 skipped, 1 deselected` (the deselection is a
48
+ pre-existing `file_chunker` failure unrelated to this work — the real ofbiz file
49
+ shrank below the test's 7500-line assertion).
50
+
51
+ ## [2.5.7] — 2026-07-17
52
+
53
+ **A framework claim must be attested by its own runtime.** The `examples` repo
54
+ read *"C#/.NET rest api using JAX-RS."* — a JVM-only framework attributed to a
55
+ .NET headline, self-contradictory inside a single clause. This release closes the
56
+ last of the narrative cluster, and settles how reconciliation findings reach the
57
+ surfaces that report them.
58
+
59
+ ### Fixed
60
+
61
+ - **A polyglot repository was narrated as one program (P2-F)**
62
+ (`reconciliation.py`, `architecture_summary.py`): the sentence was assembled
63
+ from two unrelated sources —
64
+
65
+ | part | comes from | on `examples` |
66
+ |---|---|---|
67
+ | `C#/.NET` | the **primary** stack | dotnet, root `.`, declares **zero** frameworks |
68
+ | `using JAX-RS` | **every** stack | java, workspace `microservices-orders`, the sole owner of JAX-RS |
69
+
70
+ New Evidence Reconciliation rule **`framework_stack_attestation`**: a framework
71
+ named in a single-program headline whose owning stacks are all of another
72
+ runtime family contradicts the claim. Ownership is already structural — a
73
+ `FrameworkDetection` is discovered inside the `StackDetection` whose manifest
74
+ declares it — so the rule reasons from evidence and never from what a framework
75
+ is called. Grouped by runtime family, so a Kotlin framework beside Java is one
76
+ program; `fullstack`/`monorepo`/`microservices` are exempt, their headline is
77
+ meant to span runtimes.
78
+
79
+ The type label had to go with it. `project_type` is decided by frameworks
80
+ pooled across every stack, while a separate vote picks the primary, so
81
+ *"rest api"* was java's evidence too — dropping only the `using` clause would
82
+ have left *"C#/.NET rest api."*, the same claim one clause shorter. The
83
+ headline now degrades to what is actually known:
84
+
85
+ > C#/.NET project. Polyglot repository: the rest api evidence (JAX-RS) is
86
+ > declared by the Java stack, not by C#/.NET — no single stack owns this
87
+ > repository's architecture. See `stacks`.
88
+
89
+ Every fact survives, correctly attributed.
90
+
91
+ - **The structured fields kept asserting what the sentence had retracted
92
+ (P2-F-RESIDUE(a))** (`confidence_analyzer.py`, `reconciliation.py`):
93
+ `project_type` and `primary_stack` are read on their own by agents that never
94
+ see the prose, and on `examples` they still said `"api"` + `"dotnet"` silently.
95
+ `ConfidenceAnalyzer` now consumes the same rule, emitting an
96
+ `analysis_gaps` entry (`area="framework_stack_attestation"`) plus an anomaly
97
+ beside its existing "ambiguous primary" siblings.
98
+
99
+ `project_type` deliberately keeps its value: the classifier's vote is a
100
+ separate decision, and minting a *"polyglot"* type here would silently rewrite
101
+ what every consumer branches on. Flagging the claim is honest; changing the
102
+ field is a different change.
103
+
104
+ ### Internal
105
+
106
+ - `_FRAMEWORK_DRIVEN_TYPES` moved out of `architecture_summary` into
107
+ `reconciliation.type_claim_unattested()`. Two surfaces must judge *"did the
108
+ frameworks decide this type"* identically — a duplicated table is how the
109
+ headline kept lying in the first place.
110
+ - `ReconciliationFinding.detail` is now **sink-neutral**. It used to narrate
111
+ itself (*"framework(s) named in the headline … the sentence attributes them
112
+ to"*), which the rule that strips the claim promptly made false. Details are
113
+ read by prose and by structured gaps alike; none may assume prose.
114
+
115
+ Validation across 12 field-test repos through the real CLI: `examples` emits the
116
+ degraded headline, the gap and the anomaly (*"JAX-RS declared by another runtime,
117
+ not by the dotnet primary stack"*); petclinic, keycloak, eureka, jobrunr, ofbiz,
118
+ jenkins, broadleaf, alfresco, openmrs, neo4j and spaghetti-api are **silent —
119
+ every headline byte-identical**, no gap, no anomaly. Every control's primary
120
+ stack declares its own frameworks, which is exactly why the rule stays quiet.
121
+
122
+ Measured along the way, and worth knowing: on a polyglot repo the **primary stack
123
+ is a coin flip** — `max(stacks, …)` breaks an exact tie between java and dotnet
124
+ (both manifest-detected, both high-confidence) by list order, and the narrative
125
+ then welded the loser's evidence onto the winner's label.
126
+
127
+ 14 new tests; suite 3462 passed, 5 skipped.
128
+
3
129
  ## [2.5.6] — 2026-07-17
4
130
 
5
131
  **A layer must be attested by code.** The architecture fallback chain reported
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.6
3
+ Version: 2.5.8
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.6"
7
+ version = "2.5.8"
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.6"
7
+ __version__ = "2.5.8"
@@ -205,6 +205,22 @@ class ArchitectureSummarizer:
205
205
  for f in s.frameworks[:2]
206
206
  ))[:3]
207
207
 
208
+ # F-1 reconciliation: the headline describes ONE program, but fw_names is
209
+ # gathered from every stack in the repo. On a polyglot repository that
210
+ # welds a foreign runtime's framework onto the primary's label. Drop what
211
+ # the claimed stack does not attest, and never let the TYPE label — which
212
+ # those same frameworks drove — survive its own evidence (P2-F).
213
+ from sourcecode.reconciliation import type_claim_unattested, unattested_frameworks
214
+
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
+ 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
223
+
208
224
  fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
209
225
  if runtime:
210
226
  # BUG #4: never assert "rest api" in the headline unless the endpoints
@@ -237,6 +253,29 @@ class ArchitectureSummarizer:
237
253
  return f"{stack_label} {runtime.lower()}{fw_str}."
238
254
  return f"{stack_label} project{fw_str}."
239
255
 
256
+ def _describe_foreign_evidence(
257
+ self, sm: SourceMap, stack_label: str, runtime: str, dropped: list[str]
258
+ ) -> str:
259
+ """Headline for a claim whose defining evidence belongs to another stack.
260
+
261
+ Says what IS known instead of asserting a program that does not exist:
262
+ the primary stack, the foreign frameworks, and the stacks that actually
263
+ declare them. The reader gets the same facts, correctly attributed.
264
+ """
265
+ from sourcecode.context_summarizer import _STACK_LABELS
266
+ from sourcecode.reconciliation import framework_owner_stacks
267
+
268
+ owners = framework_owner_stacks(sm.stacks)
269
+ owner_ids = sorted({sid for name in dropped for sid in owners.get(name, ())})
270
+ owner_labels = [_STACK_LABELS.get(sid, sid) for sid in owner_ids] or ["another"]
271
+ plural = "s" if len(owner_labels) > 1 else ""
272
+ return (
273
+ f"{stack_label} project. Polyglot repository: the {runtime.lower()} "
274
+ f"evidence ({', '.join(dropped)}) is declared by the "
275
+ f"{', '.join(owner_labels)} stack{plural}, not by {stack_label} — no "
276
+ "single stack owns this repository's architecture. See `stacks`."
277
+ )
278
+
240
279
  def _endpoint_support(self) -> tuple[int, int]:
241
280
  """Return (total, high_confidence) endpoint counts from the canonical
242
281
  Java endpoint extractor — the same source the `endpoints` command uses,
@@ -412,41 +451,51 @@ class ArchitectureSummarizer:
412
451
  return None
413
452
 
414
453
  def _summarize_dotnet_entry(self, stacks: list[StackDetection]) -> list[str]:
454
+ # P2-F-RESIDUE(c): every line here counts .csproj files — facts about the
455
+ # .NET stack ALONE. Unscoped, they read as claims about the repository:
456
+ # the confluent examples monorepo (20+ subprojects, mostly Java/Python/Go)
457
+ # reported a bare "1 project detected." beside a headline that had just
458
+ # called it polyglot. Naming the stack costs one word and makes each line
459
+ # true on any repo, so no polyglot test is needed to decide the wording.
460
+ from sourcecode.context_summarizer import _STACK_LABELS
461
+
415
462
  dotnet_stacks = [s for s in stacks if s.stack == "dotnet"]
416
463
  if not dotnet_stacks:
417
464
  return []
465
+ label = _STACK_LABELS.get("dotnet", "dotnet") # same table as the headline
418
466
  lines: list[str] = []
419
467
  signals = [sig for s in dotnet_stacks for sig in s.signals]
420
468
 
421
469
  for sig in signals:
422
470
  if "project" in sig and "detected" in sig:
423
- lines.append(sig[0].upper() + sig[1:] + ".")
471
+ count = sig.split(" ", 1)[0]
472
+ lines.append(f"{label} projects: {count}.")
424
473
  break
425
474
 
426
475
  for sig in signals:
427
476
  if sig.startswith("project types:"):
428
477
  types = sig.removeprefix("project types:").strip()
429
- lines.append(f"Stack: {types}.")
478
+ lines.append(f"{label} project types: {types}.")
430
479
  break
431
480
 
432
481
  for sig in signals:
433
482
  if sig.startswith("target frameworks:"):
434
483
  fws = sig.removeprefix("target frameworks:").strip()
435
- lines.append(f"Target: {fws}.")
484
+ lines.append(f"{label} target frameworks: {fws}.")
436
485
  break
437
486
 
438
487
  for sig in signals:
439
488
  if sig.startswith("architecture:"):
440
489
  pattern = sig.removeprefix("architecture:").strip()
441
- lines.append(f"Patrón detectado: {pattern}.")
490
+ lines.append(f"{label} solution pattern: {pattern}.")
442
491
  break
443
492
 
444
493
  framework_names = [f.name for s in dotnet_stacks for f in s.frameworks]
445
494
  if framework_names:
446
- lines.append(f"Frameworks: {', '.join(framework_names)}.")
495
+ lines.append(f"{label} frameworks: {', '.join(framework_names)}.")
447
496
 
448
497
  if not lines:
449
- lines.append("Solución .NET detectada.")
498
+ lines.append(f"{label} solution detected.")
450
499
  return lines
451
500
 
452
501
  def _summarize_go_entry(self, path: str, content: str) -> list[str]:
@@ -102,6 +102,45 @@ class ConfidenceAnalyzer:
102
102
  if heuristic_only and not any(s.detection_method != "heuristic" for s in sm.stacks):
103
103
  anomalies.append("All stacks detected via heuristic only — no manifest found")
104
104
 
105
+ # ── Anomaly: a claim attributed to a stack that does not attest it ────
106
+ # F-1 reconciliation, second sink. The headline degrades itself
107
+ # (architecture_summary), but `project_type` and `primary_stack` are
108
+ # separate structured fields an agent can read on their own — and they
109
+ # keep asserting the claim this rule refutes. Same rule, same judgement,
110
+ # different surface: here it becomes an anomaly + gap instead of prose.
111
+ if primary_stacks:
112
+ from sourcecode.reconciliation import (
113
+ reconcile_headline_evidence,
114
+ type_claim_unattested,
115
+ )
116
+
117
+ _primary = primary_stacks[0]
118
+ for _rf in reconcile_headline_evidence(
119
+ claimed_stack=_primary.stack,
120
+ project_type=sm.project_type,
121
+ stacks=sm.stacks,
122
+ ):
123
+ anomalies.append(
124
+ f"{', '.join(_rf.symbols)} declared by another runtime, not by the "
125
+ f"{_primary.stack} primary stack"
126
+ )
127
+ _reason = _rf.detail
128
+ if type_claim_unattested(
129
+ claimed_stack=_primary.stack,
130
+ project_type=sm.project_type,
131
+ stacks=sm.stacks,
132
+ ):
133
+ _reason += (
134
+ f' project_type="{sm.project_type}" rests on that same evidence — '
135
+ f"it is not attested for the {_primary.stack} primary stack, and "
136
+ "the repository is polyglot rather than one program."
137
+ )
138
+ gaps.append(AnalysisGap(
139
+ area=_rf.rule,
140
+ reason=_reason,
141
+ impact="medium",
142
+ ))
143
+
105
144
  # ── Anomaly: entry points all low-confidence ──────────────────────────
106
145
  if normalized_entry_points and all(ep.confidence == "low" for ep in normalized_entry_points):
107
146
  anomalies.append("All entry points are low-confidence (heuristic/code_signal only)")
@@ -42,6 +42,15 @@ Rule registry (architecture):
42
42
  subsystems (SIM-3: keycloak's "MVC … view layer" was a React SPA, zero
43
43
  Java files, sitting beside a 1088-file Java model).
44
44
 
45
+ Rule registry (narrative):
46
+ framework_stack_attestation — the headline welds a stack label to a
47
+ framework that no stack of that runtime declares. The stack table and
48
+ the framework-ownership view are contradicting each other: the sentence
49
+ describes one program while the evidence names two (P2-F: the confluent
50
+ examples repo read "C#/.NET rest api using JAX-RS" — a JVM framework,
51
+ declared by a Java stack in one sub-workspace, attributed to a .NET
52
+ primary that declares no framework at all).
53
+
45
54
  Never raises. Empty evidence → no findings.
46
55
  """
47
56
  from __future__ import annotations
@@ -438,6 +447,188 @@ def incoherent_layers(
438
447
  return frozenset(out)
439
448
 
440
449
 
450
+ # ---------------------------------------------------------------------------
451
+ # Narrative reconciliation (headline claim × stack ownership)
452
+ # ---------------------------------------------------------------------------
453
+
454
+ # Stack identifier → RUNTIME FAMILY. Same reasoning as `_RUNTIME_FAMILY` above,
455
+ # applied to the stack ontology instead of file extensions: a Kotlin framework
456
+ # named in a Java headline is ONE program (same runtime, direct interop), while a
457
+ # Java framework named in a .NET headline is a different one. Stacks absent here
458
+ # are deliberately unjudgeable: an unknown stack can never prove a contradiction.
459
+ _STACK_FAMILY: dict[str, str] = {
460
+ "java": "jvm", "kotlin": "jvm", "scala": "jvm", "groovy": "jvm",
461
+ "nodejs": "js", "deno": "js", "bun": "js",
462
+ "python": "python",
463
+ "go": "go",
464
+ "rust": "rust",
465
+ "ruby": "ruby",
466
+ "dotnet": "dotnet",
467
+ "php": "php",
468
+ "swift": "swift",
469
+ "dart": "dart",
470
+ "elixir": "beam", "erlang": "beam",
471
+ }
472
+
473
+ # Project types that are SUPPOSED to span runtimes — their headline names a tier
474
+ # split, so a framework from another runtime is the point, not a contradiction.
475
+ # Same carve-out (and same names) as `_CROSS_RUNTIME_PATTERNS` for layer claims.
476
+ _CROSS_RUNTIME_TYPES = _CROSS_RUNTIME_PATTERNS
477
+
478
+ # Project types DRIVEN by framework presence: `_classify_project_type` pools
479
+ # frameworks across every stack to pick them, while a separate vote picks the
480
+ # primary stack — so on a polyglot repo the type can be decided by one stack's
481
+ # evidence and reported as another's. When the frameworks that drove the type are
482
+ # all foreign to the primary, the type inherits their lie: the sentence
483
+ # "C#/.NET rest api" is the same claim as "using JAX-RS", one clause shorter.
484
+ # Lives here, not in a consumer: BOTH the headline and the confidence/gap surface
485
+ # must judge this identically or the duplicated table lies again.
486
+ _FRAMEWORK_DRIVEN_TYPES: frozenset[str] = frozenset({
487
+ "api", "webapp", "web_mvc", "angular-spa",
488
+ })
489
+
490
+
491
+ def framework_owner_stacks(stacks) -> dict[str, frozenset[str]]:
492
+ """{framework name -> stack ids that declare it} — the ownership view.
493
+
494
+ Ownership is already structural: a FrameworkDetection is discovered inside a
495
+ StackDetection's manifests, so which runtime a framework belongs to is
496
+ evidence the detector recorded, never a name lookup (VAI-safe — no rule here
497
+ branches on what the framework is called).
498
+ """
499
+ out: dict[str, set[str]] = {}
500
+ for stack in stacks or []:
501
+ sid = getattr(stack, "stack", "") or ""
502
+ if not sid:
503
+ continue
504
+ for fw in getattr(stack, "frameworks", None) or []:
505
+ name = getattr(fw, "name", "") or ""
506
+ if name:
507
+ out.setdefault(name, set()).add(sid)
508
+ return {name: frozenset(ids) for name, ids in out.items()}
509
+
510
+
511
+ def framework_owner_families(stacks) -> dict[str, frozenset[str]]:
512
+ """{framework name -> runtime families that declare it} — the ownership view."""
513
+ return {
514
+ name: frozenset(
515
+ fam for sid in ids if (fam := _STACK_FAMILY.get(sid)) is not None
516
+ )
517
+ for name, ids in framework_owner_stacks(stacks).items()
518
+ }
519
+
520
+
521
+ def _rule_framework_stack_attestation(
522
+ claimed_family: str | None,
523
+ owner_families: dict[str, frozenset[str]],
524
+ ) -> list[ReconciliationFinding]:
525
+ """A framework named in a headline is declared by no stack of the claimed runtime.
526
+
527
+ The headline describes ONE program: it welds a stack label ("C#/.NET") to the
528
+ frameworks found anywhere in the repo. On a polyglot repository those can be
529
+ different programs, and the sentence asserts a relationship the evidence
530
+ denies — the confluent examples repo read "C#/.NET rest api using JAX-RS",
531
+ attributing a JVM-only framework, declared by a Java stack in one
532
+ sub-workspace, to a .NET primary that declares no framework at all.
533
+
534
+ Fires only when ownership is PROVEN and disjoint: the framework has at least
535
+ one known owning family and the claimed family is not among them. An unknown
536
+ stack, or a framework no stack claims, yields nothing.
537
+ """
538
+ if not claimed_family:
539
+ return []
540
+ foreign = sorted(
541
+ name for name, fams in owner_families.items()
542
+ if fams and claimed_family not in fams
543
+ )
544
+ if not foreign:
545
+ return []
546
+ others = sorted({fam for name in foreign for fam in owner_families[name]})
547
+ return [
548
+ ReconciliationFinding(
549
+ rule="framework_stack_attestation",
550
+ symbols=tuple(foreign),
551
+ # Sink-neutral: this detail is read by a prose surface AND by a
552
+ # structured gap, so it states the contradiction, never "the
553
+ # headline says…" — the headline may already have dropped the claim.
554
+ detail=(
555
+ f"{len(foreign)} framework(s) are declared only by "
556
+ f"{', '.join(others)} stack(s), not by the {claimed_family} "
557
+ "primary stack — attributing them to it describes two separate "
558
+ "programs as one."
559
+ ),
560
+ )
561
+ ]
562
+
563
+
564
+ def reconcile_headline_evidence(
565
+ *,
566
+ claimed_stack: str,
567
+ project_type: str | None,
568
+ stacks,
569
+ ) -> list[ReconciliationFinding]:
570
+ """Run headline reconciliation rules. Never raises.
571
+
572
+ Args:
573
+ claimed_stack: stack id the headline's label is built from (the primary).
574
+ project_type: claimed type — cross-runtime types are exempt, their
575
+ headline is MEANT to name another tier's framework.
576
+ stacks: every StackDetection, each owning its own frameworks.
577
+ """
578
+ if project_type in _CROSS_RUNTIME_TYPES:
579
+ return []
580
+ findings: list[ReconciliationFinding] = []
581
+ try:
582
+ findings.extend(
583
+ _rule_framework_stack_attestation(
584
+ _STACK_FAMILY.get(claimed_stack or ""),
585
+ framework_owner_families(stacks),
586
+ )
587
+ )
588
+ except Exception:
589
+ pass
590
+ return findings
591
+
592
+
593
+ def type_claim_unattested(
594
+ *, claimed_stack: str, project_type: str | None, stacks
595
+ ) -> bool:
596
+ """True when a framework-driven type label rests ONLY on foreign evidence.
597
+
598
+ The sibling of `_rule_framework_stack_attestation` for the type clause: the
599
+ frameworks that decided `project_type` are declared, every one of them, by
600
+ stacks of another runtime. The narrative consumer degrades the sentence; the
601
+ confidence consumer records the gap — same judgement, two sinks, one table.
602
+
603
+ Provable only: silent when the type is not framework-driven, when the claimed
604
+ stack's runtime is unknown, or when no framework is owned at all.
605
+ """
606
+ try:
607
+ if project_type not in _FRAMEWORK_DRIVEN_TYPES:
608
+ return False
609
+ family = _STACK_FAMILY.get(claimed_stack or "")
610
+ if not family:
611
+ return False
612
+ owners = framework_owner_families(stacks)
613
+ if not owners:
614
+ return False
615
+ return not any(family in fams for fams in owners.values())
616
+ except Exception:
617
+ return False
618
+
619
+
620
+ def unattested_frameworks(
621
+ claimed_stack: str, project_type: str | None, stacks
622
+ ) -> frozenset[str]:
623
+ """Framework names the claimed stack does not attest (convenience view)."""
624
+ out: set[str] = set()
625
+ for f in reconcile_headline_evidence(
626
+ claimed_stack=claimed_stack, project_type=project_type, stacks=stacks
627
+ ):
628
+ out.update(f.symbols)
629
+ return frozenset(out)
630
+
631
+
441
632
  def reconcile_extraction_evidence(
442
633
  *,
443
634
  type_decl_files: frozenset[str],
@@ -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