sourcecode 2.6.0__tar.gz → 2.6.1__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 (169) hide show
  1. {sourcecode-2.6.0 → sourcecode-2.6.1}/CHANGELOG.md +35 -0
  2. {sourcecode-2.6.0 → sourcecode-2.6.1}/PKG-INFO +1 -1
  3. {sourcecode-2.6.0 → sourcecode-2.6.1}/pyproject.toml +1 -1
  4. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/repository_ir.py +32 -5
  6. sourcecode-2.6.1/src/sourcecode/retrieval/composition.py +496 -0
  7. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/errors.py +7 -0
  8. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_endpoint.py +7 -1
  9. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_impact.py +7 -1
  10. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_impact.py +16 -8
  11. {sourcecode-2.6.0 → sourcecode-2.6.1}/.github/workflows/build-windows.yml +0 -0
  12. {sourcecode-2.6.0 → sourcecode-2.6.1}/.gitignore +0 -0
  13. {sourcecode-2.6.0 → sourcecode-2.6.1}/.ruff.toml +0 -0
  14. {sourcecode-2.6.0 → sourcecode-2.6.1}/CONTRIBUTING.md +0 -0
  15. {sourcecode-2.6.0 → sourcecode-2.6.1}/LICENSE +0 -0
  16. {sourcecode-2.6.0 → sourcecode-2.6.1}/README.md +0 -0
  17. {sourcecode-2.6.0 → sourcecode-2.6.1}/SECURITY.md +0 -0
  18. {sourcecode-2.6.0 → sourcecode-2.6.1}/raw +0 -0
  19. {sourcecode-2.6.0 → sourcecode-2.6.1}/scripts/compare_integration_engines.py +0 -0
  20. {sourcecode-2.6.0 → sourcecode-2.6.1}/scripts/customer_smoke_test.sh +0 -0
  21. {sourcecode-2.6.0 → sourcecode-2.6.1}/scripts/generate_jdk_exports.py +0 -0
  22. {sourcecode-2.6.0 → sourcecode-2.6.1}/scripts/perf_harness.py +0 -0
  23. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/adaptive_scanner.py +0 -0
  24. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/archetype.py +0 -0
  25. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/architectural_baseline.py +0 -0
  26. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/architectural_delta.py +0 -0
  27. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/architecture_analyzer.py +0 -0
  28. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/architecture_summary.py +0 -0
  29. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/ast_extractor.py +0 -0
  30. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/cache.py +0 -0
  31. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/call_surface.py +0 -0
  32. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/caller_metrics.py +0 -0
  33. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/canonical_ir.py +0 -0
  34. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/change_plan.py +0 -0
  35. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/cir_graphs.py +0 -0
  36. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/classifier.py +0 -0
  37. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/cli.py +0 -0
  38. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/code_notes_analyzer.py +0 -0
  39. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/compare.py +0 -0
  40. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/confidence_analyzer.py +0 -0
  41. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/constraint_diff.py +0 -0
  42. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/context_cache.py +0 -0
  43. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/context_graph.py +0 -0
  44. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/context_scorer.py +0 -0
  45. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/context_summarizer.py +0 -0
  46. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/contract_diff.py +0 -0
  47. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/contract_model.py +0 -0
  48. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/contract_pipeline.py +0 -0
  49. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/coverage_parser.py +0 -0
  50. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/dependency_analyzer.py +0 -0
  51. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/__init__.py +0 -0
  52. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/base.py +0 -0
  53. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/csproj_parser.py +0 -0
  54. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/dart.py +0 -0
  55. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/dotnet.py +0 -0
  56. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/elixir.py +0 -0
  57. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/go.py +0 -0
  58. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/heuristic.py +0 -0
  59. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/hybrid.py +0 -0
  60. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/java.py +0 -0
  61. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/jvm_ext.py +0 -0
  62. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/nodejs.py +0 -0
  63. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/parsers.py +0 -0
  64. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/php.py +0 -0
  65. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/project.py +0 -0
  66. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/python.py +0 -0
  67. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/ruby.py +0 -0
  68. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/rust.py +0 -0
  69. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/systems.py +0 -0
  70. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/terraform.py +0 -0
  71. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/detectors/tooling.py +0 -0
  72. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/doc_analyzer.py +0 -0
  73. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/dynamic_argument_surface.py +0 -0
  74. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/endpoint_literals.py +0 -0
  75. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/endpoint_metrics.py +0 -0
  76. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/entrypoint_classifier.py +0 -0
  77. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/env_analyzer.py +0 -0
  78. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/error_schema.py +0 -0
  79. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/evidence_provider.py +0 -0
  80. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/explain.py +0 -0
  81. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/file_chunker.py +0 -0
  82. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/file_classifier.py +0 -0
  83. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/format_contract.py +0 -0
  84. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/fqn_utils.py +0 -0
  85. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/git_analyzer.py +0 -0
  86. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/graph_analyzer.py +0 -0
  87. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/graph_evidence.py +0 -0
  88. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/hibernate_strat.py +0 -0
  89. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/jdk_exports.py +0 -0
  90. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/license.py +0 -0
  91. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/__init__.py +0 -0
  92. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  93. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  94. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  95. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  96. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  97. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/orchestrator.py +0 -0
  98. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/registry.py +0 -0
  99. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/runner.py +0 -0
  100. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp/server.py +0 -0
  101. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/mcp_nudge.py +0 -0
  102. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/metrics_analyzer.py +0 -0
  103. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/migrate_check.py +0 -0
  104. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/openapi_surface.py +0 -0
  105. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/output_budget.py +0 -0
  106. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/path_filters.py +0 -0
  107. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/perf.py +0 -0
  108. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/pr_comment_renderer.py +0 -0
  109. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/pr_impact.py +0 -0
  110. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/prepare_context.py +0 -0
  111. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/progress.py +0 -0
  112. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/ranking_engine.py +0 -0
  113. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/reconciliation.py +0 -0
  114. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/redactor.py +0 -0
  115. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/relevance_scorer.py +0 -0
  116. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/rename_refactor.py +0 -0
  117. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/repo_classifier.py +0 -0
  118. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/__init__.py +0 -0
  119. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/context.py +0 -0
  120. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/executor.py +0 -0
  121. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/planner.py +0 -0
  122. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/query.py +0 -0
  123. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/request.py +0 -0
  124. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/resolution.py +0 -0
  125. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/result.py +0 -0
  126. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/retriever.py +0 -0
  127. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/runtime.py +0 -0
  128. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps.py +0 -0
  129. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_graph.py +0 -0
  130. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_intf.py +0 -0
  131. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_struct.py +0 -0
  132. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  133. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/ris.py +0 -0
  134. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/runtime_classifier.py +0 -0
  135. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/scanner.py +0 -0
  136. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/schema.py +0 -0
  137. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/security_config.py +0 -0
  138. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/security_posture.py +0 -0
  139. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/semantic_analyzer.py +0 -0
  140. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/semantic_impact_engine.py +0 -0
  141. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/semantic_integration_engine.py +0 -0
  142. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/semantic_services.py +0 -0
  143. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/serializer.py +0 -0
  144. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_event_topology.py +0 -0
  145. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_findings.py +0 -0
  146. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_model.py +0 -0
  147. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_security_audit.py +0 -0
  148. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_semantic.py +0 -0
  149. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/spring_tx_analyzer.py +0 -0
  150. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/summarizer.py +0 -0
  151. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/__init__.py +0 -0
  152. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/config.py +0 -0
  153. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/consent.py +0 -0
  154. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/events.py +0 -0
  155. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/filters.py +0 -0
  156. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/telemetry/transport.py +0 -0
  157. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/tree_utils.py +0 -0
  158. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/type_usage_surface.py +0 -0
  159. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/validation_inference.py +0 -0
  160. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/validation_surface.py +0 -0
  161. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/version_check.py +0 -0
  162. {sourcecode-2.6.0 → sourcecode-2.6.1}/src/sourcecode/workspace.py +0 -0
  163. {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/functions/README.md +0 -0
  164. {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/functions/get-license/index.ts +0 -0
  165. {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  166. {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/functions/telemetry/index.ts +0 -0
  167. {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/sql/license_event_ordering.sql +0 -0
  168. {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/sql/licensing_schema.sql +0 -0
  169. {sourcecode-2.6.0 → sourcecode-2.6.1}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.1] — 2026-07-22
4
+
5
+ **Stability patch — extraction/label fidelity on the 2.6.x line. No new
6
+ capability, no functional-contract change.** Two isolated corrections where the
7
+ system produced a correct result but described it imperfectly (P1-H) or gave a
8
+ provably-correct answer a misleading label (SIM-4). `2.6.0` remains the stable
9
+ baseline; this is a fidelity patch on top of it. Full suite: **3637 passed, 5
10
+ skipped, 1 deselected** (the deselected chunker test is a pre-existing external
11
+ fixture flake).
12
+
13
+ ### Fixed
14
+
15
+ - **P1-H — lowercase-initial type declarations were silently dropped.**
16
+ `_CLASS_DECL_RE` assumed the PascalCase convention (`[A-Z]\w*`) for the
17
+ type-name start, so valid lowercase-initial Java types never entered the
18
+ symbol graph (e.g. `iFieldMetadata`, `i18nUpdateCartServiceExtensionHandler`;
19
+ JNA C-struct mirrors `passwd`/`group`/`pam_*`). The same assumption was
20
+ duplicated in the multi-line-join gate, so a lowercase declaration whose brace
21
+ sat on a continuation line stayed lost even after widening the primary regex.
22
+ Both name-start charsets widened to `[A-Za-z]\w*`, and a length-preserving
23
+ string-literal blanking guard was added at the declaration-scan sites so a
24
+ `class`/`interface` keyword appearing inside a string literal (e.g. a log
25
+ message `"... advice class for {} ..."`) can no longer mint a phantom type.
26
+ Fleet A/B over six real repositories: +9 real types recovered, zero phantoms,
27
+ zero symbols lost, control repositories byte-stable.
28
+ - **SIM-4 — a method-precise resolution was labelled `class_expanded`.** When a
29
+ `Class#method` query matched one class by short/suffix name and its method
30
+ resolved to exactly one method node, `impact-chain` returned the correct
31
+ method-scoped result but tagged it `class_expanded` — signalling "I broadened
32
+ your query to the whole class" over a narrow, correct answer. A distinct
33
+ resolution value `method_resolved` now describes that case (confidence stays
34
+ high; nothing was widened); the genuine no-method class-expansion case keeps
35
+ `class_expanded`. No behavioural change — the resolved symbol set, confidence,
36
+ and callers are identical; only the exposed label changed to match reality.
37
+
3
38
  ## [2.6.0] — 2026-07-22
4
39
 
5
40
  **Engineering Decision Support — Part B (capability milestone) lands, plus the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.6.0
3
+ Version: 2.6.1
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.6.0"
7
+ version = "2.6.1"
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.6.0"
7
+ __version__ = "2.6.1"
@@ -134,7 +134,13 @@ _ANN_WITH_ARGS_RE = re.compile(
134
134
  _CLASS_DECL_RE = re.compile(
135
135
  r'(?:^|(?<=\s))'
136
136
  r'(?P<kind>class|interface|enum|record|@interface)\s+'
137
- r'(?P<name>[A-Z]\w*)'
137
+ # P1-H: Java identifiers may start lowercase — the uppercase-only start
138
+ # (PascalCase convention) silently dropped valid lowercase-initial types
139
+ # (Broadleaf `iFieldMetadata`, `i18nUpdateCartServiceExtensionHandler`).
140
+ # A literal type keyword + terminal `{` already gates the match, so widening
141
+ # the name-start does not admit prose/string phantoms the uppercase form
142
+ # was not already exposed to (verified fleet A/B, no new symbols on controls).
143
+ r'(?P<name>[A-Za-z]\w*)'
138
144
  r'(?:\s*<[^{;]*?(?=>|\{))?'
139
145
  # P1-F: record component list `record Point(int x, int y)`. `[^{;]*` is greedy
140
146
  # to the LAST ')' before the body brace, so annotated components with
@@ -663,7 +669,22 @@ _ANN_NAME_RE = re.compile(r'^@\w+\s*')
663
669
  _STRING_LITERAL_RE = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
664
670
 
665
671
  # Module-level cache for class-keyword detection (avoids recompilation per _extract_symbols call)
666
- _CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum|record)\s+[A-Z]')
672
+ # P1-H: lowercase-initial start mirrors _CLASS_DECL_RE so multi-line lowercase-named
673
+ # declarations (`class i18nHandler extends A\n implements B {`) are joined too.
674
+ _CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum|record)\s+[A-Za-z]')
675
+
676
+
677
+ def _blank_string_literals(text: str) -> str:
678
+ """Return `text` with each string/char literal replaced by equal-length spaces.
679
+
680
+ P1-H: type-declaration scans (_CLASS_DECL_RE / _CLASS_KW_RE) run on lines whose
681
+ comments are stripped but whose STRING content survives. A log/message literal
682
+ such as `log.debug("... advice class for {} ...")` then matches `class for {`
683
+ and mints a phantom type `for`. Blanking the literal content (length-preserving,
684
+ so match offsets stay aligned with the original line) removes the false surface
685
+ without touching genuine declarations, which never live inside a string.
686
+ """
687
+ return _STRING_LITERAL_RE.sub(lambda m: ' ' * len(m.group(0)), text)
667
688
 
668
689
 
669
690
  # ---------------------------------------------------------------------------
@@ -958,7 +979,8 @@ def _extract_symbols(
958
979
  _start = _i # 0-based source index where this joined entry begins
959
980
  _line = _raw_lines[_i]
960
981
  _stripped = _line.strip()
961
- if (_CLASS_KW_RE.search(_stripped) and '{' not in _stripped
982
+ _kw_scan = _blank_string_literals(_stripped)
983
+ if (_CLASS_KW_RE.search(_kw_scan) and '{' not in _kw_scan
962
984
  and not _stripped.startswith('//')
963
985
  and not _stripped.startswith('*')):
964
986
  # Continuation: join until we hit a line containing '{'
@@ -1043,7 +1065,10 @@ def _extract_symbols(
1043
1065
  _pop_closed(class_stack, depth)
1044
1066
  continue
1045
1067
 
1046
- cls_m = _CLASS_DECL_RE.search(stripped)
1068
+ # P1-H: search a string-blanked view so `class`/`interface` keywords that
1069
+ # appear inside string literals (log messages, templates) do not mint a
1070
+ # phantom type. Length-preserving, so cls_m offsets align with `stripped`.
1071
+ cls_m = _CLASS_DECL_RE.search(_blank_string_literals(stripped))
1047
1072
  if cls_m:
1048
1073
  kind_kw = cls_m.group("kind")
1049
1074
  name = cls_m.group("name")
@@ -2311,7 +2336,9 @@ def _extract_field_types(
2311
2336
  package = stripped[8:].rstrip(";").strip()
2312
2337
  continue
2313
2338
  net = _count_net_braces(stripped)
2314
- cls_m = _CLASS_DECL_RE.search(stripped)
2339
+ # P1-H: string-blanked search (see _blank_string_literals) — a keyword inside
2340
+ # a literal must not push a phantom nesting frame onto class_stack.
2341
+ cls_m = _CLASS_DECL_RE.search(_blank_string_literals(stripped))
2315
2342
  if cls_m:
2316
2343
  name = cls_m.group("name")
2317
2344
  fqn = f"{class_stack[-1][0]}.{name}" if class_stack else (
@@ -0,0 +1,496 @@
1
+ """Composition Semantics — Layer 1 foundations (ADR-0007).
2
+
3
+ This module is the single source of truth for the **identity namespace** of every
4
+ entity kind the retrieval step library emits. It realizes two ADR-0007 invariants
5
+ as executable contract:
6
+
7
+ - **INV-C10 (node-vs-facet).** A kind is a *base namespace* iff it carries an
8
+ independent identity; a kind whose identity is another entity's identity is a
9
+ *facet* of that entity. The three base namespaces are ``symbol`` (the join hub),
10
+ ``endpoint`` and ``module``. Facet kinds (a tx-role, a public contract, a
11
+ validation point, a security finding) live in the base namespace of the identity
12
+ they are keyed by — e.g. ``transaction_boundary`` has ``identity = b.symbol`` and
13
+ therefore reports namespace ``symbol``.
14
+ - **INV-C5 (single bridge-graph / single mapping).** There is exactly ONE table
15
+ mapping kinds to namespaces. No capability may keep its own copy; the join engine
16
+ and every future composed intent read this one.
17
+
18
+ ``integration`` and ``event_type`` carry a synthetic independent identity but have
19
+ low join value in v1 (ADR-0007 §3); they are *peripheral* namespaces, outside the
20
+ base graph. An unknown kind maps to ``None`` — the honest answer is "not classified",
21
+ never a guess (a mis-guess would silently enable a wrong join).
22
+ """
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass, replace
26
+ from typing import Any, Callable, Iterable, Optional
27
+
28
+ from .errors import CompositionError
29
+ from .result import KnowledgeEntity
30
+
31
+ # The three base namespaces (ADR-0007 §3). Order is the hub-first convention, not
32
+ # semantic — membership is what matters.
33
+ BASE_NAMESPACES: tuple[str, ...] = ("symbol", "endpoint", "module")
34
+
35
+ # Peripheral namespaces: real independent identity, deferred out of the v1 join graph.
36
+ PERIPHERAL_NAMESPACES: tuple[str, ...] = ("integration", "event")
37
+
38
+ # The single kind→namespace table. Derived from the real ``identity=`` of each kind
39
+ # emitted in ``steps_*.py`` (verified 2026-07-22). Adding a new emitted kind MUST add
40
+ # a row here (the join engine treats an unmapped kind as unclassifiable).
41
+ _KIND_TO_NAMESPACE: dict[str, str] = {
42
+ # ── symbol namespace (identity = FQN) — the join hub ────────────────────────
43
+ "dependent": "symbol", # impact reverse-reach
44
+ "caller": "symbol", # call graph, in-edge
45
+ "callee": "symbol", # call graph, out-edge
46
+ "execution_path": "symbol", # graph reachability
47
+ "type_dependent": "symbol", # type-usage reverse dep
48
+ "subsystem_member": "symbol", # module membership member
49
+ "coupling_hub": "symbol", # high-degree node
50
+ "transaction_boundary": "symbol", # facet: tx-role (identity = b.symbol)
51
+ "transaction_entrypoint": "symbol", # facet: tx-role
52
+ "propagation_point": "symbol", # facet: tx-role
53
+ "transaction_reaching": "symbol", # facet: reaches a tx (identity = b.symbol)
54
+ "security_finding": "symbol", # facet: security (identity = f.symbol)
55
+ # ── endpoint namespace (identity = route id / handler) ──────────────────────
56
+ "endpoint": "endpoint",
57
+ "endpoint_contract": "endpoint", # facet: public contract (identity = _endpoint_id)
58
+ "endpoint_security": "endpoint", # facet: security, keyed by endpoint
59
+ "validation_point": "endpoint", # facet: validation (identity = _route_id)
60
+ "validation_gap": "endpoint", # facet: validation
61
+ # ── module namespace (identity = path / label) ──────────────────────────────
62
+ "module": "module",
63
+ "subsystem": "module",
64
+ # ── peripheral (deferred, ADR-0007 §3) ──────────────────────────────────────
65
+ "integration": "integration",
66
+ "event_type": "event",
67
+ }
68
+
69
+
70
+ def namespace_of(kind: str) -> Optional[str]:
71
+ """Return the identity namespace of an entity ``kind``, or ``None`` if the kind
72
+ is not classified. ``None`` is the honest answer for an unknown kind — the join
73
+ engine must not treat an unclassifiable entity as belonging to any namespace."""
74
+ return _KIND_TO_NAMESPACE.get(kind)
75
+
76
+
77
+ def is_base_namespace(namespace: Optional[str]) -> bool:
78
+ """True iff ``namespace`` is one of the three base (join-graph) namespaces."""
79
+ return namespace in BASE_NAMESPACES
80
+
81
+
82
+ def same_namespace(kind_a: str, kind_b: str) -> bool:
83
+ """True iff both kinds are classified into the SAME namespace.
84
+
85
+ This is the join engine's discriminator (ADR-0007 §3): two entities in the same
86
+ namespace combine by a plain set operation; two in different namespaces require a
87
+ declared bridge (§4). An unknown kind (namespace ``None``) is never "the same" as
88
+ anything — an unclassifiable entity cannot be safely set-joined.
89
+ """
90
+ ns_a = namespace_of(kind_a)
91
+ ns_b = namespace_of(kind_b)
92
+ return ns_a is not None and ns_a == ns_b
93
+
94
+
95
+ # ── The bridge graph (ADR-0007 §4) ──────────────────────────────────────────────
96
+ # A bridge is a declared relation that maps an entity in one base namespace to
97
+ # entities in another, backed by a fact repository_ir already emits. It is the ONLY
98
+ # legal way to join across namespaces (INV-C3); the set of bridges is a single source
99
+ # of truth (INV-C5). Two of the four carry VARIABLE confidence (INV-C4) — and they
100
+ # are exactly the two joins ASK already measures (endpoint resolution: SIM-1/2;
101
+ # call-graph reach: P1-A/B); the rest are structural (always high).
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class Bridge:
106
+ """One declared cross-namespace relation.
107
+
108
+ ``from_ns``/``to_ns`` record the natural backing direction; a join may traverse a
109
+ bridge either way (see :func:`are_bridged`) — the confidence is symmetric.
110
+ ``variable_confidence`` marks a bridge whose per-edge confidence can be below
111
+ ``high`` and therefore participates in the §5 ``min`` as a real term; a structural
112
+ bridge is always ``high`` and can never lower a composed result.
113
+ """
114
+
115
+ from_ns: str
116
+ relation: str
117
+ to_ns: str
118
+ backing: str # the repository_ir fact that grounds the edge
119
+ variable_confidence: bool
120
+
121
+
122
+ BRIDGES: tuple[Bridge, ...] = (
123
+ Bridge("endpoint", "handled_by", "symbol", "route_surface[].handler", True),
124
+ Bridge("symbol", "reaches", "symbol", "reverse_call_graph", True),
125
+ Bridge("symbol", "enclosed_by", "symbol", "enclosing_class", False),
126
+ Bridge("module", "declares", "symbol", "subsystem_membership", False),
127
+ )
128
+
129
+
130
+ def bridges_between(from_ns: str, to_ns: str) -> tuple[Bridge, ...]:
131
+ """Declared bridges whose natural direction is exactly ``from_ns`` → ``to_ns``."""
132
+ return tuple(b for b in BRIDGES if b.from_ns == from_ns and b.to_ns == to_ns)
133
+
134
+
135
+ def are_bridged(ns_a: str, ns_b: str) -> bool:
136
+ """True iff a declared bridge connects the two namespaces in EITHER direction."""
137
+ return any(
138
+ {b.from_ns, b.to_ns} == {ns_a, ns_b} or (b.from_ns == b.to_ns == ns_a == ns_b)
139
+ for b in BRIDGES
140
+ )
141
+
142
+
143
+ def is_join_legal(kind_a: str, kind_b: str) -> bool:
144
+ """True iff two kinds may be composed: same namespace (a plain set op) OR a
145
+ declared bridge connects their namespaces (a bridged relate, ADR-0007 §4). An
146
+ unclassified kind (namespace ``None``) is never joinable — INV-C3 forbids an
147
+ implicit or guessed join."""
148
+ ns_a = namespace_of(kind_a)
149
+ ns_b = namespace_of(kind_b)
150
+ if ns_a is None or ns_b is None:
151
+ return False
152
+ return ns_a == ns_b or are_bridged(ns_a, ns_b)
153
+
154
+
155
+ # ── The confidence lattice (ADR-0007 §5 / §11.2) ────────────────────────────────
156
+ # Confidence is a TOTAL ORDER; the ONLY combinator is min (greatest-lower-bound);
157
+ # `unproven` is absorbing. No max / average / heuristic exists (INV-C9). A composed
158
+ # result can be no more certain than its weakest constituent OR its weakest bridge.
159
+
160
+ # Strongest → weakest. Index is the rank; a larger index is weaker.
161
+ CONFIDENCE_ORDER: tuple[str, ...] = ("high", "medium", "low", "unproven")
162
+ _CONF_RANK: dict[str, int] = {level: i for i, level in enumerate(CONFIDENCE_ORDER)}
163
+ _UNPROVEN_RANK: int = _CONF_RANK["unproven"]
164
+
165
+
166
+ def confidence_min(*values: Optional[str]) -> str:
167
+ """Combine confidences by greatest-lower-bound on the total order (ADR-0007 §5).
168
+
169
+ Returns the WEAKEST of ``values``. `unproven` is absorbing. An unrecognized or
170
+ ``None`` value degrades to `unproven` — the honest floor, never silently ignored.
171
+ With no values at all the result is `unproven`: a composition standing on nothing
172
+ cannot claim confidence (this deliberately breaks the empty-min=top identity in
173
+ favour of honesty).
174
+ """
175
+ worst_rank = -1
176
+ for v in values:
177
+ rank = _CONF_RANK.get(v if v is not None else "", _UNPROVEN_RANK)
178
+ if rank > worst_rank:
179
+ worst_rank = rank
180
+ if worst_rank < 0:
181
+ return "unproven"
182
+ return CONFIDENCE_ORDER[worst_rank]
183
+
184
+
185
+ # ── The typed algebra operators (ADR-0007 §2 / §11.3) ───────────────────────────
186
+ # A small, closed, TOTAL, DETERMINISTIC operator set over KnowledgeEntity sets:
187
+ # • combine — typed union / difference / intersection WITHIN one namespace (§3.1)
188
+ # • filter — a predicate over an entity's own attributes (§3.3, no join)
189
+ # • relate — a bridged join ACROSS namespaces, through a declared bridge (§4)
190
+ # They CONSUME facts and never produce them (INV-C1): `relate` takes a *bridge
191
+ # resolver* (a function that reports, per source entity, the target identities an IR
192
+ # fact already links it to, with that edge's confidence). The operators resolve
193
+ # nothing themselves — wiring a resolver to a real repository_ir fact is a separate
194
+ # build step. Confidence only ever attenuates (INV-C2) via `confidence_min` (§5),
195
+ # and includes the traversed bridge edge (INV-C4). An illegal request (cross-namespace
196
+ # combine, undeclared bridge, unclassifiable kind) RAISES `CompositionError` — never a
197
+ # silently-empty result (INV-F1-6).
198
+
199
+ # Per-entity confidence lives in `attributes` under this key. Absent → the caller's
200
+ # supplied constituent confidence, ultimately `unproven` (never silently `high`).
201
+ ATTR_CONFIDENCE = "confidence"
202
+ # The symbol an endpoint is handled_by, as a first-class attribute (ADR-0007 §11.1b) —
203
+ # so the `handled_by` bridge reads a field instead of parsing the entity identity.
204
+ ATTR_HANDLER_SYMBOL = "handler_symbol"
205
+ # Provenance breadcrumbs a `relate` writes onto each produced entity (§8.1: composition
206
+ # adds provenance edges, removes none).
207
+ ATTR_BRIDGED_VIA = "bridged_via"
208
+ ATTR_BRIDGE_TARGET = "bridge_target"
209
+
210
+ # A bridge resolver reports, for one source entity, the edges an IR fact links it to,
211
+ # as (target_identity, edge_confidence) pairs. Zero pairs = this source has no such
212
+ # edge (an honest empty, not an error).
213
+ BridgeResolver = Callable[[KnowledgeEntity], Iterable[tuple[str, str]]]
214
+
215
+
216
+ def entity_confidence(entity: KnowledgeEntity, default: str = "unproven") -> str:
217
+ """Read an entity's own confidence from its attributes.
218
+
219
+ An absent or unrecognized value degrades to ``default`` — a caller passes the
220
+ constituent surface's result-level confidence so a per-entity omission does not
221
+ silently read as ``high``. With no default supplied the floor is ``unproven``
222
+ (INV-C7: an input's uncertainty is never laundered away)."""
223
+ v = entity.attributes.get(ATTR_CONFIDENCE)
224
+ return v if v in _CONF_RANK else default
225
+
226
+
227
+ def _with_confidence(entity: KnowledgeEntity, confidence: str, **extra: Any) -> KnowledgeEntity:
228
+ """Return a copy of ``entity`` carrying ``confidence`` (and any provenance extras)
229
+ in its attributes. Frozen entity in, frozen entity out — never mutated in place."""
230
+ attrs = dict(entity.attributes)
231
+ attrs[ATTR_CONFIDENCE] = confidence
232
+ attrs.update(extra)
233
+ return replace(entity, attributes=attrs)
234
+
235
+
236
+ def _uniform_namespace(entities: Iterable[KnowledgeEntity], *, op: str) -> Optional[str]:
237
+ """The single base namespace shared by every entity, or ``None`` for the empty set.
238
+
239
+ Raises ``CompositionError`` if the entities span more than one namespace or include
240
+ an unclassifiable kind — a set operation is only defined within one namespace (§3.1;
241
+ INV-C10)."""
242
+ ns: Optional[str] = None
243
+ for e in entities:
244
+ e_ns = namespace_of(e.kind)
245
+ if e_ns is None:
246
+ raise CompositionError(
247
+ f"{op}: kind {e.kind!r} is unclassified (no namespace); "
248
+ "an unclassifiable entity cannot take part in a set operation"
249
+ )
250
+ if ns is None:
251
+ ns = e_ns
252
+ elif ns != e_ns:
253
+ raise CompositionError(
254
+ f"{op}: operands span namespaces {ns!r} and {e_ns!r}; a set operation "
255
+ "is defined only within one namespace — use `relate` across a bridge"
256
+ )
257
+ return ns
258
+
259
+
260
+ def combine(
261
+ left: Iterable[KnowledgeEntity],
262
+ right: Iterable[KnowledgeEntity],
263
+ op: str,
264
+ *,
265
+ default_confidence: str = "unproven",
266
+ ) -> tuple[KnowledgeEntity, ...]:
267
+ """Typed set operation on ONE namespace, keyed on ``identity`` (ADR-0007 §3.1).
268
+
269
+ ``op`` is ``"union"``, ``"intersection"`` or ``"difference"`` (left minus right).
270
+ Both operands MUST resolve to the same base namespace; a cross-namespace combine is
271
+ a category error and RAISES (INV-C3). Intersection attenuates: an identity present
272
+ in both carries ``min`` of the two copies' confidence (§5). Union/difference carry
273
+ each surviving entity's own confidence unchanged. Order is deterministic: left order
274
+ first, then (for union) the right-only entities in their own order."""
275
+ left = tuple(left)
276
+ right = tuple(right)
277
+ ns_left = _uniform_namespace(left, op=f"combine/{op}")
278
+ ns_right = _uniform_namespace(right, op=f"combine/{op}")
279
+ if ns_left is not None and ns_right is not None and ns_left != ns_right:
280
+ raise CompositionError(
281
+ f"combine/{op}: operands are in namespaces {ns_left!r} and {ns_right!r}; "
282
+ "a set operation is defined only within one namespace"
283
+ )
284
+ right_by_id = {e.identity: e for e in right}
285
+ left_ids = {e.identity for e in left}
286
+
287
+ if op == "union":
288
+ out = list(left)
289
+ out.extend(e for e in right if e.identity not in left_ids)
290
+ return tuple(out)
291
+ if op == "difference":
292
+ return tuple(e for e in left if e.identity not in right_by_id)
293
+ if op == "intersection":
294
+ out = []
295
+ for e in left:
296
+ match = right_by_id.get(e.identity)
297
+ if match is None:
298
+ continue
299
+ conf = confidence_min(
300
+ entity_confidence(e, default_confidence),
301
+ entity_confidence(match, default_confidence),
302
+ )
303
+ out.append(_with_confidence(e, conf))
304
+ return tuple(out)
305
+ raise CompositionError(
306
+ f"combine: unknown set operation {op!r} (expected union/intersection/difference)"
307
+ )
308
+
309
+
310
+ def filter_entities(
311
+ entities: Iterable[KnowledgeEntity],
312
+ predicate: Callable[[KnowledgeEntity], bool],
313
+ ) -> tuple[KnowledgeEntity, ...]:
314
+ """Keep entities whose ``predicate`` holds (ADR-0007 §3.3 — a facet is reached by a
315
+ predicate on its own base entity, never a join). Pure subtraction: confidence is
316
+ unchanged (filtering welds no views) and input order is preserved."""
317
+ return tuple(e for e in entities if predicate(e))
318
+
319
+
320
+ def attribute_predicate(name: str, op: str, value: Any) -> Callable[[KnowledgeEntity], bool]:
321
+ """Build a predicate over one attribute (ADR-0007 §11.5). Supports ``==`` / ``!=``
322
+ and the numeric comparators ``>`` / ``>=`` / ``<`` / ``<=``. A missing attribute
323
+ makes the predicate False (never a raise) — an entity lacking the tested attribute
324
+ simply does not match."""
325
+ comparators: dict[str, Callable[[Any, Any], bool]] = {
326
+ "==": lambda a, b: a == b,
327
+ "!=": lambda a, b: a != b,
328
+ ">": lambda a, b: a > b,
329
+ ">=": lambda a, b: a >= b,
330
+ "<": lambda a, b: a < b,
331
+ "<=": lambda a, b: a <= b,
332
+ }
333
+ cmp = comparators.get(op)
334
+ if cmp is None:
335
+ raise CompositionError(f"attribute_predicate: unknown operator {op!r}")
336
+
337
+ def _pred(entity: KnowledgeEntity) -> bool:
338
+ if name not in entity.attributes:
339
+ return False
340
+ try:
341
+ return cmp(entity.attributes[name], value)
342
+ except TypeError:
343
+ # incomparable types (e.g. numeric op on a string) → no match, never raise
344
+ return False
345
+
346
+ return _pred
347
+
348
+
349
+ def relate(
350
+ left: Iterable[KnowledgeEntity],
351
+ right: Iterable[KnowledgeEntity],
352
+ relation: str,
353
+ resolver: BridgeResolver,
354
+ *,
355
+ keep: str = "left",
356
+ default_confidence: str = "unproven",
357
+ ) -> tuple[KnowledgeEntity, ...]:
358
+ """Bridged join across two namespaces through a DECLARED bridge (ADR-0007 §4).
359
+
360
+ ``relation`` names a bridge in :data:`BRIDGES`; ``resolver`` reports, per left
361
+ entity, the ``(target_identity, edge_confidence)`` pairs an IR fact links it to.
362
+ A left entity is kept iff at least one resolved target is present in ``right``
363
+ (a semi-join). ``keep`` selects which side survives: ``"left"`` returns the matched
364
+ left entities, ``"right"`` the matched right entities.
365
+
366
+ Confidence attenuates per §5: each surviving entity carries
367
+ ``min(its own conf, the matched partner's conf, the bridge edge's conf)`` — the
368
+ bridge edge is a term in the minimum (INV-C4), so a strong pair welded through a
369
+ weak edge is weak. The bridge and matched target are recorded in the result's
370
+ attributes for provenance (§8.1).
371
+
372
+ RAISES ``CompositionError`` if ``relation`` is not a declared bridge or does not
373
+ connect the two operands' namespaces (INV-C3) — never a silently-empty result."""
374
+ left = tuple(left)
375
+ right = tuple(right)
376
+ # An undeclared bridge name is a wiring fault regardless of operand emptiness.
377
+ bridge = next((b for b in BRIDGES if b.relation == relation), None)
378
+ if bridge is None:
379
+ raise CompositionError(
380
+ f"relate: {relation!r} is not a declared bridge (INV-C3: no implicit joins)"
381
+ )
382
+ if keep not in ("left", "right"):
383
+ raise CompositionError(f"relate: keep must be 'left' or 'right', got {keep!r}")
384
+ ns_left = _uniform_namespace(left, op=f"relate/{relation}")
385
+ ns_right = _uniform_namespace(right, op=f"relate/{relation}")
386
+ if ns_left is None or ns_right is None:
387
+ # one side empty: nothing to relate, and no namespaces to validate against
388
+ return ()
389
+ if {bridge.from_ns, bridge.to_ns} != {ns_left, ns_right}:
390
+ raise CompositionError(
391
+ f"relate: bridge {relation!r} connects {bridge.from_ns!r}↔{bridge.to_ns!r}, "
392
+ f"not the operands' namespaces {ns_left!r}↔{ns_right!r}"
393
+ )
394
+
395
+ right_by_id = {e.identity: e for e in right}
396
+ out: list[KnowledgeEntity] = []
397
+ seen_right: set[str] = set()
398
+ for src in left:
399
+ for target_id, edge_conf in resolver(src):
400
+ match = right_by_id.get(target_id)
401
+ if match is None:
402
+ continue
403
+ conf = confidence_min(
404
+ entity_confidence(src, default_confidence),
405
+ entity_confidence(match, default_confidence),
406
+ edge_conf,
407
+ )
408
+ if keep == "left":
409
+ out.append(
410
+ _with_confidence(
411
+ src, conf,
412
+ **{ATTR_BRIDGED_VIA: relation, ATTR_BRIDGE_TARGET: target_id},
413
+ )
414
+ )
415
+ break # one qualifying edge is enough to keep the left entity
416
+ if match.identity not in seen_right:
417
+ seen_right.add(match.identity)
418
+ out.append(
419
+ _with_confidence(
420
+ match, conf,
421
+ **{ATTR_BRIDGED_VIA: relation, ATTR_BRIDGE_TARGET: src.identity},
422
+ )
423
+ )
424
+ return tuple(out)
425
+
426
+
427
+ # ── Concrete bridge resolvers — wiring the two variable bridges to IR facts ─────────
428
+ # `relate` consumes a BridgeResolver; these builders produce the concrete resolvers for
429
+ # the `handled_by` and `reaches` bridges (§4). They EXTRACT nothing — the facts (the
430
+ # handler symbol, the reach class) were produced by the endpoint / impact surfaces; a
431
+ # resolver only PROJECTS them into (target, edge_confidence) pairs (INV-C1). The edge
432
+ # confidence is exactly the uncertainty ASK already measures on these two joins
433
+ # (endpoint resolution SIM-1/2; call-graph reach P1-A/B) — §5's `min` consumes it, it
434
+ # does not originate it.
435
+
436
+ # Impact reach class → `reaches`-bridge edge confidence. A direct caller edge is high;
437
+ # an indirect (transitive / DI-mediated) edge is medium; anything else is unproven —
438
+ # never silently high (INV-C7).
439
+ _REACH_CONFIDENCE: dict[str, str] = {"direct": "high", "indirect": "medium"}
440
+
441
+
442
+ def reach_confidence(reach: Optional[str]) -> str:
443
+ """The `reaches`-bridge edge confidence implied by an impact reach class (P1-A/B).
444
+ An unknown/absent reach degrades to `unproven`, the honest floor."""
445
+ return _REACH_CONFIDENCE.get(reach or "", "unproven")
446
+
447
+
448
+ def endpoint_handler_symbol(endpoint: KnowledgeEntity) -> Optional[str]:
449
+ """The symbol an endpoint is `handled_by`. Reads the first-class ``handler_symbol``
450
+ attribute (ADR-0007 §11.1b); falls back to the identity only when the identity IS a
451
+ handler FQN (not the ``"METHOD path"`` / ``"/path"`` fallback form). Returns ``None``
452
+ for an endpoint with no known handler — an unresolved locator chain (SIM-1) has no
453
+ bridge edge, and that absence must not be guessed away."""
454
+ handler = endpoint.attributes.get(ATTR_HANDLER_SYMBOL)
455
+ if handler:
456
+ return handler
457
+ ident = endpoint.identity
458
+ if ident and " " not in ident and not ident.startswith("/"):
459
+ return ident
460
+ return None
461
+
462
+
463
+ def handled_by_resolver(
464
+ edge_confidence: str | Callable[[KnowledgeEntity], str] = "high",
465
+ ) -> BridgeResolver:
466
+ """Concrete resolver for the `handled_by` bridge (endpoint → handler symbol, §4).
467
+
468
+ Yields one ``(handler_symbol, edge_confidence)`` pair per endpoint that has a known
469
+ handler, and nothing for an unresolved endpoint. ``edge_confidence`` defaults to
470
+ ``high`` (a direct ``@…Mapping``); pass a callable to lower it per endpoint for a
471
+ weak resolution (JAX-RS locator chain = medium/low, SIM-1), read from the endpoint's
472
+ own attributes — the resolver infers no weakening on its own."""
473
+ def _resolve(endpoint: KnowledgeEntity) -> Iterable[tuple[str, str]]:
474
+ handler = endpoint_handler_symbol(endpoint)
475
+ if not handler:
476
+ return ()
477
+ conf = edge_confidence(endpoint) if callable(edge_confidence) else edge_confidence
478
+ return ((handler, conf),)
479
+
480
+ return _resolve
481
+
482
+
483
+ def stamp_reach_confidence(
484
+ entities: Iterable[KnowledgeEntity],
485
+ ) -> tuple[KnowledgeEntity, ...]:
486
+ """Return copies of impact ``dependent`` entities carrying a per-entity
487
+ ``confidence`` derived from their ``reach`` class (:func:`reach_confidence`), so the
488
+ `reaches` bridge's variable confidence participates as a real term in §5's ``min``.
489
+ An entity that already carries a confidence is left unchanged (never re-raised)."""
490
+ out: list[KnowledgeEntity] = []
491
+ for e in entities:
492
+ if e.attributes.get(ATTR_CONFIDENCE) in _CONF_RANK:
493
+ out.append(e)
494
+ else:
495
+ out.append(_with_confidence(e, reach_confidence(e.attributes.get("reach"))))
496
+ return tuple(out)
@@ -39,3 +39,10 @@ class StepExecutionError(RetrievalError):
39
39
  class StepContractError(RetrievalError):
40
40
  """A step required a shared-context slot that a prior step did not produce (a plan
41
41
  composed steps in an invalid order)."""
42
+
43
+
44
+ class CompositionError(RetrievalError):
45
+ """An illegal composition was requested (ADR-0007): a set operation across two
46
+ namespaces, a join over an undeclared bridge, or an unclassifiable kind. This is a
47
+ category error / plan-wiring fault — never a "no result" signal (INV-F1-6: a
48
+ composition fault is surfaced, not silently returned as an empty set)."""
@@ -83,7 +83,11 @@ def resolve_endpoint_contract(step: Step, ctx: "StepContext") -> None:
83
83
  identity=_endpoint_id(ep),
84
84
  kind="endpoint_contract",
85
85
  score=0.0,
86
- attributes={k: v for k, v in ep.items() if k not in ("source_file",)},
86
+ attributes={
87
+ **{k: v for k, v in ep.items() if k not in ("source_file",)},
88
+ # first-class handler symbol for the ADR-0007 handled_by bridge (§11.1b)
89
+ "handler_symbol": ep.get("handler", "") or "",
90
+ },
87
91
  )
88
92
  for ep in matched
89
93
  ],
@@ -131,6 +135,8 @@ def resolve_endpoint_security(step: Step, ctx: "StepContext") -> None:
131
135
  attributes={
132
136
  "method": ep.method or "",
133
137
  "path": ep.path or "",
138
+ # first-class handler symbol for the ADR-0007 handled_by bridge (§11.1b)
139
+ "handler_symbol": ep.handler_symbol or "",
134
140
  "security": _security_summary(ep.security),
135
141
  "findings": [f.pattern_id for f in findings if _finding_on_handler(f, {ep.handler_symbol})],
136
142
  },