sourcecode 3.1.1__tar.gz → 3.2.0__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 (184) hide show
  1. {sourcecode-3.1.1 → sourcecode-3.2.0}/CHANGELOG.md +146 -1
  2. {sourcecode-3.1.1 → sourcecode-3.2.0}/PKG-INFO +1 -1
  3. {sourcecode-3.1.1 → sourcecode-3.2.0}/pyproject.toml +1 -1
  4. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/cli.py +54 -9
  6. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/java.py +10 -5
  7. sourcecode-3.2.0/src/sourcecode/integration_coordinates.py +175 -0
  8. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/registry.py +3 -1
  9. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/server.py +5 -2
  10. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/repository_ir.py +25 -17
  11. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/steps_graph.py +10 -8
  12. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/steps_intf.py +23 -1
  13. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/security_posture.py +165 -19
  14. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/semantic_integration_engine.py +54 -2
  15. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_impact.py +124 -7
  16. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_semantic.py +56 -4
  17. {sourcecode-3.1.1 → sourcecode-3.2.0}/.github/workflows/build-windows.yml +0 -0
  18. {sourcecode-3.1.1 → sourcecode-3.2.0}/.gitignore +0 -0
  19. {sourcecode-3.1.1 → sourcecode-3.2.0}/.ruff.toml +0 -0
  20. {sourcecode-3.1.1 → sourcecode-3.2.0}/CONTRIBUTING.md +0 -0
  21. {sourcecode-3.1.1 → sourcecode-3.2.0}/LICENSE +0 -0
  22. {sourcecode-3.1.1 → sourcecode-3.2.0}/README.md +0 -0
  23. {sourcecode-3.1.1 → sourcecode-3.2.0}/SECURITY.md +0 -0
  24. {sourcecode-3.1.1 → sourcecode-3.2.0}/raw +0 -0
  25. {sourcecode-3.1.1 → sourcecode-3.2.0}/scripts/compare_integration_engines.py +0 -0
  26. {sourcecode-3.1.1 → sourcecode-3.2.0}/scripts/customer_smoke_test.sh +0 -0
  27. {sourcecode-3.1.1 → sourcecode-3.2.0}/scripts/generate_jdk_exports.py +0 -0
  28. {sourcecode-3.1.1 → sourcecode-3.2.0}/scripts/perf_harness.py +0 -0
  29. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/adaptive_scanner.py +0 -0
  30. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/archetype.py +0 -0
  31. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/architectural_baseline.py +0 -0
  32. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/architectural_delta.py +0 -0
  33. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/architecture_analyzer.py +0 -0
  34. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/architecture_summary.py +0 -0
  35. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/ast_extractor.py +0 -0
  36. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/cache.py +0 -0
  37. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/call_surface.py +0 -0
  38. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/caller_metrics.py +0 -0
  39. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/canonical_ir.py +0 -0
  40. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/chain_rules.py +0 -0
  41. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/change_plan.py +0 -0
  42. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/cir_graphs.py +0 -0
  43. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/classifier.py +0 -0
  44. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  45. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/compare.py +0 -0
  46. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/confidence_analyzer.py +0 -0
  47. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/constraint_diff.py +0 -0
  48. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/context_cache.py +0 -0
  49. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/context_graph.py +0 -0
  50. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/context_scorer.py +0 -0
  51. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/context_summarizer.py +0 -0
  52. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/contract_diff.py +0 -0
  53. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/contract_model.py +0 -0
  54. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/contract_pipeline.py +0 -0
  55. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/coverage_parser.py +0 -0
  56. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/dependency_analyzer.py +0 -0
  57. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/deployment_prefix.py +0 -0
  58. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/__init__.py +0 -0
  59. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/base.py +0 -0
  60. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  61. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/dart.py +0 -0
  62. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/dotnet.py +0 -0
  63. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/elixir.py +0 -0
  64. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/go.py +0 -0
  65. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/heuristic.py +0 -0
  66. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/hybrid.py +0 -0
  67. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  68. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/nodejs.py +0 -0
  69. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/parsers.py +0 -0
  70. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/php.py +0 -0
  71. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/project.py +0 -0
  72. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/python.py +0 -0
  73. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/ruby.py +0 -0
  74. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/rust.py +0 -0
  75. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/systems.py +0 -0
  76. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/terraform.py +0 -0
  77. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/detectors/tooling.py +0 -0
  78. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/doc_analyzer.py +0 -0
  79. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  80. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/endpoint_literals.py +0 -0
  81. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/endpoint_metrics.py +0 -0
  82. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  83. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/env_analyzer.py +0 -0
  84. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/envelope.py +0 -0
  85. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/error_schema.py +0 -0
  86. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/evidence_provider.py +0 -0
  87. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/explain.py +0 -0
  88. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/file_chunker.py +0 -0
  89. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/file_classifier.py +0 -0
  90. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/filter_surface.py +0 -0
  91. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/format_contract.py +0 -0
  92. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/fqn_utils.py +0 -0
  93. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/git_analyzer.py +0 -0
  94. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/graph_analyzer.py +0 -0
  95. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/graph_evidence.py +0 -0
  96. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/hibernate_strat.py +0 -0
  97. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/jdk_exports.py +0 -0
  98. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/license.py +0 -0
  99. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/__init__.py +0 -0
  100. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  101. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  102. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  103. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  104. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  105. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  106. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp/runner.py +0 -0
  107. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/mcp_nudge.py +0 -0
  108. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/metrics_analyzer.py +0 -0
  109. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/migrate_check.py +0 -0
  110. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/migration_blast.py +0 -0
  111. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/openapi_surface.py +0 -0
  112. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/output_budget.py +0 -0
  113. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/parse_cache.py +0 -0
  114. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/path_filters.py +0 -0
  115. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/perf.py +0 -0
  116. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/posture.py +0 -0
  117. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  118. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/pr_impact.py +0 -0
  119. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/prepare_context.py +0 -0
  120. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/progress.py +0 -0
  121. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/ranking_engine.py +0 -0
  122. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/readiness_timeline.py +0 -0
  123. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/reconciliation.py +0 -0
  124. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/redactor.py +0 -0
  125. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/relevance_scorer.py +0 -0
  126. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/rename_refactor.py +0 -0
  127. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/repo_classifier.py +0 -0
  128. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/__init__.py +0 -0
  129. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/context.py +0 -0
  130. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/errors.py +0 -0
  131. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/executor.py +0 -0
  132. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/planner.py +0 -0
  133. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/query.py +0 -0
  134. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/request.py +0 -0
  135. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/resolution.py +0 -0
  136. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/result.py +0 -0
  137. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/retriever.py +0 -0
  138. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/runtime.py +0 -0
  139. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/steps.py +0 -0
  140. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  141. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
  142. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
  143. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  144. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/ris.py +0 -0
  145. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/runtime_classifier.py +0 -0
  146. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/scanner.py +0 -0
  147. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/schema.py +0 -0
  148. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  149. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/security_config.py +0 -0
  150. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/semantic_analyzer.py +0 -0
  151. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  152. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/semantic_services.py +0 -0
  153. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/serializer.py +0 -0
  154. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_event_topology.py +0 -0
  155. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_findings.py +0 -0
  156. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_model.py +0 -0
  157. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_profiles.py +0 -0
  158. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_properties.py +0 -0
  159. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_security_audit.py +0 -0
  160. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  161. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/summarizer.py +0 -0
  162. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/telemetry/__init__.py +0 -0
  163. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/telemetry/config.py +0 -0
  164. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/telemetry/consent.py +0 -0
  165. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/telemetry/events.py +0 -0
  166. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/telemetry/filters.py +0 -0
  167. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/telemetry/transport.py +0 -0
  168. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/token_estimate.py +0 -0
  169. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/tree_utils.py +0 -0
  170. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/type_usage_surface.py +0 -0
  171. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/validation_inference.py +0 -0
  172. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/validation_surface.py +0 -0
  173. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/verify_edit.py +0 -0
  174. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/verify_repo.py +0 -0
  175. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/verify_rules.py +0 -0
  176. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/version_check.py +0 -0
  177. {sourcecode-3.1.1 → sourcecode-3.2.0}/src/sourcecode/workspace.py +0 -0
  178. {sourcecode-3.1.1 → sourcecode-3.2.0}/supabase/functions/README.md +0 -0
  179. {sourcecode-3.1.1 → sourcecode-3.2.0}/supabase/functions/get-license/index.ts +0 -0
  180. {sourcecode-3.1.1 → sourcecode-3.2.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  181. {sourcecode-3.1.1 → sourcecode-3.2.0}/supabase/functions/telemetry/index.ts +0 -0
  182. {sourcecode-3.1.1 → sourcecode-3.2.0}/supabase/sql/license_event_ordering.sql +0 -0
  183. {sourcecode-3.1.1 → sourcecode-3.2.0}/supabase/sql/licensing_schema.sql +0 -0
  184. {sourcecode-3.1.1 → sourcecode-3.2.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,9 +1,154 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [3.2.0] — 2026-07-28
4
+
5
+ **Answer coherence (M7, first four seams).** One fact, one authority (ADR-0008). Four
6
+ questions this tool answered twice — transaction boundaries, endpoint denominators, endpoint
7
+ security coverage, outbound integrations — now have a single authority each, and every
8
+ consumer binds to it instead of re-deriving.
9
+
10
+ **Upgrade note.** Two behaviours change for anyone gating on the output:
11
+
12
+ - `impact` / `impact-chain` / `plan` / `compare` now count transaction boundaries that were
13
+ structurally reported as `0` on every repository. Risk scores move **up**: on a
14
+ 200-class BroadleafCommerce sample, 42 classes go from 0 to ≥1 boundary and 11 change
15
+ `risk_level`, always upward. A pinned `--fail-on` threshold can now trip where it did not.
16
+ - `impact-chain.security_surfaces[]` and `endpoints_affected[]` gain `verdict` +
17
+ `confidence` and no longer let the declared annotation policy stand as a coverage answer.
18
+ `security_policy` is kept as a deprecated alias of `declared_policy` (two minor versions),
19
+ with its value unchanged.
20
+
21
+ New payload keys (`population`, `declared_clients`, three-part endpoint partitions) are
22
+ additive; the deprecated aliases listed below keep their previous values.
23
+
24
+ ### Fixed
25
+
26
+ - **`export --integrations` answered "0 LDAP" in documents whose stack block named Spring
27
+ LDAP.** Reported in two field evaluations two majors apart. Reproduced on a Boot app that
28
+ declares `spring-boot-starter-data-ldap` and configures `spring.ldap.urls` in YAML: the
29
+ directory is wired entirely by properties, so no LDAP client type is ever imported and the
30
+ construct scan is *right* to find nothing — but the report never mentioned the declaration
31
+ it could see, and the document as a whole said the system has no outbound integrations
32
+ while naming a directory client library elsewhere.
33
+ - New authority `integration_coordinates.declared_integration_coordinates(root)` answers
34
+ one question — which client libraries the build declares — from Maven/Gradle
35
+ coordinates with `file:line` evidence, excluding test/provided scope.
36
+ - `export --integrations` (and `export --c4`) now carry `declared_clients`
37
+ (`kind`, `coordinate`, `evidence`, `observed_in_source`), `declared_clients_count` and
38
+ `kinds_declared_not_observed`, and the coverage reason names the declared kind and its
39
+ coordinate. A declared coordinate is never counted as an integration: `count` and
40
+ `by_kind` are unchanged and still report observed constructs only.
41
+ - `retrieve integration-inventory` states the same declared-not-observed kinds as
42
+ observations.
43
+ - `detectors/java.py` reads its directory coordinates from that one list, so the stack
44
+ block and the integration report can never recognize different libraries for the same
45
+ external system (ADR-0008 R12).
46
+ - Measured: alfresco declares `cxf-rt-frontend-jaxws` with no SOAP construct in source —
47
+ previously invisible, now stated. Broadleaf's declared JMS/LDAP coordinates are all
48
+ marked `observed_in_source: true`; repositories that declare no client library keep an
49
+ empty block. The `export --integrations` and `export --c4` baseline cells drift by
50
+ these added keys, by design.
51
+
52
+ - **`impact-chain` called endpoints "none_detected" that the same run classified as guarded
53
+ by a custom authorization mechanism.** Measured on keycloak: `security_posture` reports
54
+ 406 endpoints `protected_custom` (Likely) — 324 of them carrying the declared policy
55
+ `none_detected`, because their gate is a bespoke annotation outside every published
56
+ vocabulary — while `impact-chain` printed `security_policy: none_detected` for those same
57
+ `endpoint_id`s. One document answered both "guarded by a custom mechanism" and "no
58
+ security detected" about `DELETE /admin/realms/{realm}`. The declared policy is a fact
59
+ about an annotation; it was never an answer about coverage.
60
+ - New authority `security_posture.endpoint_security_surface(cir)` returns the per-endpoint
61
+ answer keyed by `endpoint_id` (ADR-0008 R1). `impact-chain` binds to it and reuses the
62
+ projection the security audit already derived — no second derivation, memoized per CIR
63
+ so `pr-impact`'s per-class queries pay for it once.
64
+ - `security_surfaces[]` and `endpoints_affected[]` now carry `verdict` + `confidence`
65
+ (`security_verdict` / `security_confidence` on affected endpoints) from the four-way
66
+ posture vocabulary, plus `declared_policy` for the annotation actually found. On the
67
+ 28 endpoints reachable from `RealmAdminResource`, all 28 move `none_detected` →
68
+ `protected_custom` (Likely), matching `spring-audit` exactly.
69
+ - An endpoint the authority cannot speak about — or a repository with no posture
70
+ projection — is `coverage_unknown` with the reason stated, never a manufactured "none"
71
+ (ADR-0008 R9). On spring-petclinic all 17 endpoints read `coverage_unknown` in both
72
+ commands instead of `none_detected` in one and `coverage_unknown` in the other.
73
+ - `security_policy` is kept as a deprecated alias of `declared_policy` for two minor
74
+ versions; its value is unchanged.
75
+
76
+ - **The security posture reported three counts under names that all read as "handlers", and
77
+ they did not reconcile.** The field report put `custom_authorization_mechanisms` at
78
+ 713/936, `gate_coverage` at 698 and 2635, and a rollup of 939 + 2635 = 3574 — the only
79
+ figure matching `endpoints.total`. None was arithmetically wrong: they counted three
80
+ different populations, and no key said which. Reproduced exactly on keycloak, where the
81
+ old payload showed `gate_bearing: 337` beside `not_carrying_gate: 270` against a stated
82
+ total of 676 — a partition that visibly loses 69 endpoints, because the first number
83
+ counts handler *methods* and the second counts *endpoints*.
84
+ - One authority, `security_posture.endpoint_population`, now supplies every denominator,
85
+ and the posture payload carries a `population` block stating both units once
86
+ (ADR-0008 R1).
87
+ - `gate_coverage` gains an explicit three-part endpoint partition —
88
+ `endpoints_carrying_gate` + `endpoints_standard_guarded` +
89
+ `endpoints_not_carrying_gate` = `endpoints_total` — counted in the single loop that
90
+ visits each endpoint once, so the parts sum by construction. On keycloak:
91
+ 406 + 0 + 270 = 676. Handler-method figures keep their own names
92
+ (`handler_methods_total`, `handler_methods_carrying_gate`) and are never part of that
93
+ sum.
94
+ - Custom-gate detection states the unit its percentage used: the denominator is handler
95
+ method *declarations* (overloads share an FQN, so keycloak has 551 distinct handler
96
+ methods across 601 declarations), and `endpoints_total` / `handler_methods_total` are
97
+ printed beside it so no reader has to infer which one a coverage figure meant.
98
+ - `total_controller_handlers`, `gate_bearing`, `not_carrying_gate` and `handler_total`
99
+ are kept as deprecated aliases with their existing values for two minor versions. No
100
+ consumer breaks; the numbers they held did not change.
101
+
102
+ - **`impact` counted zero transaction boundaries on every repository — corrected, and the
103
+ two commands that answer this question now answer it once.** The field report was that
104
+ `impact` reported `transactional_boundaries_count: 0` for a symbol while `retrieve
105
+ transactions-reaching` reported 47 for the same one. The cause was worse than a
106
+ disagreement: the blast radius derived boundaries from a call-graph node
107
+ `role == "transaction_boundary"` that no producer ever assigns — 0 of 28 662 nodes carry
108
+ it on BroadleafCommerce — so the figure was **structurally always zero**, and the
109
+ `n_txn` term never contributed to `risk_score` or to the `n_txn >= 2` escalation rule
110
+ behind `risk_level`.
111
+ - Both consumers now ask one authority, `spring_semantic.boundaries_declared_within`
112
+ over the transaction-boundary index (ADR-0008 R1), which follows Spring's own
113
+ inheritance in both directions: a method carries its class's boundary, a class carries
114
+ the boundaries of the methods it declares, and asking about one method never sweeps in
115
+ a sibling's.
116
+ - Two named facts, one authority (ADR-0008 R3): `transactional_boundaries_touched` is
117
+ the target plus everything that calls it; the new `transactional_boundaries_reaching`
118
+ is the callers-only subset — what would run this change inside a transaction — and is
119
+ the number `retrieve transactions-reaching` reports, now by construction rather than
120
+ by coincidence.
121
+ - **Figures move, upward, because a real cost was being counted as nothing.** Measured
122
+ on BroadleafCommerce (161 declared boundaries): `CatalogServiceImpl` 0 → 10 boundaries
123
+ and `risk_score` 76.73 → 77.54; `OrderServiceImpl` 0 → 32 and 73.83 → 76.86;
124
+ `CustomerServiceImpl` 0 → 32 and 66.10 → 71.01. Across a 200-class sample, 42 classes
125
+ now report at least one boundary and 11 change `risk_level` — every one of them
126
+ upward. A pinned `--fail-on` threshold may therefore trip where it did not before;
127
+ that is the correction, not a regression.
4
128
 
5
129
  ### Added
6
130
 
131
+ - **ADR-0008 "Answer Coherence" — one fact, one authority (normative, no behaviour change
132
+ yet).** Two independent field evaluations, on releases two majors apart, scored the
133
+ product identically and named the same ceiling: not a missing capability, but the same
134
+ question answered twice with different numbers (`impact` reporting 0 transactional
135
+ boundaries where `retrieve` reports 47; `compare` ranking on caller arrays truncated at
136
+ 30 while the exact magnitudes sit in `stats`; `has_tests` contradicting `analysis_gaps`
137
+ inside one document). The ADR fixes the rule the codebase had never stated: a fact has
138
+ exactly one authority, consumers transport it, quantities never come from the length of a
139
+ rendered collection, and a surface that cannot resolve emits `unknown` — never `0`. It
140
+ also changes the unit of remediation from *defect* to *fact*: a coherence fix is complete
141
+ only when the authority is corrected, every consumer is rebound, and a cross-command
142
+ parity assertion is added to the release battery.
143
+ - `docs/architecture/adr/0008-answer-coherence.md` — the rule, with the code-verified seam
144
+ inventory in Appendix A.
145
+ - `docs/DEFECT-LEDGER.md` — every field defect, its class, and the release that closed it,
146
+ published rather than tracked privately.
147
+ - `.planning/FIELD-EVAL-SYNTHESIS-2026-07-28.md` and
148
+ `.planning/ROADMAP-STRATEGIC-2026-07-28.md` — the evidence and the resulting direction
149
+ (M7 Coherence → M8 Payload economy → M9 Core & surface → M10 Enrichment interop →
150
+ M11 Compliance substrate).
151
+
7
152
  - **`migrate-check --blast-radius` states the regression scope behind each blocker.** The
8
153
  finding list says what must change; a migration plan is built around what has to be
9
154
  re-tested when that change lands. Each affected product file now resolves to the types
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 3.1.1
3
+ Version: 3.2.0
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 = "3.1.1"
7
+ version = "3.2.0"
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__ = "3.1.1"
7
+ __version__ = "3.2.0"
@@ -4084,7 +4084,11 @@ def impact_cmd(
4084
4084
  - direct_callers — classes that directly call or depend on the target
4085
4085
  - indirect_callers — transitive callers (BFS, bounded by --depth)
4086
4086
  - endpoints_affected — HTTP endpoints that transitively depend on the target
4087
- - transactional_boundaries_touched — @Transactional classes in the call chain
4087
+ - transactional_boundaries_touched — @Transactional declarations on the target
4088
+ and on everything that calls it
4089
+ - transactional_boundaries_reaching — the callers-only subset: what would run
4090
+ this change inside a transaction (the figure
4091
+ `ask retrieve transactions-reaching` reports)
4088
4092
  - risk_score / risk_level — quantified change risk
4089
4093
 
4090
4094
  \b
@@ -4714,7 +4718,10 @@ def export_cmd(
4714
4718
  ),
4715
4719
  integrations: bool = typer.Option(
4716
4720
  False, "--integrations",
4717
- help="Detect outbound integrations (HTTP/LDAP/JMS clients) with file:line evidence.",
4721
+ help=(
4722
+ "Detect outbound integrations (HTTP/LDAP/JMS clients) with file:line "
4723
+ "evidence, plus the client libraries the build declares."
4724
+ ),
4718
4725
  ),
4719
4726
  c4: bool = typer.Option(
4720
4727
  False, "--c4",
@@ -4742,6 +4749,11 @@ def export_cmd(
4742
4749
  rolled up from class-level relation edges.
4743
4750
  --integrations Outbound integrations (RestTemplate/WebClient/Feign/LDAP/JMS)
4744
4751
  with file:line evidence — external-system dependency arrows.
4752
+ Also lists the client libraries the BUILD declares
4753
+ (declared_clients) and which of those no source construct
4754
+ uses — a config-wired client has no construct to find, so a
4755
+ count of 0 is never left standing alone against a client
4756
+ library named in the stack block.
4745
4757
  --c4 Unified architecture document mapped onto the open C4 model
4746
4758
  (context/containers/components/code) + an API surface and a
4747
4759
  per-directory content-hash manifest for incremental consumers.
@@ -4792,9 +4804,12 @@ def export_cmd(
4792
4804
  graph = ir.get("graph", {})
4793
4805
  _ep_data = extract_java_endpoints(root)
4794
4806
  endpoints = _ep_data.get("endpoints", [])
4807
+ from sourcecode.integration_coordinates import declared_integration_coordinates
4795
4808
  _integrations = SemanticIntegrationEngine(
4796
4809
  ContextGraph.build(file_list, root)
4797
- ).as_report(len(file_list))
4810
+ ).as_report(
4811
+ len(file_list), declared=declared_integration_coordinates(root),
4812
+ )
4798
4813
  data = _build_c4_export(
4799
4814
  root,
4800
4815
  file_list,
@@ -4831,9 +4846,12 @@ def export_cmd(
4831
4846
  if integrations:
4832
4847
  from sourcecode.context_graph import ContextGraph
4833
4848
  from sourcecode.semantic_integration_engine import SemanticIntegrationEngine
4849
+ from sourcecode.integration_coordinates import declared_integration_coordinates
4834
4850
  data["integrations"] = SemanticIntegrationEngine(
4835
4851
  ContextGraph.build(file_list, root)
4836
- ).as_report(len(file_list))
4852
+ ).as_report(
4853
+ len(file_list), declared=declared_integration_coordinates(root),
4854
+ )
4837
4855
 
4838
4856
  _prog.finish()
4839
4857
  output = _serialize_dict(data, format)
@@ -5684,19 +5702,35 @@ def _render_gate_coverage_section(result: "SpringAuditResult") -> list[str]: #
5684
5702
  gc = (result.security_posture or {}).get("gate_coverage")
5685
5703
  if not gc:
5686
5704
  return []
5687
- not_covered = gc.get("not_carrying_gate", 0)
5705
+ not_covered = gc.get("endpoints_not_carrying_gate", gc.get("not_carrying_gate", 0))
5688
5706
  gates = ", ".join(f"`{g}`" for g in gc.get("gate_annotations", [])) or "the detected gate"
5689
- total = gc.get("total_controller_handlers", 0)
5707
+ # One unit, named: these are endpoints, not handler methods. The two counts differ
5708
+ # (one method can serve several mappings) and must never be summed together.
5709
+ total = gc.get("endpoints_total", gc.get("total_controller_handlers", 0))
5690
5710
  lines: list[str] = ["", "---", ""]
5691
5711
  if not_covered == 0:
5692
- lines.append(f" **Gate coverage** — all {total} controller handlers carry {gates}.")
5712
+ _gated = gc.get("endpoints_carrying_gate", total)
5713
+ _std = gc.get("endpoints_standard_guarded", 0)
5714
+ if _std:
5715
+ lines.append(
5716
+ f"✅ **Gate coverage** — all {total} endpoints are guarded: {_gated} carry "
5717
+ f"{gates}, {_std} rely on a standard guard."
5718
+ )
5719
+ else:
5720
+ lines.append(f"✅ **Gate coverage** — all {total} endpoints carry {gates}.")
5693
5721
  return lines
5694
5722
 
5695
5723
  covered = gc.get("possibly_filter_covered", 0)
5724
+ _standard = gc.get("endpoints_standard_guarded", 0)
5696
5725
  lines.append(
5697
- f"🔓 **Gate coverage** — {not_covered} of {total} controller handlers do not carry "
5726
+ f"🔓 **Gate coverage** — {not_covered} of {total} endpoints do not carry "
5698
5727
  f"{gates}."
5699
5728
  )
5729
+ if _standard:
5730
+ lines.append(
5731
+ f"_{gc.get('endpoints_carrying_gate', 0)} carry the gate, {_standard} rely on a "
5732
+ f"standard guard, {not_covered} on neither — the three sum to {total}._"
5733
+ )
5700
5734
  if gc.get("reconstructed_filter_patterns"):
5701
5735
  lines.append(
5702
5736
  f"_{covered} of those match a reconstructed servlet filter pattern "
@@ -6369,6 +6403,16 @@ def migrate_check_cmd(
6369
6403
  ask migrate-check . --output migration.json
6370
6404
  ask migrate-check . --snapshot --ref sprint-12 persist a readiness point
6371
6405
  ask migrate-check . --trend days-remaining over time
6406
+ ask migrate-check . --blast-radius endpoints behind each blocker
6407
+
6408
+ \b
6409
+ Blast radius (--blast-radius):
6410
+ Ranks the affected product files by the HTTP endpoints whose call path runs
6411
+ through them — the re-test plan, ordered by regression scope. Reach is never
6412
+ build breakage: a javax→jakarta finding fails compilation whether or not an
6413
+ endpoint reaches it. A file whose types the IR could not resolve is reported
6414
+ unresolved (reach unknown, never zero), and a repository with no endpoints in
6415
+ the IR gets a null share with the reason stated instead of a 0.0.
6372
6416
 
6373
6417
  \b
6374
6418
  Readiness time series:
@@ -6587,7 +6631,8 @@ def impact_chain_cmd(
6587
6631
  - indirect_callers — transitive callers (BFS up to --depth hops)
6588
6632
  - endpoints_affected — HTTP endpoints reachable through the call chain
6589
6633
  - transaction_boundary — @Transactional semantics on the target (if any)
6590
- - security_surfaces — per-endpoint security policy + SEC findings
6634
+ - security_surfaces — per-endpoint security verdict + confidence
6635
+ (security_posture authority) + declared policy + SEC findings
6591
6636
  - impact_findings — TX/SEC audit findings touching the call chain
6592
6637
  - risk_level — critical | high | medium | low
6593
6638
 
@@ -15,6 +15,7 @@ from sourcecode.detectors.parsers import (
15
15
  substitute_maven_properties as _resolve_maven_property,
16
16
  unique_strings,
17
17
  )
18
+ from sourcecode.integration_coordinates import coordinates_for_kind
18
19
  from sourcecode.schema import FrameworkDetection
19
20
  from sourcecode.tree_utils import flatten_file_tree
20
21
 
@@ -488,11 +489,15 @@ class JavaDetector(AbstractDetector):
488
489
  if "spring-boot-starter-data-jpa" in text or "spring-data-jpa" in text:
489
490
  frameworks.append(FrameworkDetection(name="Spring Data JPA", source=source))
490
491
  # The Boot starter names neither of the module coordinates, so a Boot app
491
- # wiring LDAP the standard way declared no LDAP framework at all.
492
- if (
493
- "spring-ldap-core" in text
494
- or "spring-security-ldap" in text
495
- or "spring-boot-starter-data-ldap" in text
492
+ # wiring LDAP the standard way declared no LDAP framework at all. The coordinate
493
+ # list lives in `integration_coordinates` — the same set `export --integrations`
494
+ # reads, so this stack block and that report can never recognize different
495
+ # libraries for the same external system (ADR-0008 R12).
496
+ # Only the Spring-family coordinates may carry the "Spring LDAP" label — the
497
+ # authority also knows non-Spring directory clients, and naming the wrong library
498
+ # would trade one incoherence for another.
499
+ if any(
500
+ c in text for c in coordinates_for_kind("ldap") if c.startswith("spring-")
496
501
  ):
497
502
  frameworks.append(FrameworkDetection(name="Spring LDAP", source=source))
498
503
  if "spring-aspects" in text or "spring-aop" in text:
@@ -0,0 +1,175 @@
1
+ """integration_coordinates.py — the declared-client signal for outbound integrations.
2
+
3
+ THE authority for one question: *which outbound-integration client libraries does this
4
+ repository's build declare?* (ADR-0008 R1). It is a different question from "which client
5
+ constructs does the source contain" — that one belongs to
6
+ :mod:`sourcecode.semantic_integration_engine` — and the two answers only conflict when a
7
+ document reports one and stays silent about the other.
8
+
9
+ That is exactly what the field reported, twice, two majors apart: `export --integrations`
10
+ answered `count: 0` on a repository whose stack block listed **Spring LDAP**. Reproduced on
11
+ a Boot app that declares `spring-boot-starter-data-ldap` and configures
12
+ `spring.ldap.urls` in YAML: the directory is wired entirely by properties, so no LDAP
13
+ client type is ever imported and the construct scan is *right* to find nothing — but the
14
+ document as a whole then said the system has no outbound integrations while its own stack
15
+ block said it talks to a directory.
16
+
17
+ A declared coordinate is **evidence of a declared client, never an observed integration**
18
+ (the same discipline that stopped a `org.mybatis` coordinate from being reported as MyBatis
19
+ usage in `detectors/java.py`). This module answers what the build declares; the engine
20
+ answers what the code does; the payload states both and says which is which.
21
+
22
+ VAI (see `security_posture` §0.5): the table below lists **published open-source artifact
23
+ coordinates** — the identity of a library, matched literally, the same basis as the engine's
24
+ published client-type table. No proprietary, client or convention name appears here, and no
25
+ predicate branches on a repository's own naming.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import re
30
+ from dataclasses import dataclass
31
+ from pathlib import Path
32
+ from typing import Iterable, Optional
33
+
34
+ # Build-manifest artifact coordinate → the outbound-integration kind it declares.
35
+ # Kinds are exactly the engine's kinds, so declared and observed answers are comparable.
36
+ # Only coordinates whose sole purpose is to talk to an external system are listed: a
37
+ # driver-or-anything coordinate that a repository might carry for unrelated reasons has no
38
+ # place here, because a declaration nobody can act on is noise, not evidence.
39
+ _COORDINATE_KIND: "dict[str, str]" = {
40
+ # Directory (LDAP)
41
+ "spring-ldap-core": "ldap",
42
+ "spring-security-ldap": "ldap",
43
+ "spring-boot-starter-data-ldap": "ldap",
44
+ "spring-boot-starter-ldap": "ldap",
45
+ "unboundid-ldapsdk": "ldap",
46
+ # Kafka
47
+ "spring-kafka": "kafka",
48
+ "kafka-clients": "kafka",
49
+ "spring-cloud-stream-binder-kafka": "kafka",
50
+ # RabbitMQ / AMQP
51
+ "spring-boot-starter-amqp": "rabbitmq",
52
+ "spring-rabbit": "rabbitmq",
53
+ "amqp-client": "rabbitmq",
54
+ # JMS brokers
55
+ "spring-boot-starter-activemq": "jms",
56
+ "spring-boot-starter-artemis": "jms",
57
+ "activemq-client": "jms",
58
+ "activemq-broker": "jms",
59
+ "artemis-jms-client": "jms",
60
+ "spring-jms": "jms",
61
+ # Redis
62
+ "spring-boot-starter-data-redis": "redis",
63
+ "spring-data-redis": "redis",
64
+ "lettuce-core": "redis",
65
+ "jedis": "redis",
66
+ # Elasticsearch
67
+ "spring-boot-starter-data-elasticsearch": "elasticsearch",
68
+ "elasticsearch-rest-high-level-client": "elasticsearch",
69
+ "elasticsearch-java": "elasticsearch",
70
+ # Mail (SMTP)
71
+ "spring-boot-starter-mail": "smtp",
72
+ # Declarative HTTP clients
73
+ "spring-cloud-starter-openfeign": "http",
74
+ "feign-core": "http",
75
+ "okhttp": "http",
76
+ # SOAP
77
+ "spring-ws-core": "soap",
78
+ "spring-boot-starter-web-services": "soap",
79
+ "cxf-rt-frontend-jaxws": "soap",
80
+ }
81
+
82
+ # Build manifests scanned. Text-level, like the framework-coordinate scan in
83
+ # `detectors/java.py` — a build file is configuration, not source.
84
+ _MANIFEST_NAMES: frozenset[str] = frozenset({
85
+ "pom.xml", "build.gradle", "build.gradle.kts",
86
+ })
87
+
88
+ # Directories that never hold a production build manifest.
89
+ _SKIP_DIRS: frozenset[str] = frozenset({
90
+ ".git", ".idea", "target", "build", "node_modules", ".sourcecode-cache",
91
+ ".gradle", "out", "dist",
92
+ })
93
+
94
+ # A repository with thousands of modules is real (OFBiz, Broadleaf); an unbounded walk is
95
+ # not. The cap is stated in the payload when it bites, never applied silently.
96
+ _MAX_MANIFESTS = 400
97
+
98
+ _TEST_SCOPE_WINDOW = 4 # lines after a Maven coordinate that may carry its <scope>
99
+ _TEST_SCOPE = re.compile(r"<scope>\s*(test|provided)\s*</scope>", re.IGNORECASE)
100
+ _GRADLE_TEST_CONF = re.compile(r"^\s*(test|androidTest)[A-Za-z]*\s*[(\s'\"]", re.IGNORECASE)
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class DeclaredClient:
105
+ """A client library the build declares, with the line that declares it."""
106
+
107
+ kind: str
108
+ coordinate: str
109
+ evidence: str # "relpath:line" — same shape as an Integration's evidence
110
+
111
+ def to_dict(self) -> dict:
112
+ return {"kind": self.kind, "coordinate": self.coordinate, "evidence": self.evidence}
113
+
114
+
115
+ def coordinates_for_kind(kind: str) -> tuple[str, ...]:
116
+ """Published coordinates that declare ``kind``. One home for the list, so a consumer
117
+ that recognizes a library recognizes the same set this authority does."""
118
+ return tuple(sorted(c for c, k in _COORDINATE_KIND.items() if k == kind))
119
+
120
+
121
+ def _manifest_paths(root: Path) -> list[Path]:
122
+ found: list[Path] = []
123
+ for name in sorted(_MANIFEST_NAMES):
124
+ for path in sorted(root.rglob(name)):
125
+ if len(found) >= _MAX_MANIFESTS:
126
+ return found
127
+ if any(part in _SKIP_DIRS for part in path.parts):
128
+ continue
129
+ if path.is_file():
130
+ found.append(path)
131
+ return found
132
+
133
+
134
+ def _is_test_scoped(lines: list[str], idx: int, manifest: str) -> bool:
135
+ """True when the declaration is a test/provided dependency — a build-time coordinate
136
+ is not a statement that the running system talks to that external party."""
137
+ if manifest == "pom.xml":
138
+ for line in lines[idx: idx + _TEST_SCOPE_WINDOW + 1]:
139
+ if _TEST_SCOPE.search(line):
140
+ return True
141
+ return False
142
+ return bool(_GRADLE_TEST_CONF.match(lines[idx]))
143
+
144
+
145
+ def declared_integration_coordinates(
146
+ root: Optional[Path], *, manifests: Optional[Iterable[Path]] = None,
147
+ ) -> list[DeclaredClient]:
148
+ """Client libraries the build declares, deterministically ordered, deduplicated by
149
+ (kind, coordinate). Never raises: an unreadable manifest contributes nothing.
150
+ """
151
+ if root is None:
152
+ return []
153
+ root = Path(root)
154
+ paths = list(manifests) if manifests is not None else _manifest_paths(root)
155
+
156
+ seen: dict[tuple[str, str], DeclaredClient] = {}
157
+ for path in paths:
158
+ try:
159
+ lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
160
+ except OSError:
161
+ continue
162
+ try:
163
+ rel = str(path.relative_to(root)).replace("\\", "/")
164
+ except ValueError:
165
+ rel = path.name
166
+ for idx, line in enumerate(lines):
167
+ for coordinate, kind in _COORDINATE_KIND.items():
168
+ if coordinate not in line:
169
+ continue
170
+ if _is_test_scoped(lines, idx, path.name):
171
+ continue
172
+ key = (kind, coordinate)
173
+ if key not in seen:
174
+ seen[key] = DeclaredClient(kind, coordinate, f"{rel}:{idx + 1}")
175
+ return [seen[k] for k in sorted(seen)]
@@ -781,7 +781,9 @@ Blast-radius analysis: who calls a class and what breaks if it changes? Java onl
781
781
 
782
782
  Maps to: ask impact <target> <repo_path> [--depth <depth>]
783
783
  Returns: direct_callers, indirect_callers, endpoints_affected,
784
- transactional_boundaries_touched, risk_score, risk_level, stats.
784
+ transactional_boundaries_touched (target + callers),
785
+ transactional_boundaries_reaching (callers only), risk_score,
786
+ risk_level, stats.
785
787
 
786
788
  Use this when:
787
789
  - Planning a refactor: understand the full call chain before changing a class
@@ -890,7 +890,8 @@ def get_impact_chain(repo_path: str = ".", symbol: str = "", depth: int = 4) ->
890
890
  Returns: ImpactChainResult with schema_version, symbol, resolution,
891
891
  direct_callers, indirect_callers, endpoints_affected,
892
892
  transaction_boundary (propagation/isolation/read_only),
893
- security_surfaces (per-endpoint policy + finding IDs),
893
+ security_surfaces (per-endpoint verdict/confidence from the
894
+ security_posture authority + declared policy + finding IDs),
894
895
  impact_findings (TX-001..005 + SEC-001..003 findings in call chain),
895
896
  analysis_warnings, risk_level, confidence, metadata.
896
897
 
@@ -1320,7 +1321,9 @@ def get_impact_context(repo_path: str = ".", target: str = "", depth: int = 4) -
1320
1321
 
1321
1322
  Maps to: ask impact <target> <repo_path> [--depth <depth>]
1322
1323
  Returns: direct_callers, indirect_callers, endpoints_affected,
1323
- transactional_boundaries_touched, risk_score, risk_level, stats.
1324
+ transactional_boundaries_touched (target + callers),
1325
+ transactional_boundaries_reaching (callers only), risk_score,
1326
+ risk_level, stats.
1324
1327
 
1325
1328
  Use this when:
1326
1329
  - Planning a refactor: understand the full call chain before changing a class
@@ -26,6 +26,10 @@ from typing import Any, Iterable, Optional
26
26
  from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
27
27
  from sourcecode.endpoint_metrics import ENDPOINT_SURFACE_RECONCILIATION
28
28
  from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
29
+ from sourcecode.spring_semantic import (
30
+ boundaries_declared_within as _boundaries_declared_within,
31
+ build_tx_index_from_ir as _build_tx_index_from_ir,
32
+ )
29
33
  from sourcecode.path_filters import (
30
34
  is_test_path as _is_test_path,
31
35
  is_test_fixture_module_path as _is_test_fixture_module_path,
@@ -6803,6 +6807,7 @@ def compute_blast_radius(
6803
6807
  "security_surface_affected": [],
6804
6808
  "cross_module_impact": [],
6805
6809
  "transactional_boundaries_touched": [],
6810
+ "transactional_boundaries_reaching": [],
6806
6811
  "risk_score": 0.0,
6807
6812
  "risk_level": "unknown",
6808
6813
  "confidence_score": 0.0,
@@ -6828,6 +6833,7 @@ def compute_blast_radius(
6828
6833
  "security_surface_affected": [],
6829
6834
  "cross_module_impact": [],
6830
6835
  "transactional_boundaries_touched": [],
6836
+ "transactional_boundaries_reaching": [],
6831
6837
  "risk_score": 0.0,
6832
6838
  "risk_level": "unknown",
6833
6839
  "confidence_score": 0.0,
@@ -7134,23 +7140,19 @@ def compute_blast_radius(
7134
7140
  })
7135
7141
  cross_module_impact = cross_module_impact[:10]
7136
7142
 
7137
- # ── 7. Transactional boundaries touched ───────────────────────────────────
7138
- txn_nodes: list[str] = []
7139
- for node_dict in graph_nodes:
7140
- fqn = node_dict.get("fqn") or ""
7141
- role = node_dict.get("role") or ""
7142
- symbol_kind = node_dict.get("symbol_kind") or ""
7143
- if role == "transaction_boundary" or "Transactional" in (node_dict.get("canonical_name") or ""):
7144
- if fqn in affected_classes or _enclosing_class(fqn) in affected_classes:
7145
- txn_nodes.append(fqn)
7146
- elif symbol_kind == "method" and fqn in affected_classes:
7147
- enc = _enclosing_class(fqn)
7148
- for n2 in graph_nodes:
7149
- if n2.get("fqn") == enc and n2.get("role") == "transaction_boundary":
7150
- txn_nodes.append(fqn)
7151
- break
7152
-
7153
- txn_nodes = sorted(set(txn_nodes))
7143
+ # ── 7. Transactional boundaries ───────────────────────────────────────────
7144
+ # Both figures come from the one transaction-boundary authority (ADR-0008 R1);
7145
+ # they differ only in which symbols are asked about:
7146
+ # touched — the target and everything that calls it (what this change sits in)
7147
+ # reaching — the callers alone (what would run this change inside a transaction)
7148
+ # This used to be derived here from a graph node `role == "transaction_boundary"`
7149
+ # that no producer ever assigns, so `impact` reported 0 boundaries on repos where
7150
+ # `retrieve transactions-reaching` reported dozens, and `n_txn` never contributed
7151
+ # to the risk score or its escalation rules.
7152
+ _tx_index = _build_tx_index_from_ir(ir)
7153
+ _caller_symbols = set(direct_callers) | set(indirect_callers)
7154
+ txn_nodes = sorted({b.symbol for b in _boundaries_declared_within(_tx_index, affected_classes)})
7155
+ txn_reaching = sorted({b.symbol for b in _boundaries_declared_within(_tx_index, _caller_symbols)})
7154
7156
 
7155
7157
  # ── 8. Risk score ─────────────────────────────────────────────────────────
7156
7158
  n_direct = len(direct_callers)
@@ -7288,6 +7290,11 @@ def compute_blast_radius(
7288
7290
  "security_surface_affected": security_surface_affected,
7289
7291
  "cross_module_impact": cross_module_impact,
7290
7292
  "transactional_boundaries_touched": txn_nodes,
7293
+ # The callers-only subset: the boundaries that would run this change inside a
7294
+ # transaction. Same authority, narrower question — and the figure
7295
+ # `retrieve transactions-reaching` answers, so the two commands agree by
7296
+ # construction rather than by coincidence.
7297
+ "transactional_boundaries_reaching": txn_reaching,
7291
7298
  "depth_reached": _effective_depth, # actual BFS depth used, not the requested max
7292
7299
  "bfs_truncated": _bfs_truncated,
7293
7300
  "stats": {
@@ -7297,6 +7304,7 @@ def compute_blast_radius(
7297
7304
  "indirect_callers_sampled": _indirect_sampled,
7298
7305
  "endpoints_affected_count": n_ep,
7299
7306
  "transactional_boundaries_count": n_txn,
7307
+ "transactional_boundaries_reaching_count": len(txn_reaching),
7300
7308
  "mappers_affected_count": n_mappers,
7301
7309
  "modules_affected_count": n_modules,
7302
7310
  "security_surface_count": n_sec,