sourcecode 3.0.0__tar.gz → 3.1.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 (182) hide show
  1. {sourcecode-3.0.0 → sourcecode-3.1.0}/CHANGELOG.md +85 -0
  2. {sourcecode-3.0.0 → sourcecode-3.1.0}/PKG-INFO +1 -1
  3. {sourcecode-3.0.0 → sourcecode-3.1.0}/pyproject.toml +1 -1
  4. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/__init__.py +1 -1
  5. sourcecode-3.1.0/src/sourcecode/chain_rules.py +289 -0
  6. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/cli.py +64 -6
  7. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/parse_cache.py +24 -5
  8. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/posture.py +373 -31
  9. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/repository_ir.py +75 -15
  10. sourcecode-3.1.0/src/sourcecode/spring_properties.py +217 -0
  11. {sourcecode-3.0.0 → sourcecode-3.1.0}/.github/workflows/build-windows.yml +0 -0
  12. {sourcecode-3.0.0 → sourcecode-3.1.0}/.gitignore +0 -0
  13. {sourcecode-3.0.0 → sourcecode-3.1.0}/.ruff.toml +0 -0
  14. {sourcecode-3.0.0 → sourcecode-3.1.0}/CONTRIBUTING.md +0 -0
  15. {sourcecode-3.0.0 → sourcecode-3.1.0}/LICENSE +0 -0
  16. {sourcecode-3.0.0 → sourcecode-3.1.0}/README.md +0 -0
  17. {sourcecode-3.0.0 → sourcecode-3.1.0}/SECURITY.md +0 -0
  18. {sourcecode-3.0.0 → sourcecode-3.1.0}/raw +0 -0
  19. {sourcecode-3.0.0 → sourcecode-3.1.0}/scripts/compare_integration_engines.py +0 -0
  20. {sourcecode-3.0.0 → sourcecode-3.1.0}/scripts/customer_smoke_test.sh +0 -0
  21. {sourcecode-3.0.0 → sourcecode-3.1.0}/scripts/generate_jdk_exports.py +0 -0
  22. {sourcecode-3.0.0 → sourcecode-3.1.0}/scripts/perf_harness.py +0 -0
  23. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/adaptive_scanner.py +0 -0
  24. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/archetype.py +0 -0
  25. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/architectural_baseline.py +0 -0
  26. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/architectural_delta.py +0 -0
  27. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/architecture_analyzer.py +0 -0
  28. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/architecture_summary.py +0 -0
  29. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/ast_extractor.py +0 -0
  30. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/cache.py +0 -0
  31. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/call_surface.py +0 -0
  32. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/caller_metrics.py +0 -0
  33. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/canonical_ir.py +0 -0
  34. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/change_plan.py +0 -0
  35. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/cir_graphs.py +0 -0
  36. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/classifier.py +0 -0
  37. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  38. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/compare.py +0 -0
  39. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/confidence_analyzer.py +0 -0
  40. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/constraint_diff.py +0 -0
  41. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/context_cache.py +0 -0
  42. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/context_graph.py +0 -0
  43. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/context_scorer.py +0 -0
  44. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/context_summarizer.py +0 -0
  45. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/contract_diff.py +0 -0
  46. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/contract_model.py +0 -0
  47. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/contract_pipeline.py +0 -0
  48. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/coverage_parser.py +0 -0
  49. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/dependency_analyzer.py +0 -0
  50. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/deployment_prefix.py +0 -0
  51. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/__init__.py +0 -0
  52. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/base.py +0 -0
  53. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  54. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/dart.py +0 -0
  55. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/dotnet.py +0 -0
  56. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/elixir.py +0 -0
  57. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/go.py +0 -0
  58. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/heuristic.py +0 -0
  59. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/hybrid.py +0 -0
  60. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/java.py +0 -0
  61. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  62. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/nodejs.py +0 -0
  63. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/parsers.py +0 -0
  64. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/php.py +0 -0
  65. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/project.py +0 -0
  66. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/python.py +0 -0
  67. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/ruby.py +0 -0
  68. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/rust.py +0 -0
  69. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/systems.py +0 -0
  70. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/terraform.py +0 -0
  71. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/detectors/tooling.py +0 -0
  72. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/doc_analyzer.py +0 -0
  73. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  74. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/endpoint_literals.py +0 -0
  75. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/endpoint_metrics.py +0 -0
  76. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  77. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/env_analyzer.py +0 -0
  78. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/envelope.py +0 -0
  79. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/error_schema.py +0 -0
  80. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/evidence_provider.py +0 -0
  81. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/explain.py +0 -0
  82. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/file_chunker.py +0 -0
  83. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/file_classifier.py +0 -0
  84. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/filter_surface.py +0 -0
  85. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/format_contract.py +0 -0
  86. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/fqn_utils.py +0 -0
  87. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/git_analyzer.py +0 -0
  88. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/graph_analyzer.py +0 -0
  89. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/graph_evidence.py +0 -0
  90. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/hibernate_strat.py +0 -0
  91. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/jdk_exports.py +0 -0
  92. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/license.py +0 -0
  93. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/__init__.py +0 -0
  94. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  95. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  96. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  97. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  98. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  99. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  100. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/registry.py +0 -0
  101. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/runner.py +0 -0
  102. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp/server.py +0 -0
  103. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/mcp_nudge.py +0 -0
  104. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/metrics_analyzer.py +0 -0
  105. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/migrate_check.py +0 -0
  106. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/openapi_surface.py +0 -0
  107. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/output_budget.py +0 -0
  108. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/path_filters.py +0 -0
  109. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/perf.py +0 -0
  110. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  111. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/pr_impact.py +0 -0
  112. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/prepare_context.py +0 -0
  113. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/progress.py +0 -0
  114. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/ranking_engine.py +0 -0
  115. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/readiness_timeline.py +0 -0
  116. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/reconciliation.py +0 -0
  117. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/redactor.py +0 -0
  118. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/relevance_scorer.py +0 -0
  119. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/rename_refactor.py +0 -0
  120. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/repo_classifier.py +0 -0
  121. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/__init__.py +0 -0
  122. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/context.py +0 -0
  123. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/errors.py +0 -0
  124. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/executor.py +0 -0
  125. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/planner.py +0 -0
  126. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/query.py +0 -0
  127. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/request.py +0 -0
  128. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/resolution.py +0 -0
  129. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/result.py +0 -0
  130. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/retriever.py +0 -0
  131. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/runtime.py +0 -0
  132. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/steps.py +0 -0
  133. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  134. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
  135. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
  136. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
  137. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
  138. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  139. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/ris.py +0 -0
  140. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/runtime_classifier.py +0 -0
  141. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/scanner.py +0 -0
  142. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/schema.py +0 -0
  143. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  144. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/security_config.py +0 -0
  145. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/security_posture.py +0 -0
  146. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/semantic_analyzer.py +0 -0
  147. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  148. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/semantic_integration_engine.py +0 -0
  149. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/semantic_services.py +0 -0
  150. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/serializer.py +0 -0
  151. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/spring_event_topology.py +0 -0
  152. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/spring_findings.py +0 -0
  153. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/spring_impact.py +0 -0
  154. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/spring_model.py +0 -0
  155. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/spring_profiles.py +0 -0
  156. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/spring_security_audit.py +0 -0
  157. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/spring_semantic.py +0 -0
  158. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  159. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/summarizer.py +0 -0
  160. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/telemetry/__init__.py +0 -0
  161. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/telemetry/config.py +0 -0
  162. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/telemetry/consent.py +0 -0
  163. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/telemetry/events.py +0 -0
  164. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/telemetry/filters.py +0 -0
  165. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/telemetry/transport.py +0 -0
  166. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/token_estimate.py +0 -0
  167. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/tree_utils.py +0 -0
  168. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/type_usage_surface.py +0 -0
  169. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/validation_inference.py +0 -0
  170. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/validation_surface.py +0 -0
  171. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/verify_edit.py +0 -0
  172. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/verify_repo.py +0 -0
  173. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/verify_rules.py +0 -0
  174. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/version_check.py +0 -0
  175. {sourcecode-3.0.0 → sourcecode-3.1.0}/src/sourcecode/workspace.py +0 -0
  176. {sourcecode-3.0.0 → sourcecode-3.1.0}/supabase/functions/README.md +0 -0
  177. {sourcecode-3.0.0 → sourcecode-3.1.0}/supabase/functions/get-license/index.ts +0 -0
  178. {sourcecode-3.0.0 → sourcecode-3.1.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  179. {sourcecode-3.0.0 → sourcecode-3.1.0}/supabase/functions/telemetry/index.ts +0 -0
  180. {sourcecode-3.0.0 → sourcecode-3.1.0}/supabase/sql/license_event_ordering.sql +0 -0
  181. {sourcecode-3.0.0 → sourcecode-3.1.0}/supabase/sql/licensing_schema.sql +0 -0
  182. {sourcecode-3.0.0 → sourcecode-3.1.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,90 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.1.0] — 2026-07-28
4
+
5
+ **Posture answers about requests, not only about beans.** Milestone M4 of the strategic
6
+ roadmap: the conditional-resolution kernel now decides `@ConditionalOnProperty` from the
7
+ repository's own configuration, and the request chain a profile set activates is read down
8
+ to per-endpoint access — with the rule that decided each endpoint cited by file and line.
9
+ Plus the field-defect queue: a fully-qualified annotation was invisible to the whole IR,
10
+ `@Bean` bodies were skipped by every body scan, the custom-gate label disagreed across
11
+ three surfaces, the parse cache served pre-fix parses, and `migrate-check` emitted 900KB
12
+ to stdout.
13
+
14
+ ### Added
15
+
16
+ - **`posture` resolves `@ConditionalOnProperty`** against the properties the repository's
17
+ own configuration sets under the same profile set — `application.yml` /
18
+ `application.properties`, the `application-{profile}` filename convention, and
19
+ multi-document `spring.config.activate.on-profile`. A profile-activated document
20
+ outranks an unconditional one. Previously every property condition was reported
21
+ unresolved, which on a property-driven repository was most of the answer.
22
+ - **`posture --property key=value`** (repeatable) states a deployment value the
23
+ repository's files do not carry; it outranks every file. Measured on jobrunr's Spring
24
+ Boot starter: `--property jobrunr.database.type=mem` turns 4 undecided storage
25
+ auto-configurations into 1 active and 3 inactive, each citing the value that decided it.
26
+ - **`properties` block in the payload** — which configuration files were read, how many
27
+ keys resolved, which keys nobody could decide, and which values the caller supplied.
28
+ - **`posture` resolves what the active filter chain permits, per endpoint.** The
29
+ authorization DSL of the chain configuration a profile set activates
30
+ (`requestMatchers` / `antMatchers` / `mvcMatchers` / `anyRequest` and the access method
31
+ that decides them) is read in declaration order, and each endpoint is resolved against
32
+ the first rule that matches it — `endpoints.effective_access`, with the deciding rule's
33
+ file and line cited for every stated endpoint. HTTP methods on a rule are honoured.
34
+ - **`posture --diff` states the endpoint-level security delta** (`endpoint_access`): the
35
+ requests two profile sets decide differently, with the count that require no
36
+ authentication on one side and do on the other. Bean-level movement was the previous
37
+ answer; this is the one a reviewer acts on.
38
+
39
+ ### Fixed
40
+
41
+ - **A fully-qualified annotation is the same annotation.** `@a.b.Foo` and `@Foo` are one
42
+ annotation to the compiler, but the IR stored the qualified token and matched a
43
+ simple-name vocabulary against it. A controller written as
44
+ `@org.springframework.web.bind.annotation.RestController` produced **zero endpoints**,
45
+ and a handler guarded with `@jakarta.annotation.security.RolesAllowed` read as carrying
46
+ no guard — which an endpoint-security contract then reported as a violation on a
47
+ guarded endpoint. Found by field-testing `require_endpoint_security` on
48
+ spring-petclinic. A/B on six battery repos: endpoint and security counts unchanged, so
49
+ this recovers a blind spot without widening any claim.
50
+ - **The parse cache keys on the analyzer, not only on the file.** It was described as
51
+ content-addressed and never stale, but the content it addressed was the source file
52
+ alone: after any change to symbol extraction, an updated install kept serving pre-fix
53
+ parses for every unchanged file, so a shipped fix read as inert until the cache was
54
+ cleared by hand — exactly the failure the whole-CIR cache was fixed for. The key now
55
+ carries `analyzer_fingerprint()`.
56
+ - **`migrate-check` guards its output size**, the way `repo-ir` already did. A default
57
+ run emitted 909KB on openmrs and 702KB on keycloak (~227K and ~175K tokens) straight to
58
+ stdout — past every context window it is meant to be pasted into, and only discovered
59
+ after the wait. Over ~50K estimated tokens it now stops with `OUTPUT_TOO_LARGE` naming
60
+ the four ways out (`--compact`, `--min-severity high`, `--output`, `--force`), and warns
61
+ from ~10K. Writing to a file is exempt; `--compact` on openmrs is 23KB.
62
+ - **The custom-gate label agrees across every surface.** A repository whose authorization
63
+ runs through its own annotation read `custom_annotation_based` from `endpoints`,
64
+ `unknown` from `spring-audit`, and `evidence_absent` from `retrieve endpoint-security`
65
+ — same repo, same evidence, three answers. The gate predicate was applied to the
66
+ `endpoints` surface only; the assembled surface every CIR consumer reads never saw it,
67
+ so those endpoints arrived carrying no security policy at all. It is now applied once,
68
+ at the seam all surfaces share, and the inferred gate is named in
69
+ `security_model_detail.custom_spi_detected`. Measured on the battery: no repository's
70
+ model changed — the declared-in-repo discriminator still rejects library annotations.
71
+ - **`@Bean` method bodies are scanned like any other body.** `@Bean` classifies a symbol
72
+ as `bean` rather than `method`, and every body-level scan — literal facts, body facts,
73
+ guard clauses, intra-class call edges — filtered on `method`. A configuration class's
74
+ own body was therefore invisible, which is precisely where datasource URLs, directory
75
+ URIs and client base paths are written. Measured on a clean cache: spring-petclinic
76
+ recovered 67 → 128 literal atoms across 23 → 36 owners; jobrunr gained 20 body-fact
77
+ owners. A `@Bean` method is also a call target now (a configuration wiring one bean
78
+ from another calls it by name).
79
+ - **Conditions are ANDed the way Spring ANDs them.** A bean excluded by its `@Profile` is
80
+ now `inactive` even when another condition on it is undecided; it was reported
81
+ unresolved, which hid a bean that provably does not register.
82
+
83
+ Unresolved stays first-class: a key no configuration source sets, a `${…}` placeholder,
84
+ two documents disagreeing at the same precedence, and arguments the parser does not read
85
+ are each reported unresolved with the key named — `matchIfMissing` is *reported*, never
86
+ applied, because a key absent from the repository is not a key absent from the deployment.
87
+
3
88
  ## [3.0.0] — 2026-07-28
4
89
 
5
90
  **One answer per question, a contract around every answer, and a gate that can be
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 3.0.0
3
+ Version: 3.1.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.0.0"
7
+ version = "3.1.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.0.0"
7
+ __version__ = "3.1.0"
@@ -0,0 +1,289 @@
1
+ """chain_rules.py — the path rules a security filter chain declares.
2
+
3
+ `posture` could say *which* chain configuration a profile activates, never what
4
+ that chain permits. "Under `prod` the security configuration is active" is a fact
5
+ about a bean; "under `prod`, `POST /admin/**` requires ROLE_ADMIN and `/health` is
6
+ open to anyone" is a fact about the request, and only the second one is what a
7
+ reviewer is asking.
8
+
9
+ This is an EXTRACTOR: it reads Java source for the published `HttpSecurity`
10
+ authorization DSL (`requestMatchers` / `antMatchers` / `mvcMatchers` /
11
+ `anyRequest`, followed by the access method that decides them) and returns the
12
+ rules **in declaration order**, because Spring applies the first one that matches
13
+ and any other order would answer a different question.
14
+
15
+ What it deliberately does not do: evaluate `access(...)` expressions, follow
16
+ matchers built by a helper method, or read matchers passed as constructed
17
+ objects. Those are recorded as rules whose decision is *not evaluated*, so the
18
+ caller reports the endpoint undecided rather than silently open.
19
+
20
+ VAI: only the published Spring Security DSL vocabulary appears. Path patterns and
21
+ role names are DATA — carried as evidence, never branched on.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+ from typing import Optional
29
+
30
+ #: Published matcher methods that introduce a rule.
31
+ _MATCHER_METHODS = ("requestMatchers", "antMatchers", "mvcMatchers", "regexMatchers")
32
+ _ANY_REQUEST = "anyRequest"
33
+
34
+ #: Published access methods → the decision they express. `access`/`hasIpAddress`
35
+ #: are listed so the rule is SEEN; their verdict is that they were not evaluated.
36
+ _ACCESS_DECISIONS = {
37
+ "permitAll": "permit_all",
38
+ "denyAll": "deny_all",
39
+ "authenticated": "authenticated",
40
+ "fullyAuthenticated": "authenticated",
41
+ "rememberMe": "authenticated",
42
+ "anonymous": "anonymous",
43
+ "hasRole": "role_required",
44
+ "hasAnyRole": "role_required",
45
+ "hasAuthority": "role_required",
46
+ "hasAnyAuthority": "role_required",
47
+ "access": "not_evaluated",
48
+ "hasIpAddress": "not_evaluated",
49
+ }
50
+
51
+ _QUOTED_RE = re.compile(r'"([^"]*)"')
52
+ _HTTP_METHOD_RE = re.compile(r"\bHttpMethod\s*\.\s*([A-Z]+)\b")
53
+ _MATCHER_CALL_RE = re.compile(
54
+ r"\.\s*(" + "|".join(_MATCHER_METHODS + (_ANY_REQUEST,)) + r")\s*\("
55
+ )
56
+ _ACCESS_CALL_RE = re.compile(
57
+ r"\s*\.\s*(" + "|".join(_ACCESS_DECISIONS) + r")\s*\("
58
+ )
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class AccessRule:
63
+ """One `matcher → access` pair, positioned in the order Spring applies it."""
64
+
65
+ patterns: "tuple[str, ...]" # empty → anyRequest (matches everything)
66
+ methods: "tuple[str, ...]" # empty → any HTTP method
67
+ decision: str
68
+ authorities: "tuple[str, ...]" = ()
69
+ source_file: str = ""
70
+ line: int = 0
71
+ order: int = 0
72
+ matcher: str = "" # the DSL method that declared it
73
+
74
+ @property
75
+ def is_any_request(self) -> bool:
76
+ """`anyRequest()` — the catch-all, and the only patternless rule that
77
+ Spring resolves. A matcher call with no readable pattern is a different
78
+ thing entirely: see `paths_unknown`."""
79
+ return self.matcher == _ANY_REQUEST
80
+
81
+ @property
82
+ def paths_unknown(self) -> bool:
83
+ """A matcher whose paths this parser could not read. It may cover any
84
+ request, so nothing after it can be concluded from the rules alone."""
85
+ return self.matcher != _ANY_REQUEST and not self.patterns
86
+
87
+ def to_dict(self) -> dict:
88
+ out: dict = {
89
+ "matcher": self.matcher,
90
+ "patterns": (
91
+ ["**"] if self.is_any_request
92
+ else (list(self.patterns) or ["(paths this parser could not read)"])
93
+ ),
94
+ "decision": self.decision,
95
+ "source_file": self.source_file,
96
+ "line": self.line,
97
+ }
98
+ if self.methods:
99
+ out["methods"] = list(self.methods)
100
+ if self.authorities:
101
+ out["authorities"] = list(self.authorities)
102
+ return out
103
+
104
+
105
+ def _balanced(text: str, open_index: int) -> "tuple[str, int]":
106
+ """(body, index after ')') for the parenthesis opening at `open_index`.
107
+
108
+ String literals are respected so a `)` inside a pattern does not close the
109
+ call early. Returns ("", -1) when the call is not balanced in this file.
110
+ """
111
+ depth = 0
112
+ i = open_index
113
+ in_string = False
114
+ escaped = False
115
+ while i < len(text):
116
+ char = text[i]
117
+ if in_string:
118
+ if escaped:
119
+ escaped = False
120
+ elif char == "\\":
121
+ escaped = True
122
+ elif char == '"':
123
+ in_string = False
124
+ elif char == '"':
125
+ in_string = True
126
+ elif char == "(":
127
+ depth += 1
128
+ elif char == ")":
129
+ depth -= 1
130
+ if depth == 0:
131
+ return text[open_index + 1:i], i + 1
132
+ i += 1
133
+ return "", -1
134
+
135
+
136
+ def rules_from_source(source: str, source_file: str = "") -> "list[AccessRule]":
137
+ """Every authorization rule in one file, in declaration order."""
138
+ rules: list[AccessRule] = []
139
+ position = 0
140
+ while True:
141
+ match = _MATCHER_CALL_RE.search(source, position)
142
+ if match is None:
143
+ break
144
+ matcher = match.group(1)
145
+ body, after = _balanced(source, match.end() - 1)
146
+ if after < 0:
147
+ break
148
+ access = _ACCESS_CALL_RE.match(source, after)
149
+ if access is None:
150
+ # A matcher whose access method is not adjacent (chained through a
151
+ # variable, or a form this parser does not read). Recorded so the
152
+ # caller knows the path is governed by something it could not read.
153
+ position = after
154
+ patterns = tuple(p for p in _QUOTED_RE.findall(body) if p)
155
+ if matcher != _ANY_REQUEST and not patterns:
156
+ continue
157
+ rules.append(AccessRule(
158
+ patterns=() if matcher == _ANY_REQUEST else patterns,
159
+ methods=tuple(sorted(set(_HTTP_METHOD_RE.findall(body)))),
160
+ decision="not_evaluated",
161
+ source_file=source_file,
162
+ line=source.count("\n", 0, match.start()) + 1,
163
+ order=len(rules),
164
+ matcher=matcher,
165
+ ))
166
+ continue
167
+ method_name = access.group(1)
168
+ access_body, access_end = _balanced(source, access.end() - 1)
169
+ position = access_end if access_end > 0 else after
170
+ patterns = tuple(p for p in _QUOTED_RE.findall(body) if p)
171
+ if matcher != _ANY_REQUEST and not patterns:
172
+ # `requestMatchers(new AntPathRequestMatcher(...))` and friends: the
173
+ # rule exists, its paths are not literals in this call.
174
+ rules.append(AccessRule(
175
+ patterns=(), methods=(), decision="not_evaluated",
176
+ source_file=source_file,
177
+ line=source.count("\n", 0, match.start()) + 1,
178
+ order=len(rules), matcher=matcher,
179
+ ))
180
+ continue
181
+ rules.append(AccessRule(
182
+ patterns=() if matcher == _ANY_REQUEST else patterns,
183
+ methods=tuple(sorted(set(_HTTP_METHOD_RE.findall(body)))),
184
+ decision=_ACCESS_DECISIONS[method_name],
185
+ authorities=tuple(_QUOTED_RE.findall(access_body)),
186
+ source_file=source_file,
187
+ line=source.count("\n", 0, match.start()) + 1,
188
+ order=len(rules),
189
+ matcher=matcher,
190
+ ))
191
+ return rules
192
+
193
+
194
+ def extract_chain_rules(
195
+ root: Path, java_files: "Optional[list[str]]" = None
196
+ ) -> "dict[str, list[AccessRule]]":
197
+ """`{repo-relative file: rules in declaration order}`. Never raises."""
198
+ root = Path(root)
199
+ if java_files is None:
200
+ try:
201
+ from sourcecode.repository_ir import find_java_files
202
+
203
+ java_files = find_java_files(root)
204
+ except Exception:
205
+ java_files = []
206
+ out: dict[str, list[AccessRule]] = {}
207
+ for rel in java_files or []:
208
+ try:
209
+ source = (root / rel).read_text(encoding="utf-8", errors="replace")
210
+ except OSError:
211
+ continue
212
+ # Cheap pre-check: the DSL always names one of these methods.
213
+ if not any(name in source for name in _MATCHER_METHODS + (_ANY_REQUEST,)):
214
+ continue
215
+ rules = rules_from_source(source, rel)
216
+ if rules:
217
+ out[rel] = rules
218
+ return out
219
+
220
+
221
+ def ant_matches(pattern: str, path: str) -> bool:
222
+ """Spring Ant path semantics: `?` one char, `*` one segment, `**` many.
223
+
224
+ A trailing `/**` also matches the base path itself, as Spring's matcher does
225
+ (`/admin/**` covers `/admin`).
226
+ """
227
+ if not pattern or not path:
228
+ return False
229
+ if pattern == path:
230
+ return True
231
+ regex = _ant_regex(pattern)
232
+ if regex.match(path):
233
+ return True
234
+ if pattern.endswith("/**"):
235
+ return _ant_regex(pattern[:-3] or "/").match(path) is not None
236
+ return False
237
+
238
+
239
+ _ANT_CACHE: "dict[str, re.Pattern]" = {}
240
+
241
+
242
+ def _ant_regex(pattern: str) -> "re.Pattern":
243
+ cached = _ANT_CACHE.get(pattern)
244
+ if cached is not None:
245
+ return cached
246
+ out: list[str] = ["^"]
247
+ i = 0
248
+ while i < len(pattern):
249
+ char = pattern[i]
250
+ if pattern.startswith("**", i):
251
+ out.append(".*")
252
+ i += 2
253
+ elif char == "*":
254
+ out.append("[^/]*")
255
+ i += 1
256
+ elif char == "?":
257
+ out.append("[^/]")
258
+ i += 1
259
+ else:
260
+ out.append(re.escape(char))
261
+ i += 1
262
+ out.append("$")
263
+ compiled = re.compile("".join(out))
264
+ _ANT_CACHE[pattern] = compiled
265
+ return compiled
266
+
267
+
268
+ def rule_matches(rule: AccessRule, method: str, path: str) -> bool:
269
+ """Does this rule govern `method path`? Method-less rules cover every method."""
270
+ if rule.methods and method and method.upper() not in rule.methods:
271
+ return False
272
+ if rule.is_any_request:
273
+ return True
274
+ return any(ant_matches(pattern, path) for pattern in rule.patterns)
275
+
276
+
277
+ def first_matching_rule(
278
+ rules: "list[AccessRule]", method: str, path: str
279
+ ) -> "Optional[AccessRule]":
280
+ """The rule Spring would apply: the first declared one that matches."""
281
+ for rule in rules:
282
+ if rule.paths_unknown:
283
+ # A rule whose paths could not be read may cover this request, and
284
+ # everything after it is only reachable if it does not. Stopping here
285
+ # is what keeps a later `permitAll` from being reported as the answer.
286
+ return rule
287
+ if rule_matches(rule, method, path):
288
+ return rule
289
+ return None
@@ -6112,6 +6112,13 @@ def posture_cmd(
6112
6112
  "--diff",
6113
6113
  help="Compare two profile sets: --diff dev:prod (each side comma-separated).",
6114
6114
  ),
6115
+ property_overrides: Optional[list[str]] = typer.Option(
6116
+ None,
6117
+ "--property",
6118
+ "-D",
6119
+ help="State a deployment property: --property jobrunr.dashboard.enabled=true "
6120
+ "(repeatable; outranks the repository's own configuration).",
6121
+ ),
6115
6122
  output_path: Optional[Path] = typer.Option(
6116
6123
  None, "--output", "-o", help="Write the report to a file instead of stdout."
6117
6124
  ),
@@ -6125,10 +6132,11 @@ def posture_cmd(
6125
6132
  which are not, and which conditions this analyzer could not evaluate.
6126
6133
 
6127
6134
  \b
6128
- EXPERIMENTAL: `@Profile` is resolved completely; every other condition
6129
- (`@ConditionalOnProperty` and the rest of the `@Conditional*` family) is
6130
- named and reported unresolved, never decided. On a repository that gates
6131
- beans on properties, most of the answer will be "unresolved".
6135
+ EXPERIMENTAL: `@Profile` is resolved completely, and `@ConditionalOnProperty`
6136
+ is resolved against the properties this repository's configuration sets under
6137
+ the same profile set (state the rest with --property). Every other condition
6138
+ (the remaining `@Conditional*` family) is named and reported unresolved,
6139
+ never decided.
6132
6140
 
6133
6141
  \b
6134
6142
  Unresolved is a first-class outcome. A bean whose registration depends on a
@@ -6140,12 +6148,34 @@ def posture_cmd(
6140
6148
  ask posture . --profile prod
6141
6149
  ask posture . --profile prod,metrics
6142
6150
  ask posture . --diff dev:prod
6151
+ ask posture . --profile prod --property app.security.enabled=true
6143
6152
  """
6144
6153
  from sourcecode.posture import build_posture, diff_posture
6145
6154
 
6146
6155
  def _parse_set(raw: str) -> set:
6147
6156
  return {p.strip() for p in raw.split(",") if p.strip()}
6148
6157
 
6158
+ overrides: dict = {}
6159
+ for item in property_overrides or []:
6160
+ if "=" not in item:
6161
+ _emit_error_json(
6162
+ INVALID_INPUT_CODE,
6163
+ f"--property needs key=value (got {item!r}).",
6164
+ hint="Example: --property jobrunr.dashboard.enabled=true",
6165
+ expected="key=value",
6166
+ )
6167
+ raise typer.Exit(code=1)
6168
+ key, _, value = item.partition("=")
6169
+ if not key.strip():
6170
+ _emit_error_json(
6171
+ INVALID_INPUT_CODE,
6172
+ f"--property needs a key before '=' (got {item!r}).",
6173
+ hint="Example: --property jobrunr.dashboard.enabled=true",
6174
+ expected="key=value",
6175
+ )
6176
+ raise typer.Exit(code=1)
6177
+ overrides[key.strip()] = value.strip()
6178
+
6149
6179
  if diff:
6150
6180
  if ":" not in diff:
6151
6181
  _emit_error_json(
@@ -6156,9 +6186,9 @@ def posture_cmd(
6156
6186
  )
6157
6187
  raise typer.Exit(code=1)
6158
6188
  left_raw, right_raw = diff.split(":", 1)
6159
- data = diff_posture(path, _parse_set(left_raw), _parse_set(right_raw))
6189
+ data = diff_posture(path, _parse_set(left_raw), _parse_set(right_raw), overrides)
6160
6190
  else:
6161
- data = build_posture(path, _parse_set(profile) if profile else set())
6191
+ data = build_posture(path, _parse_set(profile) if profile else set(), overrides)
6162
6192
 
6163
6193
  _emit_command_output(
6164
6194
  _serialize_dict(data, format),
@@ -6231,6 +6261,10 @@ def migrate_check_cmd(
6231
6261
  None, "--ref",
6232
6262
  help="Label for a --snapshot capture (e.g. a version or sprint tag).",
6233
6263
  ),
6264
+ force: bool = typer.Option(
6265
+ False, "--force",
6266
+ help="Emit to stdout even when the report exceeds the output-size guard.",
6267
+ ),
6234
6268
  ) -> None:
6235
6269
  """Spring Boot 2→3 migration readiness: detect javax→jakarta namespace blockers.
6236
6270
 
@@ -6349,6 +6383,30 @@ def migrate_check_cmd(
6349
6383
  _snap_path = write_snapshot(_snap, _history_dir)
6350
6384
  _snapshot_note = f"; snapshot → {_snap_path}"
6351
6385
 
6386
+ # Output-size guard, same contract as repo-ir. A default migrate-check run on
6387
+ # a large repository emits ~900KB (~227K tokens) — larger than any context
6388
+ # window it is meant to be pasted into, and the caller only learns that after
6389
+ # the wait. Writing to a file is exempt: the size is the point of a file.
6390
+ _mig_tokens_est = len(output.encode("utf-8")) // 4
6391
+ if output_path is None and not force and _mig_tokens_est > 50_000:
6392
+ _emit_error_json(
6393
+ "OUTPUT_TOO_LARGE",
6394
+ f"Estimated output is ~{_mig_tokens_est // 1000}K tokens — too large for "
6395
+ "most LLM context windows.",
6396
+ hint=(
6397
+ "Use --compact (bounded decision summary, same report), "
6398
+ "--min-severity high to drop low-severity findings, "
6399
+ "--output FILE to save to disk, or --force to bypass this guard."
6400
+ ),
6401
+ expected="Output under 50K estimated tokens.",
6402
+ )
6403
+ raise typer.Exit(1)
6404
+ if output_path is None and _mig_tokens_est > 10_000:
6405
+ sys.stderr.write(
6406
+ f"[migrate-check] ~{_mig_tokens_est // 1000}K tokens — "
6407
+ "use --compact or --output FILE for smaller output.\n"
6408
+ )
6409
+
6352
6410
  _total = report.summary.get("total_findings", 0)
6353
6411
  _emit_command_output(
6354
6412
  output, output_path, copy,
@@ -9,9 +9,12 @@ This cache stores the output of ``_extract_symbols`` — ``(package, imports, sy
9
9
  keyed by a content hash of ``(rel_path, capture_signature, source)``. Properties that
10
10
  make it safe:
11
11
 
12
- * **Content-addressed ⇒ never stale.** The key *is* the content; a changed file yields
13
- a different key and misses (re-parses), it never serves an old parse. There is no
14
- invalidation to get wrong unlike a signature-keyed cache.
12
+ * **Content-addressed ⇒ never stale.** The key *is* the content the file's bytes AND
13
+ the analyzer's own source (``analyzer_fingerprint``). A changed file or a changed
14
+ extractor yields a different key and misses (re-parses); it never serves an old
15
+ parse. There is no invalidation to get wrong — unlike a signature-keyed cache.
16
+ (The analyzer half was missing until a field test caught a shipped extractor fix
17
+ reading as inert: the parse was right in process and stale on disk.)
15
18
  * **Correctness never depends on it.** A miss, a corrupt entry, or an unwritable dir all
16
19
  degrade to "parse it" — the CIR is byte-identical whether the cache hit or missed
17
20
  (proven by the cir_hash-equivalence test, not assumed).
@@ -35,7 +38,10 @@ from sourcecode.context_cache import _atomic_write, _base_dir
35
38
  if TYPE_CHECKING:
36
39
  from sourcecode.repository_ir import SymbolRecord
37
40
 
38
- _CACHE_SUBDIR = "parse-cache-v1" # bump when the SymbolRecord shape or extractor changes
41
+ #: The key carries `analyzer_fingerprint()`, so an extractor change invalidates
42
+ #: entries by itself — this constant only needs a bump for something the source
43
+ #: hash cannot see (a changed dependency, a corrected on-disk format).
44
+ _CACHE_SUBDIR = "parse-cache-v1"
39
45
 
40
46
 
41
47
  def is_enabled() -> bool:
@@ -50,12 +56,25 @@ def _cache_root() -> Path:
50
56
 
51
57
  def file_key(rel_path: str, source: str, capture_sig: str) -> str:
52
58
  """Content hash of everything ``_extract_symbols`` depends on. Two identical inputs
53
- ⇒ same key (reuse); any change ⇒ different key (miss, re-parse)."""
59
+ ⇒ same key (reuse); any change ⇒ different key (miss, re-parse).
60
+
61
+ "Everything" includes the EXTRACTOR. Keying on the file's bytes alone made this
62
+ cache content-addressed with respect to the input and blind to the code reading
63
+ it: after a fix to symbol extraction, an updated install kept serving pre-fix
64
+ parses for every unchanged file, and the fix looked inert until the cache was
65
+ cleared by hand. That is the same failure the whole-CIR cache was fixed for; the
66
+ ``_CACHE_SUBDIR`` comment asking a human to bump a constant is exactly the
67
+ discipline that fails. The analyzer fingerprint makes it structural instead.
68
+ """
69
+ from sourcecode.cache import analyzer_fingerprint
70
+
54
71
  h = hashlib.sha256()
55
72
  h.update(rel_path.encode("utf-8", "replace"))
56
73
  h.update(b"\x00")
57
74
  h.update(capture_sig.encode("utf-8", "replace"))
58
75
  h.update(b"\x00")
76
+ h.update(analyzer_fingerprint().encode("utf-8", "replace"))
77
+ h.update(b"\x00")
59
78
  h.update(source.encode("utf-8", "replace"))
60
79
  return h.hexdigest()[:32]
61
80