sourcecode 4.17.0__tar.gz → 4.18.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 (232) hide show
  1. {sourcecode-4.17.0 → sourcecode-4.18.0}/CHANGELOG.md +44 -0
  2. {sourcecode-4.17.0 → sourcecode-4.18.0}/PKG-INFO +3 -3
  3. {sourcecode-4.17.0 → sourcecode-4.18.0}/README.md +2 -2
  4. {sourcecode-4.17.0 → sourcecode-4.18.0}/pyproject.toml +1 -1
  5. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/cli.py +18 -13
  7. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/context_cache.py +94 -7
  8. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/identity_fallback.py +6 -12
  9. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/posture.py +9 -14
  10. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/risk.py +29 -6
  11. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/security_config_scan.py +4 -8
  12. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/source_text.py +65 -0
  13. {sourcecode-4.17.0 → sourcecode-4.18.0}/.github/workflows/build-windows.yml +0 -0
  14. {sourcecode-4.17.0 → sourcecode-4.18.0}/.github/workflows/perf-gate.yml +0 -0
  15. {sourcecode-4.17.0 → sourcecode-4.18.0}/.gitignore +0 -0
  16. {sourcecode-4.17.0 → sourcecode-4.18.0}/.ruff.toml +0 -0
  17. {sourcecode-4.17.0 → sourcecode-4.18.0}/CLAUDE.md +0 -0
  18. {sourcecode-4.17.0 → sourcecode-4.18.0}/CONTRIBUTING.md +0 -0
  19. {sourcecode-4.17.0 → sourcecode-4.18.0}/LICENSE +0 -0
  20. {sourcecode-4.17.0 → sourcecode-4.18.0}/SECURITY.md +0 -0
  21. {sourcecode-4.17.0 → sourcecode-4.18.0}/raw +0 -0
  22. {sourcecode-4.17.0 → sourcecode-4.18.0}/scripts/compare_integration_engines.py +0 -0
  23. {sourcecode-4.17.0 → sourcecode-4.18.0}/scripts/customer_smoke_test.sh +0 -0
  24. {sourcecode-4.17.0 → sourcecode-4.18.0}/scripts/generate_jdk_exports.py +0 -0
  25. {sourcecode-4.17.0 → sourcecode-4.18.0}/scripts/perf_gate.py +0 -0
  26. {sourcecode-4.17.0 → sourcecode-4.18.0}/scripts/perf_harness.py +0 -0
  27. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/adaptive_scanner.py +0 -0
  28. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/archetype.py +0 -0
  29. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/architectural_baseline.py +0 -0
  30. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/architectural_delta.py +0 -0
  31. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/architecture_analyzer.py +0 -0
  32. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/architecture_summary.py +0 -0
  33. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/ast_extractor.py +0 -0
  34. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/audit_report.py +0 -0
  35. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/baseline_autocapture.py +0 -0
  36. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/cache.py +0 -0
  37. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/cache_model.py +0 -0
  38. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/call_surface.py +0 -0
  39. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/caller_metrics.py +0 -0
  40. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/caller_reach.py +0 -0
  41. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/canonical_ir.py +0 -0
  42. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/chain_rules.py +0 -0
  43. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/change_plan.py +0 -0
  44. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/cir_graphs.py +0 -0
  45. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/classifier.py +0 -0
  46. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/client_calls.py +0 -0
  47. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  48. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/compare.py +0 -0
  49. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/confidence_analyzer.py +0 -0
  50. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/constraint_diff.py +0 -0
  51. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/container_wiring.py +0 -0
  52. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/context_graph.py +0 -0
  53. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/context_scorer.py +0 -0
  54. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/context_summarizer.py +0 -0
  55. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/contract_diff.py +0 -0
  56. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/contract_init.py +0 -0
  57. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/contract_model.py +0 -0
  58. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/contract_pipeline.py +0 -0
  59. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/coverage_parser.py +0 -0
  60. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/data_exposure.py +0 -0
  61. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/data_labels.py +0 -0
  62. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/declarations.py +0 -0
  63. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/defect_identity.py +0 -0
  64. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/degradation.py +0 -0
  65. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/dependency_analyzer.py +0 -0
  66. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/deployment_prefix.py +0 -0
  67. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detach.py +0 -0
  68. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/__init__.py +0 -0
  69. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/base.py +0 -0
  70. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  71. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/dart.py +0 -0
  72. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/dotnet.py +0 -0
  73. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/elixir.py +0 -0
  74. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/go.py +0 -0
  75. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/heuristic.py +0 -0
  76. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/hybrid.py +0 -0
  77. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/java.py +0 -0
  78. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  79. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/nodejs.py +0 -0
  80. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/parsers.py +0 -0
  81. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/php.py +0 -0
  82. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/project.py +0 -0
  83. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/python.py +0 -0
  84. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/ruby.py +0 -0
  85. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/rust.py +0 -0
  86. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/systems.py +0 -0
  87. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/terraform.py +0 -0
  88. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/detectors/tooling.py +0 -0
  89. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/doc_analyzer.py +0 -0
  90. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  91. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/endpoint_literals.py +0 -0
  92. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/endpoint_metrics.py +0 -0
  93. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  94. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/env_analyzer.py +0 -0
  95. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/envelope.py +0 -0
  96. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/environment_resolution.py +0 -0
  97. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/error_schema.py +0 -0
  98. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/evidence_provider.py +0 -0
  99. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/execution_plan.py +0 -0
  100. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/explain.py +0 -0
  101. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/facts/__init__.py +0 -0
  102. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/facts/registry.json +0 -0
  103. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/file_chunker.py +0 -0
  104. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/file_classifier.py +0 -0
  105. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/filter_surface.py +0 -0
  106. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/format_contract.py +0 -0
  107. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/fqn_utils.py +0 -0
  108. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/git_analyzer.py +0 -0
  109. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/git_checkout.py +0 -0
  110. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/graph_analyzer.py +0 -0
  111. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/graph_evidence.py +0 -0
  112. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/hibernate_strat.py +0 -0
  113. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/integration_coordinates.py +0 -0
  114. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/jdk_exports.py +0 -0
  115. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/license.py +0 -0
  116. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/__init__.py +0 -0
  117. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/dependency_import.py +0 -0
  118. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  119. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  120. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  121. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  122. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  123. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  124. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/registry.py +0 -0
  125. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/runner.py +0 -0
  126. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp/server.py +0 -0
  127. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/mcp_nudge.py +0 -0
  128. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/metrics_analyzer.py +0 -0
  129. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/migrate_check.py +0 -0
  130. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/migration_blast.py +0 -0
  131. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/non_coverage.py +0 -0
  132. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/openapi_surface.py +0 -0
  133. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/openrewrite_recipe.py +0 -0
  134. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/output_budget.py +0 -0
  135. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/output_encoding.py +0 -0
  136. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/parallel.py +0 -0
  137. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/parse_cache.py +0 -0
  138. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/partial_contract.py +0 -0
  139. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/path_admission.py +0 -0
  140. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/path_filters.py +0 -0
  141. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/perf.py +0 -0
  142. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/phased_run.py +0 -0
  143. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/pipe_contract.py +0 -0
  144. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  145. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/pr_impact.py +0 -0
  146. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/prepare_context.py +0 -0
  147. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/progress.py +0 -0
  148. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/provenance.py +0 -0
  149. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/ranking_engine.py +0 -0
  150. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/readiness_timeline.py +0 -0
  151. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/readonly.py +0 -0
  152. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/reconciliation.py +0 -0
  153. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/redactor.py +0 -0
  154. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/reference_facts.py +0 -0
  155. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/release_info.py +0 -0
  156. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/relevance_scorer.py +0 -0
  157. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/remedies.py +0 -0
  158. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/rename_refactor.py +0 -0
  159. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/repo_classifier.py +0 -0
  160. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/repository_ir.py +0 -0
  161. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/__init__.py +0 -0
  162. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/context.py +0 -0
  163. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/errors.py +0 -0
  164. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/executor.py +0 -0
  165. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/planner.py +0 -0
  166. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/query.py +0 -0
  167. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/request.py +0 -0
  168. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/resolution.py +0 -0
  169. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/result.py +0 -0
  170. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/retriever.py +0 -0
  171. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/runtime.py +0 -0
  172. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/steps.py +0 -0
  173. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  174. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
  175. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
  176. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
  177. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
  178. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  179. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/ris.py +0 -0
  180. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/rule_catalog.py +0 -0
  181. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/rule_pass.py +0 -0
  182. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/runs.py +0 -0
  183. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/runtime_classifier.py +0 -0
  184. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/sarif.py +0 -0
  185. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/scanner.py +0 -0
  186. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/schema.py +0 -0
  187. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  188. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/security_config.py +0 -0
  189. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/security_posture.py +0 -0
  190. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/semantic_analyzer.py +0 -0
  191. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  192. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/semantic_integration_engine.py +0 -0
  193. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/semantic_services.py +0 -0
  194. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/serializer.py +0 -0
  195. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/servlet_surface.py +0 -0
  196. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_event_topology.py +0 -0
  197. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_findings.py +0 -0
  198. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_impact.py +0 -0
  199. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_model.py +0 -0
  200. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_profiles.py +0 -0
  201. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_properties.py +0 -0
  202. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_security_audit.py +0 -0
  203. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_semantic.py +0 -0
  204. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  205. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/summarizer.py +0 -0
  206. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/target_admission.py +0 -0
  207. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/telemetry/__init__.py +0 -0
  208. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/telemetry/config.py +0 -0
  209. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/telemetry/consent.py +0 -0
  210. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/telemetry/events.py +0 -0
  211. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/telemetry/filters.py +0 -0
  212. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/telemetry/transport.py +0 -0
  213. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/test_gap_ranking.py +0 -0
  214. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/test_sources.py +0 -0
  215. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/text_input.py +0 -0
  216. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/token_estimate.py +0 -0
  217. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/tree_utils.py +0 -0
  218. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/type_usage_surface.py +0 -0
  219. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/validation_inference.py +0 -0
  220. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/validation_surface.py +0 -0
  221. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/verify_edit.py +0 -0
  222. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/verify_repo.py +0 -0
  223. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/verify_rules.py +0 -0
  224. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/version_check.py +0 -0
  225. {sourcecode-4.17.0 → sourcecode-4.18.0}/src/sourcecode/workspace.py +0 -0
  226. {sourcecode-4.17.0 → sourcecode-4.18.0}/supabase/functions/README.md +0 -0
  227. {sourcecode-4.17.0 → sourcecode-4.18.0}/supabase/functions/get-license/index.ts +0 -0
  228. {sourcecode-4.17.0 → sourcecode-4.18.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  229. {sourcecode-4.17.0 → sourcecode-4.18.0}/supabase/functions/telemetry/index.ts +0 -0
  230. {sourcecode-4.17.0 → sourcecode-4.18.0}/supabase/sql/license_event_ordering.sql +0 -0
  231. {sourcecode-4.17.0 → sourcecode-4.18.0}/supabase/sql/licensing_schema.sql +0 -0
  232. {sourcecode-4.17.0 → sourcecode-4.18.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -2,6 +2,50 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.18.0] — 2026-08-10
6
+
7
+ **A field battery over the test repositories, run against 4.17.0 and aimed first
8
+ at where the time goes.** Three defects, all found by measuring rather than by
9
+ reading: one answer that was silently about the wrong repository, and two scans
10
+ that could not finish. `risk` on BroadleafCommerce (2 985 files) did not return in
11
+ twenty minutes and now returns a complete answer in **68 s**.
12
+
13
+ **Fixed in this release:**
14
+
15
+ - **A run bounded to a subdirectory answered for the whole repository (C3-94).**
16
+ The shared Java parse was keyed on the worktree alone, so a run over a
17
+ subdirectory stored its subset under the repository-wide key and the next
18
+ repository-wide command read it. Measured on openmrs-core: `explain WebUtil
19
+ <repo>/web` left a **42-file / 382-symbol** CIR where the repository has 861 and
20
+ 11 605, and `impact-chain` then answered from it — reporting 861 files beside
21
+ 382 symbols, and dropping from `confidence: high` to `low` with no input change.
22
+ The analysed set is now part of the key; a repository-wide run owns the shared
23
+ entry and a bounded run keys on its own, so bounded runs are **cached** instead
24
+ of being exempt. The rule lived in one call site and four others wrote the key
25
+ unguarded; it now lives in the cache, behind one door.
26
+ - **Naming the enclosing type backtracked exponentially (C3-95).** Two scanners
27
+ carried the same ambiguous pattern — a whitespace branch inside a starred group,
28
+ followed by another starred whitespace group. Measured: **13 ms** for 200 leading
29
+ spaces on one line, 1,17 s for 800, **10,1 s for 1 600**. Now linear: 0,06 ms at
30
+ 1 600 and 0,76 ms at 20 000. `scan_security_configuration` on BroadleafCommerce
31
+ **3,34 s → 1,53 s**, same findings.
32
+ - **`risk` could not finish, inside a regex no budget can interrupt (C3-96).**
33
+ `_java_methods` runs a method pattern over every source, and that pattern let the
34
+ same whitespace be consumed in three places at once. A catastrophic match runs in
35
+ CPython's C regex engine, which never returns to the interpreter — so the
36
+ deadline C3-87 installed is not read, and neither is a Ctrl-C. Rewritten so every
37
+ separator is consumed once, with the matches asserted **identical** over 1 771
38
+ files: eureka **142,35 s → 2,10 s**, jobrunr 29,38 s → 1,75 s, openmrs-core
39
+ 35,25 s → 11,11 s, and `risk` on BroadleafCommerce **>20 min (killed) → 68 s**,
40
+ 15 of 15 defects composed.
41
+
42
+ **Not claimed by this release:** routing `spring-audit` and `risk` onto the shared
43
+ parse is a correctness change, not a measured speed-up — the per-file parse cache
44
+ already absorbed most of it, and the A/B on openmrs-core and BroadleafCommerce
45
+ came out inside the noise. The residual cost of the method scan is polynomial, not
46
+ linear, and `migrate-check` and `endpoints` still gain nothing from a warm; both
47
+ are open in `.planning/ROADMAP-FIELD-BATTERY-2026-08-10.md`.
48
+
5
49
  ## [4.17.0] — 2026-08-10
6
50
 
7
51
  **The Part 1 queues of field evaluations #20 and #21, and the first three
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 4.17.0
3
+ Version: 4.18.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
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
44
44
 
45
- ![Version](https://img.shields.io/badge/version-4.17.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-4.18.0-blue)
46
46
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
47
47
 
48
48
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -126,7 +126,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
126
126
  # pip / pipx
127
127
  pipx install sourcecode # or: pip install sourcecode
128
128
 
129
- ask version # ask 4.17.0 — and, on a build that has aged,
129
+ ask version # ask 4.18.0 — and, on a build that has aged,
130
130
  # how many releases have probably shipped since
131
131
  ```
132
132
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
6
6
 
7
- ![Version](https://img.shields.io/badge/version-4.17.0-blue)
7
+ ![Version](https://img.shields.io/badge/version-4.18.0-blue)
8
8
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
9
9
 
10
10
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -88,7 +88,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
88
88
  # pip / pipx
89
89
  pipx install sourcecode # or: pip install sourcecode
90
90
 
91
- ask version # ask 4.17.0 — and, on a build that has aged,
91
+ ask version # ask 4.18.0 — and, on a build that has aged,
92
92
  # how many releases have probably shipped since
93
93
  ```
94
94
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "4.17.0"
7
+ version = "4.18.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__ = "4.17.0"
7
+ __version__ = "4.18.0"
@@ -8285,14 +8285,20 @@ def spring_audit_cmd(
8285
8285
  )
8286
8286
  _stopped_early: Optional[str] = None
8287
8287
 
8288
+ from sourcecode import context_cache as _ctxcache
8289
+
8288
8290
  phase = f"auditing {len(file_list)} Java files"
8289
8291
  with _expensive_analysis_scope("spring-audit", target, phase):
8290
8292
  _prog = Progress()
8291
8293
  _prog.start(phase)
8292
8294
  try:
8293
- cir = ContextGraph.build(
8294
- file_list, target, progress=_work_sink(_prog)
8295
- ).cir
8295
+ # The parse is ~89 % of this command's wall clock (measured: 8,54 s of
8296
+ # 9,53 s on a 1 303-file repository) and it is the same parse the other
8297
+ # knowledge commands share, so it is fetched rather than rebuilt — which
8298
+ # is what makes a warm worth anything here (cold == warm before C3-94).
8299
+ cir = _ctxcache.shared_cir(
8300
+ target, file_list, progress=_work_sink(_prog)
8301
+ )
8296
8302
  # C3-85: the stretch between the last `linking` tick and the first rule
8297
8303
  # tick reports nothing, and what stays on the line is `n/n` — a counter
8298
8304
  # that finished, reading as a stage still running. Measured on keycloak
@@ -9966,7 +9972,7 @@ def _migration_blast_radius(
9966
9972
  from sourcecode.spring_model import SpringSemanticModel
9967
9973
 
9968
9974
  try:
9969
- cir, _ = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, file_list)
9975
+ cir = _ctxcache.shared_cir(target, file_list)
9970
9976
  except Exception:
9971
9977
  cir = ContextGraph.build(file_list, target).cir
9972
9978
  model = SpringSemanticModel.build(cir)
@@ -10520,10 +10526,7 @@ def impact_chain_cmd(
10520
10526
  # a fresh build, exactly as explain does, so impact-chain never breaks.
10521
10527
  from sourcecode import context_cache as _ctxcache
10522
10528
  try:
10523
- cir, _ = _ctxcache.get_or_build_cir(
10524
- _resolve_repo_root(target), target, file_list,
10525
- progress=_work_sink(_prog),
10526
- )
10529
+ cir = _ctxcache.shared_cir(target, file_list, progress=_work_sink(_prog))
10527
10530
  except Exception:
10528
10531
  cir = ContextGraph.build(file_list, target, progress=_work_sink(_prog)).cir
10529
10532
  _model = SpringSemanticModel.build(cir)
@@ -10849,11 +10852,13 @@ def explain_cmd(
10849
10852
  )
10850
10853
  raise typer.Exit(code=1)
10851
10854
 
10852
- # AI Context Cache — operates at the *knowledge* level: it caches the
10853
- # reusable Canonical IR (the expensive Java parse), keyed only by knowledge
10854
- # state, not by command. explain then derives its answer cheaply from that
10855
- # shared CIR. The same cached CIR is reused for free by any other command.
10856
- # Provider-agnostic and best-effort: any fault degrades to a fresh build.
10855
+ # AI Context Cache — operates at the *knowledge* level: it caches the reusable
10856
+ # Canonical IR (the expensive Java parse), keyed by knowledge state and by the
10857
+ # analysed set, not by command. explain then derives its answer cheaply from
10858
+ # that shared CIR, and any other command over the same files reuses it for
10859
+ # free. Provider-agnostic and best-effort: any fault degrades to a fresh build.
10860
+ # This call site keeps the lower-level entry point because it reports the
10861
+ # lookup (`_cc_look`); `shared_cir` is the same door without the receipt.
10857
10862
  _prog = Progress()
10858
10863
  _prog.start(f"explaining {class_name} ({len(file_list)} files)")
10859
10864
  _cc_look = None
@@ -651,6 +651,33 @@ class KnowledgeLookup:
651
651
  return f"Context cache: MISS (build {self.build_ms:.0f}ms)"
652
652
 
653
653
 
654
+ def analysed_set_signature(root: Path, file_list: list[str]) -> str:
655
+ """Fingerprint of *what was analysed*: the scope root and the exact file set.
656
+
657
+ The shared CIR is keyed on knowledge state, and until C3-94 "knowledge state"
658
+ was read as the worktree alone — which is only half of it. A run bounded to a
659
+ subdirectory parses a *subset* of the same worktree, and the resulting CIR is
660
+ a different artifact answering a different question. Keyed on the worktree
661
+ alone, both artifacts collided on one entry: measured on a 861-file repository,
662
+ ``explain WebUtil <repo>/web`` stored a 42-file / 382-symbol CIR under the
663
+ repo-wide key, and the next repo-wide command read it and answered about 3 % of
664
+ the repository — silently, because the reader passes its own ``file_paths`` to
665
+ ``ir_dict_to_canonical`` and the reconstructed CIR then reports 861 files
666
+ beside 382 symbols.
667
+
668
+ So the analysed set is stated, not hoped for. The scope path and the file list
669
+ are both in it: two roots can yield the same file count, and a caller that
670
+ filters its list (tests in or out) is asking about a different population than
671
+ one that does not.
672
+ """
673
+ payload = "".join([
674
+ str(root.resolve()),
675
+ str(len(file_list)),
676
+ hashlib.sha256("\n".join(sorted(file_list)).encode("utf-8")).hexdigest(),
677
+ ])
678
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
679
+
680
+
654
681
  def get_or_build_cir(
655
682
  repo_root: Path,
656
683
  root: Path,
@@ -662,11 +689,19 @@ def get_or_build_cir(
662
689
  """Return ``(CanonicalRepositoryIR, KnowledgeLookup)``, caching the CIR.
663
690
 
664
691
  This is the knowledge-level entry point. It caches the **reusable** raw IR
665
- (the expensive Java parse) keyed only by knowledge state not by command
666
- so every command that needs a CIR shares one cache entry. On a hit the full
667
- CIR is rebuilt cheaply from the cached raw IR via ``ir_dict_to_canonical``,
668
- skipping the parse entirely. Best-effort: any cache fault falls back to a
669
- fresh build.
692
+ (the expensive Java parse) keyed by knowledge state and by the analysed set
693
+ not by command — so every command that parses the same files shares one cache
694
+ entry. On a hit the full CIR is rebuilt cheaply from the cached raw IR via
695
+ ``ir_dict_to_canonical``, skipping the parse entirely. Best-effort: any cache
696
+ fault falls back to a fresh build.
697
+
698
+ A **repo-wide** run (``root`` is ``repo_root``) owns the scope-free entry —
699
+ the one :func:`peek_cir` reads without knowing any file list, which is why
700
+ that key must stay free of one. Every **bounded** run keys on its own analysed
701
+ set instead, so it can neither read nor overwrite the repo-wide answer. Either
702
+ way the entry carries the analysed signature it was built from and a reader
703
+ whose set does not match rebuilds: the key prevents the collision, the check
704
+ makes the prevention verifiable rather than assumed (C3-94).
670
705
 
671
706
  `progress` is the (done, total, stage) sink the caller's heartbeat reads
672
707
  (C3-56). It reaches only the *building* paths, which is the honest place for
@@ -684,14 +719,23 @@ def get_or_build_cir(
684
719
  cir = build_canonical_ir(file_list, root, since=since, progress=progress)
685
720
  return cir, KnowledgeLookup(hit=False, enabled=False)
686
721
 
687
- key = cache.knowledge_key(SCOPE_JAVA_CIR, options={"since": since} if since else None)
722
+ analysed = analysed_set_signature(root, file_list)
723
+ options: dict[str, Any] = {"since": since} if since else None
724
+ if root.resolve() != repo_root.resolve():
725
+ # A bounded run: its own entry, never the repo-wide one.
726
+ options = dict(options or {})
727
+ options["analysed"] = analysed
728
+ key = cache.knowledge_key(SCOPE_JAVA_CIR, options=options)
688
729
 
689
730
  t0 = time.perf_counter()
690
731
  cached = cache.get(key)
691
732
  lookup_ms = (time.perf_counter() - t0) * 1000
692
733
  if cached is not None:
693
734
  raw = cached.payload.get("raw_ir")
694
- if isinstance(raw, dict):
735
+ stored = (cached.metadata or {}).get("analysed_set")
736
+ # An entry written before C3-94 carries no signature; it cannot be shown to
737
+ # describe this analysed set, so it is not served as if it did.
738
+ if isinstance(raw, dict) and stored == analysed:
695
739
  try:
696
740
  cir = ir_dict_to_canonical(raw, file_paths=file_list)
697
741
  if not validate_canonical_ir(cir):
@@ -708,6 +752,8 @@ def get_or_build_cir(
708
752
  "scope": SCOPE_JAVA_CIR,
709
753
  "cir_hash": cir.cir_hash,
710
754
  "schema_version": cir.schema_version,
755
+ "analysed_set": analysed,
756
+ "analysed_root": str(root.resolve()),
711
757
  "file_count": len(cir.files),
712
758
  "symbol_count": len(cir.symbols),
713
759
  },
@@ -725,6 +771,41 @@ def get_or_build_cir(
725
771
  return cir, KnowledgeLookup(hit=False, enabled=True, build_ms=build_ms)
726
772
 
727
773
 
774
+ def repo_root_of(root: Path) -> Path:
775
+ """The enclosing git root, or *root* itself outside a repository."""
776
+ candidate = root.resolve()
777
+ while True:
778
+ if (candidate / ".git").exists():
779
+ return candidate
780
+ if candidate.parent == candidate:
781
+ return root.resolve()
782
+ candidate = candidate.parent
783
+
784
+
785
+ def shared_cir(
786
+ root: Path,
787
+ file_list: list[str],
788
+ *,
789
+ since: Optional[str] = None,
790
+ progress: "Optional[Callable[[int, int, str], None]]" = None,
791
+ ) -> Any:
792
+ """The Java CIR for *root*, from the shared knowledge cache.
793
+
794
+ The one door for a command that needs the repository parse and does not care
795
+ where it comes from: it resolves the enclosing repository itself, so a caller
796
+ holds no opinion about cache scope — the reason each call-site that built its
797
+ own CIR was one more place for the scope rule to be got wrong (C3-94).
798
+
799
+ Best-effort by construction: `get_or_build_cir` degrades to a fresh build on
800
+ any cache fault, so the answer never depends on the cache being there.
801
+ """
802
+ cir, _lookup = get_or_build_cir(
803
+ repo_root_of(root), Path(root).resolve(), file_list,
804
+ since=since, progress=progress,
805
+ )
806
+ return cir
807
+
808
+
728
809
  def peek_cir(
729
810
  repo_root: Path,
730
811
  *,
@@ -755,6 +836,12 @@ def peek_cir(
755
836
  raw = cached.payload.get("raw_ir")
756
837
  if not isinstance(raw, dict):
757
838
  return None
839
+ # The caller is promised the *repo-wide* CIR, so the entry has to say it is one.
840
+ # C3-94 makes the key unreachable to bounded runs; this re-reads the fact from
841
+ # the entry rather than inferring it from the key, and treats an entry written
842
+ # before that guarantee (no recorded root) as a miss.
843
+ if (cached.metadata or {}).get("analysed_root") != str(repo_root.resolve()):
844
+ return None
758
845
  try:
759
846
  cir = ir_dict_to_canonical(raw, file_paths=None)
760
847
  # validate_canonical_ir returns a LIST of problems — empty means valid.
@@ -81,11 +81,6 @@ _METHOD_RE = re.compile(
81
81
  re.MULTILINE,
82
82
  )
83
83
 
84
- _TYPE_RE = re.compile(
85
- r"^[ \t]*(?:public|final|abstract|static|\s)*\s*(?:class|interface|enum)\s+(?P<name>\w+)",
86
- re.MULTILINE,
87
- )
88
-
89
84
  #: Returns that are not an identity even inside an absence branch: re-dispatch,
90
85
  #: constants and the empty-optional answers, which are refusals in disguise.
91
86
  _NON_IDENTITY_RETURNS = re.compile(
@@ -126,13 +121,12 @@ class IdentityFallback:
126
121
 
127
122
  def _enclosing_type(source: str, offset: int) -> str:
128
123
  """The type declared closest above `offset`. Nested types resolve to the nearest
129
- enclosing declaration, which is the one a reader would name."""
130
- name = ""
131
- for m in _TYPE_RE.finditer(source):
132
- if m.start() > offset:
133
- break
134
- name = m.group("name")
135
- return name
124
+ enclosing declaration, which is the one a reader would name.
125
+
126
+ Thin alias of the one authority (C3-95) — see `source_text.enclosing_type`."""
127
+ from sourcecode.source_text import enclosing_type
128
+
129
+ return enclosing_type(source, offset, "")
136
130
 
137
131
 
138
132
  def _method_body(source: str, brace_index: int) -> str:
@@ -1109,27 +1109,22 @@ def _repo_root_of(root: Path) -> Path:
1109
1109
 
1110
1110
 
1111
1111
  def shared_cir(root: Path, files: "list[str]"):
1112
- """The Java CIR for *root*, from the shared knowledge cache when it is safe.
1112
+ """The Java CIR for *root*, from the shared knowledge cache.
1113
1113
 
1114
1114
  `posture` built its own CIR on every run — the same parse `explain`,
1115
1115
  `impact-chain` and `cache warm` already share — so the one command the
1116
1116
  product differentiates on was the one a warm could not help (C3-25).
1117
1117
 
1118
- Poison-safe by the same rule `peek_cir` documents: the shared knowledge key
1119
- is repo-wide and carries no scope, so a run bounded to a subdirectory
1120
- neither reads it (it describes more than this run's scope) nor writes it (it
1121
- would store a truncated CIR under the repo-wide key). Those runs keep their
1122
- own unshared build.
1118
+ Poison-safety used to live here: the shared key was repo-wide and carried no
1119
+ scope, so a run bounded to a subdirectory had to keep its own unshared build
1120
+ or it would store a truncated CIR under the repo-wide key. C3-94 moved that
1121
+ rule into the cache itself — a bounded run now keys on its own analysed set
1122
+ so the bounded case is cached too instead of paying for the parse every time,
1123
+ and this function is the thin alias of the one authority it left behind.
1123
1124
  """
1124
- from sourcecode.context_graph import ContextGraph
1125
+ from sourcecode.context_cache import shared_cir as _shared_cir
1125
1126
 
1126
- repo_root = _repo_root_of(root)
1127
- if repo_root != root:
1128
- return ContextGraph.build(files, root).cir
1129
- from sourcecode.context_cache import get_or_build_cir
1130
-
1131
- cir, _lookup = get_or_build_cir(repo_root, root, files)
1132
- return cir
1127
+ return _shared_cir(root, files)
1133
1128
 
1134
1129
 
1135
1130
  def _posture(
@@ -150,10 +150,30 @@ _URL_PATTERN_RE = re.compile(
150
150
  re.IGNORECASE,
151
151
  )
152
152
  _HTTP_INPUT_ANNOTATION_RE = re.compile(r"@(RequestParam|PathVariable|RequestBody)\b")
153
+ #: C3-96. Three of this pattern's groups used to let the same whitespace be
154
+ #: consumed in more than one place — `(?:\s*@…\s*)*`, `\s*(?:public|…)?\s*`, and
155
+ #: `[\w<>\[\].?,\s]+\s+`, whose character class contains the `\s` the following
156
+ #: `\s+` also matches. Each run of whitespace can then be split between the two in
157
+ #: many ways, and on text that never completes a signature the engine tries them
158
+ #: all: `_java_methods` runs this over **every** source, and a repository-wide
159
+ #: `risk` on a 2 985-file repository did not finish in 20 minutes — inside the C
160
+ #: regex engine, where no signal, budget or Ctrl-C can reach it (C3-87's deadline
161
+ #: is checked between stages, and this never returns to one).
162
+ #:
163
+ #: Rewritten so every separator is consumed once: each modifier takes its own
164
+ #: trailing whitespace, and the return type is tokens joined by explicit
165
+ #: separators instead of one class that contains the separator too.
166
+ #:
167
+ #: The **same** language, deliberately, including the part that reads like a bug:
168
+ #: the old class matched whitespace alone, so a bare `if (cond) {` was admitted as
169
+ #: a method named `if`. Those pseudo-methods carry propagation through control-flow
170
+ #: blocks in the walk below, so dropping them is a change to what `risk` finds, not
171
+ #: a speed-up — which is why the type stays optional here and the question of
172
+ #: whether it should is left to its own increment, with its own field measurement.
153
173
  _JAVA_METHOD_RE = re.compile(
154
- r"(?P<ann>(?:\s*@[\w.]+(?:\([^;{}]*?\))?\s*)*)"
155
- r"(?P<sig>\s*(?:public|protected|private)?\s*(?:static\s+)?"
156
- r"[\w<>\[\].?,\s]+\s+(?P<name>[A-Za-z_$][\w$]*)\s*"
174
+ r"(?P<ann>(?:@[\w.]+(?:\([^;{}]*?\))?\s*)*)"
175
+ r"(?P<sig>(?:(?:public|protected|private)\s+)?(?:static\s+)?"
176
+ r"(?:[\w<>\[\].?,]+(?:\s+[\w<>\[\].?,]+)*\s+|\s{2,})(?P<name>[A-Za-z_$][\w$]*)\s*"
157
177
  r"\((?P<params>[^)]*)\)\s*)\{",
158
178
  re.MULTILINE,
159
179
  )
@@ -797,7 +817,7 @@ class RiskComposer:
797
817
  profiles: "Optional[set[str]]" = None,
798
818
  progress: "Optional[Callable[..., Any]]" = None,
799
819
  ) -> None:
800
- from sourcecode.canonical_ir import build_canonical_ir
820
+ from sourcecode.context_cache import shared_cir
801
821
  from sourcecode.repository_ir import extract_java_endpoints, find_java_files
802
822
  from sourcecode.security_posture import (
803
823
  endpoint_security_surface,
@@ -808,8 +828,11 @@ class RiskComposer:
808
828
  from sourcecode.spring_tx_analyzer import run_tx_audit
809
829
 
810
830
  self.root = Path(root).resolve()
811
- self.cir = cir if cir is not None else build_canonical_ir(
812
- find_java_files(self.root), self.root
831
+ # The parse is ~80 % of this command's wall clock (measured: 12,45 s of
832
+ # 15,64 s on a 1 303-file repository) and it is the same parse `posture`,
833
+ # `explain` and `cache warm` already share — so it is fetched, not rebuilt.
834
+ self.cir = cir if cir is not None else shared_cir(
835
+ self.root, find_java_files(self.root)
813
836
  )
814
837
  self.model = SpringSemanticModel.build(self.cir)
815
838
 
@@ -249,14 +249,10 @@ def _line_of(source: str, index: int) -> int:
249
249
 
250
250
 
251
251
  def _enclosing_type(source: str, offset: int, fallback: str) -> str:
252
- name = fallback
253
- for m in re.finditer(
254
- r"^[ \t]*(?:public|final|abstract|static|\s)*\s*(?:class|interface|enum)\s+(?P<n>\w+)",
255
- source[:offset],
256
- re.MULTILINE,
257
- ):
258
- name = m.group("n")
259
- return name
252
+ """Thin alias of the one authority (C3-95) — see `source_text.enclosing_type`."""
253
+ from sourcecode.source_text import enclosing_type
254
+
255
+ return enclosing_type(source, offset, fallback)
260
256
 
261
257
 
262
258
  def _scan_java(source: str, rel: str) -> "list[SecurityConfigObservation]":
@@ -30,6 +30,7 @@ other.
30
30
  from __future__ import annotations
31
31
 
32
32
  import re
33
+ from typing import Optional
33
34
 
34
35
  #: Java/C-family lexical tokens, in precedence order. String and character
35
36
  #: literals are matched **first** so that a `//` or `/*` living inside one — as in
@@ -84,6 +85,70 @@ def blank_hash_comments(source: str) -> str:
84
85
  return _HASH_COMMENT_LINE.sub(lambda m: _blank(m.group(0)), source)
85
86
 
86
87
 
88
+ #: The one authority for "a type is declared here", used by every scanner that has
89
+ #: to name the type enclosing a match it found in raw text.
90
+ #:
91
+ #: C3-95. Two modules kept their own copy, and both were written as
92
+ #: ``(?:public|final|abstract|static|\s)*\s*`` — a whitespace branch inside a
93
+ #: starred group followed by another starred whitespace group. Every run of spaces
94
+ #: can then be split between the two in exponentially many ways, and on a line that
95
+ #: never reaches `class` the engine tries them all before failing: measured at
96
+ #: 13 ms for 200 leading spaces, 1,17 s for 800 and **10,1 s for 1 600** — one line,
97
+ #: one file. A generated or reformatted source is enough to hang a scan that is
98
+ #: supposed to take seconds, and `spring-audit` and `risk` both run this scan.
99
+ #:
100
+ #: Written so each modifier must consume its own trailing whitespace, the group is
101
+ #: unambiguous and the match is linear. It accepts the same declarations: the
102
+ #: modifier list is wider than the original (which silently rejected
103
+ #: `private`/`protected`/`sealed`), and the separator is still any whitespace, so a
104
+ #: modifier on its own line still binds to the declaration below it.
105
+ JAVA_TYPE_DECLARATION = re.compile(
106
+ r"^[ \t]*"
107
+ r"(?:(?:public|protected|private|final|abstract|static|sealed|non-sealed)\s+)*"
108
+ r"(?:class|interface|enum|record)\s+(?P<name>\w+)",
109
+ re.MULTILINE,
110
+ )
111
+
112
+
113
+ def type_declarations(source: str) -> "list[tuple[int, str]]":
114
+ """``(offset, name)`` of every type declared in *source*, in source order."""
115
+ return [(m.start(), m.group("name")) for m in JAVA_TYPE_DECLARATION.finditer(source)]
116
+
117
+
118
+ def enclosing_type(source: str, offset: int, fallback: str = "") -> str:
119
+ """Name of the type declared closest above *offset*, else *fallback*.
120
+
121
+ Nested types resolve to the nearest enclosing declaration, which is the one a
122
+ reader would name. Callers ask this once per observation and a file can carry
123
+ many, so the scan of a given source is memoised: the previous shape sliced
124
+ ``source[:offset]`` and re-scanned it per call, which is a copy and a walk of
125
+ the whole prefix for every finding in the file.
126
+
127
+ At the boundary the two prior copies disagreed, and this one keeps
128
+ ``identity_fallback``'s rule: the nearest declaration **at or above** the
129
+ offset. Slicing the prefix dropped a declaration that straddled the offset, so
130
+ a match on the declaration's own line was attributed to the type above it or to
131
+ the fallback. Every real observation sits strictly after its declaration, which
132
+ is why the field results are unchanged either way.
133
+ """
134
+ global _DECL_MEMO
135
+ memo_key = (len(source), hash(source))
136
+ if _DECL_MEMO is None or _DECL_MEMO[0] != memo_key:
137
+ _DECL_MEMO = (memo_key, type_declarations(source))
138
+ name = fallback
139
+ for start, declared in _DECL_MEMO[1]:
140
+ if start > offset:
141
+ break
142
+ name = declared
143
+ return name
144
+
145
+
146
+ #: One entry: callers sweep a file's findings together, so the next question is
147
+ #: almost always about the same source. Holding one keeps the memo from becoming a
148
+ #: second copy of every file the process ever read.
149
+ _DECL_MEMO: "Optional[tuple[tuple[int, int], list[tuple[int, str]]]]" = None
150
+
151
+
87
152
  def commented_spans(source: str, *, xml: bool) -> "list[tuple[int, int]]":
88
153
  """`(start, end)` offsets of every comment — what was switched off, not what runs.
89
154
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes