sourcecode 2.5.6__tar.gz → 2.5.7__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.7}/CHANGELOG.md +78 -0
  2. {sourcecode-2.5.6 → sourcecode-2.5.7}/PKG-INFO +1 -1
  3. {sourcecode-2.5.6 → sourcecode-2.5.7}/pyproject.toml +1 -1
  4. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/architecture_summary.py +55 -6
  6. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/confidence_analyzer.py +39 -0
  7. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/reconciliation.py +191 -0
  8. {sourcecode-2.5.6 → sourcecode-2.5.7}/.github/workflows/build-windows.yml +0 -0
  9. {sourcecode-2.5.6 → sourcecode-2.5.7}/.gitignore +0 -0
  10. {sourcecode-2.5.6 → sourcecode-2.5.7}/.ruff.toml +0 -0
  11. {sourcecode-2.5.6 → sourcecode-2.5.7}/CONTRIBUTING.md +0 -0
  12. {sourcecode-2.5.6 → sourcecode-2.5.7}/LICENSE +0 -0
  13. {sourcecode-2.5.6 → sourcecode-2.5.7}/README.md +0 -0
  14. {sourcecode-2.5.6 → sourcecode-2.5.7}/SECURITY.md +0 -0
  15. {sourcecode-2.5.6 → sourcecode-2.5.7}/raw +0 -0
  16. {sourcecode-2.5.6 → sourcecode-2.5.7}/scripts/compare_integration_engines.py +0 -0
  17. {sourcecode-2.5.6 → sourcecode-2.5.7}/scripts/customer_smoke_test.sh +0 -0
  18. {sourcecode-2.5.6 → sourcecode-2.5.7}/scripts/generate_jdk_exports.py +0 -0
  19. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/adaptive_scanner.py +0 -0
  20. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/archetype.py +0 -0
  21. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/architecture_analyzer.py +0 -0
  22. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/ast_extractor.py +0 -0
  23. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/cache.py +0 -0
  24. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/call_surface.py +0 -0
  25. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/caller_metrics.py +0 -0
  26. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/canonical_ir.py +0 -0
  27. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/cir_graphs.py +0 -0
  28. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/classifier.py +0 -0
  29. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/cli.py +0 -0
  30. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/context_cache.py +0 -0
  32. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/context_graph.py +0 -0
  33. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/context_scorer.py +0 -0
  34. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/context_summarizer.py +0 -0
  35. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/contract_model.py +0 -0
  36. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/contract_pipeline.py +0 -0
  37. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/coverage_parser.py +0 -0
  38. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/dependency_analyzer.py +0 -0
  39. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/__init__.py +0 -0
  40. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/base.py +0 -0
  41. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/csproj_parser.py +0 -0
  42. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/dart.py +0 -0
  43. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/dotnet.py +0 -0
  44. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/elixir.py +0 -0
  45. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/go.py +0 -0
  46. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/heuristic.py +0 -0
  47. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/hybrid.py +0 -0
  48. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/java.py +0 -0
  49. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/jvm_ext.py +0 -0
  50. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/nodejs.py +0 -0
  51. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/parsers.py +0 -0
  52. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/php.py +0 -0
  53. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/project.py +0 -0
  54. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/python.py +0 -0
  55. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/ruby.py +0 -0
  56. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/rust.py +0 -0
  57. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/systems.py +0 -0
  58. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/terraform.py +0 -0
  59. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/detectors/tooling.py +0 -0
  60. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/doc_analyzer.py +0 -0
  61. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/dynamic_argument_surface.py +0 -0
  62. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/endpoint_literals.py +0 -0
  63. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/endpoint_metrics.py +0 -0
  64. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/entrypoint_classifier.py +0 -0
  65. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/env_analyzer.py +0 -0
  66. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/error_schema.py +0 -0
  67. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/evidence_provider.py +0 -0
  68. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/explain.py +0 -0
  69. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/file_chunker.py +0 -0
  70. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/file_classifier.py +0 -0
  71. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/format_contract.py +0 -0
  72. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/fqn_utils.py +0 -0
  73. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/git_analyzer.py +0 -0
  74. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/graph_analyzer.py +0 -0
  75. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/graph_evidence.py +0 -0
  76. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/hibernate_strat.py +0 -0
  77. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/jdk_exports.py +0 -0
  78. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/license.py +0 -0
  79. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/__init__.py +0 -0
  80. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  81. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  82. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  83. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  84. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  85. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/orchestrator.py +0 -0
  86. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/registry.py +0 -0
  87. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/runner.py +0 -0
  88. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp/server.py +0 -0
  89. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/mcp_nudge.py +0 -0
  90. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/metrics_analyzer.py +0 -0
  91. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/migrate_check.py +0 -0
  92. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/openapi_surface.py +0 -0
  93. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/output_budget.py +0 -0
  94. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/path_filters.py +0 -0
  95. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/pr_comment_renderer.py +0 -0
  96. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/pr_impact.py +0 -0
  97. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/prepare_context.py +0 -0
  98. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/progress.py +0 -0
  99. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/ranking_engine.py +0 -0
  100. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/redactor.py +0 -0
  101. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/relevance_scorer.py +0 -0
  102. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/rename_refactor.py +0 -0
  103. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/repo_classifier.py +0 -0
  104. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/repository_ir.py +0 -0
  105. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/__init__.py +0 -0
  106. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/context.py +0 -0
  107. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/errors.py +0 -0
  108. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/executor.py +0 -0
  109. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/planner.py +0 -0
  110. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/query.py +0 -0
  111. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/request.py +0 -0
  112. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/resolution.py +0 -0
  113. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/result.py +0 -0
  114. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/retriever.py +0 -0
  115. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/runtime.py +0 -0
  116. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/steps.py +0 -0
  117. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  118. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/steps_graph.py +0 -0
  119. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/steps_impact.py +0 -0
  120. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/steps_intf.py +0 -0
  121. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/steps_struct.py +0 -0
  122. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  123. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/ris.py +0 -0
  124. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/runtime_classifier.py +0 -0
  125. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/scanner.py +0 -0
  126. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/schema.py +0 -0
  127. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/security_config.py +0 -0
  128. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/security_posture.py +0 -0
  129. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/semantic_analyzer.py +0 -0
  130. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/semantic_impact_engine.py +0 -0
  131. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/semantic_integration_engine.py +0 -0
  132. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/semantic_services.py +0 -0
  133. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/serializer.py +0 -0
  134. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/spring_event_topology.py +0 -0
  135. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/spring_findings.py +0 -0
  136. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/spring_impact.py +0 -0
  137. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/spring_model.py +0 -0
  138. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/spring_security_audit.py +0 -0
  139. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/spring_semantic.py +0 -0
  140. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/spring_tx_analyzer.py +0 -0
  141. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/summarizer.py +0 -0
  142. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/telemetry/__init__.py +0 -0
  143. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/telemetry/config.py +0 -0
  144. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/telemetry/consent.py +0 -0
  145. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/telemetry/events.py +0 -0
  146. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/telemetry/filters.py +0 -0
  147. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/telemetry/transport.py +0 -0
  148. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/tree_utils.py +0 -0
  149. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/type_usage_surface.py +0 -0
  150. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/validation_inference.py +0 -0
  151. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/validation_surface.py +0 -0
  152. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/version_check.py +0 -0
  153. {sourcecode-2.5.6 → sourcecode-2.5.7}/src/sourcecode/workspace.py +0 -0
  154. {sourcecode-2.5.6 → sourcecode-2.5.7}/supabase/functions/README.md +0 -0
  155. {sourcecode-2.5.6 → sourcecode-2.5.7}/supabase/functions/get-license/index.ts +0 -0
  156. {sourcecode-2.5.6 → sourcecode-2.5.7}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  157. {sourcecode-2.5.6 → sourcecode-2.5.7}/supabase/functions/telemetry/index.ts +0 -0
  158. {sourcecode-2.5.6 → sourcecode-2.5.7}/supabase/sql/license_event_ordering.sql +0 -0
  159. {sourcecode-2.5.6 → sourcecode-2.5.7}/supabase/sql/licensing_schema.sql +0 -0
  160. {sourcecode-2.5.6 → sourcecode-2.5.7}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,83 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.7] — 2026-07-17
4
+
5
+ **A framework claim must be attested by its own runtime.** The `examples` repo
6
+ read *"C#/.NET rest api using JAX-RS."* — a JVM-only framework attributed to a
7
+ .NET headline, self-contradictory inside a single clause. This release closes the
8
+ last of the narrative cluster, and settles how reconciliation findings reach the
9
+ surfaces that report them.
10
+
11
+ ### Fixed
12
+
13
+ - **A polyglot repository was narrated as one program (P2-F)**
14
+ (`reconciliation.py`, `architecture_summary.py`): the sentence was assembled
15
+ from two unrelated sources —
16
+
17
+ | part | comes from | on `examples` |
18
+ |---|---|---|
19
+ | `C#/.NET` | the **primary** stack | dotnet, root `.`, declares **zero** frameworks |
20
+ | `using JAX-RS` | **every** stack | java, workspace `microservices-orders`, the sole owner of JAX-RS |
21
+
22
+ New Evidence Reconciliation rule **`framework_stack_attestation`**: a framework
23
+ named in a single-program headline whose owning stacks are all of another
24
+ runtime family contradicts the claim. Ownership is already structural — a
25
+ `FrameworkDetection` is discovered inside the `StackDetection` whose manifest
26
+ declares it — so the rule reasons from evidence and never from what a framework
27
+ is called. Grouped by runtime family, so a Kotlin framework beside Java is one
28
+ program; `fullstack`/`monorepo`/`microservices` are exempt, their headline is
29
+ meant to span runtimes.
30
+
31
+ The type label had to go with it. `project_type` is decided by frameworks
32
+ pooled across every stack, while a separate vote picks the primary, so
33
+ *"rest api"* was java's evidence too — dropping only the `using` clause would
34
+ have left *"C#/.NET rest api."*, the same claim one clause shorter. The
35
+ headline now degrades to what is actually known:
36
+
37
+ > C#/.NET project. Polyglot repository: the rest api evidence (JAX-RS) is
38
+ > declared by the Java stack, not by C#/.NET — no single stack owns this
39
+ > repository's architecture. See `stacks`.
40
+
41
+ Every fact survives, correctly attributed.
42
+
43
+ - **The structured fields kept asserting what the sentence had retracted
44
+ (P2-F-RESIDUE(a))** (`confidence_analyzer.py`, `reconciliation.py`):
45
+ `project_type` and `primary_stack` are read on their own by agents that never
46
+ see the prose, and on `examples` they still said `"api"` + `"dotnet"` silently.
47
+ `ConfidenceAnalyzer` now consumes the same rule, emitting an
48
+ `analysis_gaps` entry (`area="framework_stack_attestation"`) plus an anomaly
49
+ beside its existing "ambiguous primary" siblings.
50
+
51
+ `project_type` deliberately keeps its value: the classifier's vote is a
52
+ separate decision, and minting a *"polyglot"* type here would silently rewrite
53
+ what every consumer branches on. Flagging the claim is honest; changing the
54
+ field is a different change.
55
+
56
+ ### Internal
57
+
58
+ - `_FRAMEWORK_DRIVEN_TYPES` moved out of `architecture_summary` into
59
+ `reconciliation.type_claim_unattested()`. Two surfaces must judge *"did the
60
+ frameworks decide this type"* identically — a duplicated table is how the
61
+ headline kept lying in the first place.
62
+ - `ReconciliationFinding.detail` is now **sink-neutral**. It used to narrate
63
+ itself (*"framework(s) named in the headline … the sentence attributes them
64
+ to"*), which the rule that strips the claim promptly made false. Details are
65
+ read by prose and by structured gaps alike; none may assume prose.
66
+
67
+ Validation across 12 field-test repos through the real CLI: `examples` emits the
68
+ degraded headline, the gap and the anomaly (*"JAX-RS declared by another runtime,
69
+ not by the dotnet primary stack"*); petclinic, keycloak, eureka, jobrunr, ofbiz,
70
+ jenkins, broadleaf, alfresco, openmrs, neo4j and spaghetti-api are **silent —
71
+ every headline byte-identical**, no gap, no anomaly. Every control's primary
72
+ stack declares its own frameworks, which is exactly why the rule stays quiet.
73
+
74
+ Measured along the way, and worth knowing: on a polyglot repo the **primary stack
75
+ is a coin flip** — `max(stacks, …)` breaks an exact tie between java and dotnet
76
+ (both manifest-detected, both high-confidence) by list order, and the narrative
77
+ then welded the loser's evidence onto the winner's label.
78
+
79
+ 14 new tests; suite 3462 passed, 5 skipped.
80
+
3
81
  ## [2.5.6] — 2026-07-17
4
82
 
5
83
  **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.7
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.7"
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.7"
@@ -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],
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes