sourcecode 4.15.0__tar.gz → 4.16.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 (230) hide show
  1. {sourcecode-4.15.0 → sourcecode-4.16.0}/CHANGELOG.md +104 -0
  2. {sourcecode-4.15.0 → sourcecode-4.16.0}/PKG-INFO +3 -3
  3. {sourcecode-4.15.0 → sourcecode-4.16.0}/README.md +2 -2
  4. {sourcecode-4.15.0 → sourcecode-4.16.0}/pyproject.toml +1 -1
  5. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/cli.py +29 -32
  7. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/phased_run.py +55 -2
  8. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/progress.py +27 -0
  9. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/repository_ir.py +12 -0
  10. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/risk.py +136 -3
  11. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/rule_pass.py +12 -0
  12. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/security_config_scan.py +5 -0
  13. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_security_audit.py +5 -0
  14. {sourcecode-4.15.0 → sourcecode-4.16.0}/.github/workflows/build-windows.yml +0 -0
  15. {sourcecode-4.15.0 → sourcecode-4.16.0}/.github/workflows/perf-gate.yml +0 -0
  16. {sourcecode-4.15.0 → sourcecode-4.16.0}/.gitignore +0 -0
  17. {sourcecode-4.15.0 → sourcecode-4.16.0}/.ruff.toml +0 -0
  18. {sourcecode-4.15.0 → sourcecode-4.16.0}/CLAUDE.md +0 -0
  19. {sourcecode-4.15.0 → sourcecode-4.16.0}/CONTRIBUTING.md +0 -0
  20. {sourcecode-4.15.0 → sourcecode-4.16.0}/LICENSE +0 -0
  21. {sourcecode-4.15.0 → sourcecode-4.16.0}/SECURITY.md +0 -0
  22. {sourcecode-4.15.0 → sourcecode-4.16.0}/raw +0 -0
  23. {sourcecode-4.15.0 → sourcecode-4.16.0}/scripts/compare_integration_engines.py +0 -0
  24. {sourcecode-4.15.0 → sourcecode-4.16.0}/scripts/customer_smoke_test.sh +0 -0
  25. {sourcecode-4.15.0 → sourcecode-4.16.0}/scripts/generate_jdk_exports.py +0 -0
  26. {sourcecode-4.15.0 → sourcecode-4.16.0}/scripts/perf_gate.py +0 -0
  27. {sourcecode-4.15.0 → sourcecode-4.16.0}/scripts/perf_harness.py +0 -0
  28. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/adaptive_scanner.py +0 -0
  29. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/archetype.py +0 -0
  30. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/architectural_baseline.py +0 -0
  31. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/architectural_delta.py +0 -0
  32. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/architecture_analyzer.py +0 -0
  33. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/architecture_summary.py +0 -0
  34. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/ast_extractor.py +0 -0
  35. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/audit_report.py +0 -0
  36. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/baseline_autocapture.py +0 -0
  37. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/cache.py +0 -0
  38. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/cache_model.py +0 -0
  39. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/call_surface.py +0 -0
  40. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/caller_metrics.py +0 -0
  41. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/caller_reach.py +0 -0
  42. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/canonical_ir.py +0 -0
  43. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/chain_rules.py +0 -0
  44. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/change_plan.py +0 -0
  45. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/cir_graphs.py +0 -0
  46. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/classifier.py +0 -0
  47. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/client_calls.py +0 -0
  48. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  49. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/compare.py +0 -0
  50. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/confidence_analyzer.py +0 -0
  51. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/constraint_diff.py +0 -0
  52. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/container_wiring.py +0 -0
  53. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/context_cache.py +0 -0
  54. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/context_graph.py +0 -0
  55. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/context_scorer.py +0 -0
  56. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/context_summarizer.py +0 -0
  57. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/contract_diff.py +0 -0
  58. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/contract_init.py +0 -0
  59. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/contract_model.py +0 -0
  60. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/contract_pipeline.py +0 -0
  61. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/coverage_parser.py +0 -0
  62. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/data_exposure.py +0 -0
  63. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/data_labels.py +0 -0
  64. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/defect_identity.py +0 -0
  65. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/degradation.py +0 -0
  66. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/dependency_analyzer.py +0 -0
  67. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/deployment_prefix.py +0 -0
  68. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detach.py +0 -0
  69. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/__init__.py +0 -0
  70. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/base.py +0 -0
  71. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  72. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/dart.py +0 -0
  73. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/dotnet.py +0 -0
  74. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/elixir.py +0 -0
  75. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/go.py +0 -0
  76. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/heuristic.py +0 -0
  77. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/hybrid.py +0 -0
  78. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/java.py +0 -0
  79. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  80. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/nodejs.py +0 -0
  81. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/parsers.py +0 -0
  82. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/php.py +0 -0
  83. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/project.py +0 -0
  84. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/python.py +0 -0
  85. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/ruby.py +0 -0
  86. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/rust.py +0 -0
  87. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/systems.py +0 -0
  88. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/terraform.py +0 -0
  89. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/detectors/tooling.py +0 -0
  90. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/doc_analyzer.py +0 -0
  91. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  92. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/endpoint_literals.py +0 -0
  93. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/endpoint_metrics.py +0 -0
  94. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  95. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/env_analyzer.py +0 -0
  96. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/envelope.py +0 -0
  97. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/environment_resolution.py +0 -0
  98. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/error_schema.py +0 -0
  99. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/evidence_provider.py +0 -0
  100. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/execution_plan.py +0 -0
  101. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/explain.py +0 -0
  102. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/facts/__init__.py +0 -0
  103. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/facts/registry.json +0 -0
  104. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/file_chunker.py +0 -0
  105. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/file_classifier.py +0 -0
  106. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/filter_surface.py +0 -0
  107. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/format_contract.py +0 -0
  108. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/fqn_utils.py +0 -0
  109. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/git_analyzer.py +0 -0
  110. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/git_checkout.py +0 -0
  111. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/graph_analyzer.py +0 -0
  112. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/graph_evidence.py +0 -0
  113. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/hibernate_strat.py +0 -0
  114. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/identity_fallback.py +0 -0
  115. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/integration_coordinates.py +0 -0
  116. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/jdk_exports.py +0 -0
  117. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/license.py +0 -0
  118. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/__init__.py +0 -0
  119. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/dependency_import.py +0 -0
  120. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  121. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  122. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  123. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  124. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  125. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  126. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/registry.py +0 -0
  127. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/runner.py +0 -0
  128. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp/server.py +0 -0
  129. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/mcp_nudge.py +0 -0
  130. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/metrics_analyzer.py +0 -0
  131. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/migrate_check.py +0 -0
  132. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/migration_blast.py +0 -0
  133. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/non_coverage.py +0 -0
  134. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/openapi_surface.py +0 -0
  135. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/openrewrite_recipe.py +0 -0
  136. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/output_budget.py +0 -0
  137. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/output_encoding.py +0 -0
  138. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/parallel.py +0 -0
  139. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/parse_cache.py +0 -0
  140. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/path_admission.py +0 -0
  141. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/path_filters.py +0 -0
  142. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/perf.py +0 -0
  143. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/pipe_contract.py +0 -0
  144. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/posture.py +0 -0
  145. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  146. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/pr_impact.py +0 -0
  147. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/prepare_context.py +0 -0
  148. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/provenance.py +0 -0
  149. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/ranking_engine.py +0 -0
  150. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/readiness_timeline.py +0 -0
  151. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/readonly.py +0 -0
  152. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/reconciliation.py +0 -0
  153. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/redactor.py +0 -0
  154. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/reference_facts.py +0 -0
  155. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/release_info.py +0 -0
  156. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/relevance_scorer.py +0 -0
  157. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/remedies.py +0 -0
  158. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/rename_refactor.py +0 -0
  159. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/repo_classifier.py +0 -0
  160. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/__init__.py +0 -0
  161. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/context.py +0 -0
  162. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/errors.py +0 -0
  163. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/executor.py +0 -0
  164. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/planner.py +0 -0
  165. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/query.py +0 -0
  166. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/request.py +0 -0
  167. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/resolution.py +0 -0
  168. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/result.py +0 -0
  169. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/retriever.py +0 -0
  170. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/runtime.py +0 -0
  171. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/steps.py +0 -0
  172. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  173. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
  174. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
  175. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
  176. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
  177. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  178. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/ris.py +0 -0
  179. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/rule_catalog.py +0 -0
  180. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/runs.py +0 -0
  181. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/runtime_classifier.py +0 -0
  182. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/sarif.py +0 -0
  183. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/scanner.py +0 -0
  184. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/schema.py +0 -0
  185. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  186. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/security_config.py +0 -0
  187. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/security_posture.py +0 -0
  188. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/semantic_analyzer.py +0 -0
  189. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  190. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/semantic_integration_engine.py +0 -0
  191. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/semantic_services.py +0 -0
  192. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/serializer.py +0 -0
  193. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/servlet_surface.py +0 -0
  194. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/source_text.py +0 -0
  195. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_event_topology.py +0 -0
  196. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_findings.py +0 -0
  197. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_impact.py +0 -0
  198. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_model.py +0 -0
  199. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_profiles.py +0 -0
  200. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_properties.py +0 -0
  201. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_semantic.py +0 -0
  202. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  203. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/summarizer.py +0 -0
  204. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/target_admission.py +0 -0
  205. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/telemetry/__init__.py +0 -0
  206. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/telemetry/config.py +0 -0
  207. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/telemetry/consent.py +0 -0
  208. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/telemetry/events.py +0 -0
  209. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/telemetry/filters.py +0 -0
  210. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/telemetry/transport.py +0 -0
  211. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/test_gap_ranking.py +0 -0
  212. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/test_sources.py +0 -0
  213. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/text_input.py +0 -0
  214. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/token_estimate.py +0 -0
  215. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/tree_utils.py +0 -0
  216. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/type_usage_surface.py +0 -0
  217. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/validation_inference.py +0 -0
  218. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/validation_surface.py +0 -0
  219. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/verify_edit.py +0 -0
  220. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/verify_repo.py +0 -0
  221. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/verify_rules.py +0 -0
  222. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/version_check.py +0 -0
  223. {sourcecode-4.15.0 → sourcecode-4.16.0}/src/sourcecode/workspace.py +0 -0
  224. {sourcecode-4.15.0 → sourcecode-4.16.0}/supabase/functions/README.md +0 -0
  225. {sourcecode-4.15.0 → sourcecode-4.16.0}/supabase/functions/get-license/index.ts +0 -0
  226. {sourcecode-4.15.0 → sourcecode-4.16.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  227. {sourcecode-4.15.0 → sourcecode-4.16.0}/supabase/functions/telemetry/index.ts +0 -0
  228. {sourcecode-4.15.0 → sourcecode-4.16.0}/supabase/sql/license_event_ordering.sql +0 -0
  229. {sourcecode-4.15.0 → sourcecode-4.16.0}/supabase/sql/licensing_schema.sql +0 -0
  230. {sourcecode-4.15.0 → sourcecode-4.16.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -2,6 +2,110 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.16.0] — 2026-08-10
6
+
7
+ **The Part 1 queue of field evaluation #19 — three rows, one commit each — and
8
+ the round where reading the field's *numbers* against our source found more than
9
+ reading its *conclusions* did.** The sixth measurement of the same repository at
10
+ the same commit scored **8,2/10**, the highest of the series: *"4.15.0 es la
11
+ mejor versión medida."* It verified both of the previous release's rows from the
12
+ outside (C3-81's `stage_stopped_in`, C3-83's budget provenance), determinism held
13
+ a **sixth** consecutive build across five version jumps, and five commands got
14
+ much faster (`validation` −57 %, `posture --diff` −54 %).
15
+
16
+ Two of the three complaints it left open turned out not to be what was filed.
17
+
18
+ **Fixed in this release:**
19
+
20
+ - **`risk` declares a budget and never asks whether it is spent (C3-87).** The
21
+ field filed `risk` at 2 842 s under a 420 s budget as a +41 % performance
22
+ regression. It is a consumer left unbound. C3-71 and C3-79 made the deadline
23
+ reach inside the rule pass and bound exactly one: `spring-audit` passes
24
+ `stop_when=_run.exhausted` at both of its call sites, while `risk` built the
25
+ same `PhasedRun`, with the same `budget_seconds` and the same `budget_source`,
26
+ and then constructed its observers with **no `stop_when` at all** — so
27
+ `should_stop()` answered `False` for the life of the run and the budget could
28
+ only be observed on the boundary between two phases. Their own two numbers
29
+ prove it on one machine under one budget: `spring-audit` **1,94×** over, `risk`
30
+ **6,8×** over. Reproduced here on keycloak (5 486 files) with a 5 s budget —
31
+ `spring-audit` stops at 9,9 s with `_partial` published; `risk` was still
32
+ running at **10 minutes, 120×**, and had to be killed. Both rule passes and the
33
+ composition join now ask, each on the unit it iterates. A cut run publishes the
34
+ floor marks **where the numbers are** (`partial`, `counts_are_floor`,
35
+ `counts_basis` — C2-31's rule applied to the second consumer), names the join's
36
+ own cut (`composition_stopped_after`), and says PARTIAL on the terminal. A
37
+ complete run gains no field. The three rule-pass keys of `_partial` move into
38
+ `PhasedRun.status`: for four releases there was one caller, so "the caller" and
39
+ "the authority" were the same object by accident, and a second consumer would
40
+ otherwise have duplicated thirty lines of composition or invented a second
41
+ vocabulary for one fact. **The battery asserts an absence**, because the defect
42
+ was one — a missing argument at a call site is invisible in the payload of the
43
+ command that omits it, so it reads `cli.py` and fails on any
44
+ `_rule_pass_progress(...)` built without `stop_when`. A third consumer cannot
45
+ be added unbound.
46
+
47
+ - **A stage of an audit either counts its population or names itself (C3-85).**
48
+ Six rounds of field reports read the 14,5-minute silence as a dead emitter,
49
+ proposing that the timer be re-armed. Measured through the real seam, the
50
+ emitter is alive and correct: a per-unit tick loop produced **5 evenly spaced
51
+ lines over 5,4 s at a 1 s interval**. What is silent is the work no counter
52
+ covers — instrumented on keycloak, **8,2 s of a 24 s warm run, 34 %**, between
53
+ the last `linking` tick and the first rule tick, with `linking 5486/5486`
54
+ frozen on screen throughout; 2,7 s more inside `GATE-001`; 24,7 s on a cold run
55
+ after `parsing 50/5486`. Named, not counted: the IR build's tail (`assembling
56
+ the IR`, `recovering spec-declared routes`, `scanning XML security
57
+ configuration`), the semantic-model build, the whole-tree walk *inside* the
58
+ configuration family (through `FamilyBudget.stage`, so the line says which
59
+ family it belongs to), the rule pass's dedup-and-order tail, and each of the
60
+ five repository-wide walks `risk` makes before it composes. A single walk has
61
+ no population, and inventing a denominator for it is the fabrication C3-56
62
+ refused. The assertion is the invariant rather than the symptom: a build may
63
+ not end on a completed counter, because everything after it runs under a line
64
+ that reads as still running.
65
+
66
+ - **A count that has stopped moving says so, and stops estimating (C3-86).** The
67
+ field's line — `3340/3342 java files eta=0.5s` — was written **fourteen
68
+ minutes** after the counter last advanced, and both of its halves are claims
69
+ the run cannot support: it asserts the stage is 99,9 % done *now*, and the ETA
70
+ divides the remaining units by a rate measured over a window in which nothing
71
+ happened, so the longer the stall lasted the more imminent the finish looked. A
72
+ line whose counted state has not advanced for a whole interval now publishes
73
+ `unchanged_for=<duration>` and withholds the estimate — the rule
74
+ `_eta_seconds` already applies to a stage too young to extrapolate from. The
75
+ count itself is still printed: it is true, and it is what a reader wants; what
76
+ is withheld is the assertion that it is current. The staleness clock is
77
+ deliberately not the stage clock — a long, healthy, advancing stage must not be
78
+ marked frozen, and both directions are asserted. An uncounted stage is never
79
+ called stale: it never claimed a denominator.
80
+
81
+ **Measured and not reproduced:** the report's third proposed fix — *"corregir la
82
+ documentación del flag `--jobs`"* — is already shipped. `JOBS_OPTION_HELP` in
83
+ this build reads *"It does NOT parallelise the rule pass, which is where a
84
+ repository-wide audit spends most of its time, and on a warm cache there is
85
+ nothing left to parse, so raising it buys nothing there."* The report quotes a
86
+ truncated earlier string. Recorded on CL-18 so it is not re-opened, which is the
87
+ CL-17 precedent.
88
+
89
+ **Deliberately not in this release**, and each for a stated reason: **F-AT**
90
+ (parallel rule evaluation — the field's single priority, *"lo único que separa
91
+ este producto de un 9"*: a capability, and it does not start before this queue
92
+ finishes), **F-AV** (a distinct exit code for a partial answer, or renaming
93
+ `total_defects` to `total_defects_floor` when `partial` is true — a published
94
+ contract change, never a side effect of a correction), **F-AU** (reusing the
95
+ cached audit in the compositions), and **C3-76/F-AW** (the cross-version `risk`
96
+ variance: macOS measures both sides flat for the second consecutive A/B, and a
97
+ regression that reproduces on one platform cannot be bisected from the platform
98
+ where it does not).
99
+
100
+ **What C3-87 does not close, stated plainly: the wall time.** After the fix,
101
+ `risk` on BroadleafCommerce (2 985 files) still runs past ten minutes under a 3 s
102
+ budget, because the cost is not in the passes that now ask. That is **C3-88**,
103
+ and it is open with a measurement rather than a hypothesis — the stage-by-stage
104
+ profile was started and cancelled before it attributed the time, so the row says
105
+ so instead of guessing.
106
+
107
+ Suite **7 833** (7 813 before), 20 new assertions, zero failures.
108
+
5
109
  ## [4.15.0] — 2026-08-10
6
110
 
7
111
  **The Part 1 queue of Amendment O — the four rows field evaluation #18 opened,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 4.15.0
3
+ Version: 4.16.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.15.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-4.16.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.15.0 — and, on a build that has aged,
129
+ ask version # ask 4.16.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.15.0-blue)
7
+ ![Version](https://img.shields.io/badge/version-4.16.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.15.0 — and, on a build that has aged,
91
+ ask version # ask 4.16.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.15.0"
7
+ version = "4.16.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.15.0"
7
+ __version__ = "4.16.0"
@@ -8230,6 +8230,14 @@ def spring_audit_cmd(
8230
8230
  cir = ContextGraph.build(
8231
8231
  file_list, target, progress=_work_sink(_prog)
8232
8232
  ).cir
8233
+ # C3-85: the stretch between the last `linking` tick and the first rule
8234
+ # tick reports nothing, and what stays on the line is `n/n` — a counter
8235
+ # that finished, reading as a stage still running. Measured on keycloak
8236
+ # (5 486 files, warm): **8,2 s of a 24 s run, 34 %**, all of it here.
8237
+ # It has no population to count — it is one walk that builds the model
8238
+ # every rule then reads — so it is named, which is what C3-56 does for
8239
+ # a stage whose denominator would have to be invented.
8240
+ _prog.update(f"building the semantic model over {len(file_list)} Java files")
8233
8241
  _model = SpringSemanticModel.build(cir)
8234
8242
  _run.checkpoint("ir")
8235
8243
  # The IR is built; the rule passes are the work now, and they are not
@@ -8353,37 +8361,15 @@ def spring_audit_cmd(
8353
8361
 
8354
8362
  data = combined.to_compact_dict() if compact else combined.to_dict()
8355
8363
  if _stopped_early:
8356
- data["_partial"] = _run.status(_stopped_early)
8357
- if _rules_partial:
8358
- # C3-79: neither run nor not-run. How far it got is what a
8359
- # reader needs to decide whether to re-run with more time, and
8360
- # the findings it produced before the cut are in the payload.
8361
- data["_partial"]["rules_partially_run"] = _rules_partial
8362
- data["_partial"]["how_to_read"] += (
8363
- " One rule family was stopped part-way through its own "
8364
- "population: "
8365
- + "; ".join(
8366
- f"{'/'.join(entry['rule_ids'])} after "
8367
- f"{entry['units_done']} of {entry['units_total']} "
8368
- f"{entry['unit']}"
8369
- for entry in _rules_partial
8370
- )
8371
- + ". What it found before the cut is reported and is a floor."
8372
- )
8373
- if _rules_cut:
8374
- # C3-71: a phase stopped *inside* is neither completed nor
8375
- # pending, and calling it either would misreport the run. The
8376
- # families it did not reach are named instead.
8377
- data["_partial"]["rules_not_run"] = _rules_cut
8378
- # A phase that was stopped inside is in neither list, and
8379
- # putting it in `phases_pending` alone would claim none of it
8380
- # ran when most of it did.
8381
- data["_partial"]["phases_stopped_inside"] = _phases_cut
8382
- data["_partial"]["how_to_read"] += (
8383
- " The rule pass was stopped between families: "
8384
- f"{', '.join(_rules_cut)} did not run, so every count here "
8385
- "is a floor over the families that did."
8386
- )
8364
+ # C3-87: the block is composed by `PhasedRun.status`, which is the
8365
+ # authority `risk` is now bound to as well — one vocabulary for
8366
+ # "the budget cut this run", not one per command.
8367
+ data["_partial"] = _run.status(
8368
+ _stopped_early,
8369
+ rules_partially_run=_rules_partial,
8370
+ rules_not_run=_rules_cut,
8371
+ phases_stopped_inside=_phases_cut,
8372
+ )
8387
8373
 
8388
8374
  # Non-fatal RIS side-effect — persist summary only (not full findings).
8389
8375
  try:
@@ -8949,10 +8935,18 @@ def risk_cmd(
8949
8935
  profiles=_profile_set(profile),
8950
8936
  # C3-73: 54 minutes with one line on stderr in the field. The
8951
8937
  # factory hands the composer an observer per counted stage.
8938
+ #
8939
+ # C3-87: and the observer carries the deadline. This factory built
8940
+ # its observers with no `stop_when` for four releases, so `risk`
8941
+ # declared `budget_seconds` on the run above and then never asked
8942
+ # whether it was spent — the budget was honoured only between the
8943
+ # two phases. `PhasedRun.exhausted` stays the one authority; what
8944
+ # is new is that the second consumer asks it.
8952
8945
  progress=lambda stage, unit="rule families": _rule_pass_progress(
8953
- _prog, stage, unit
8946
+ _prog, stage, unit, stop_when=_run.exhausted
8954
8947
  ),
8955
8948
  checkpoint=_run.checkpoint,
8949
+ partial_status=lambda **cut: _run.status(_WHY_BUDGET, **cut),
8956
8950
  )
8957
8951
  finally:
8958
8952
  _prog.stop()
@@ -8969,6 +8963,9 @@ def risk_cmd(
8969
8963
  success_msg=(
8970
8964
  f"risk written to {output_path} ({data['shown']} of "
8971
8965
  f"{data['total_defects']} defects composed)"
8966
+ # C3-87: the terminal says it too. A reader who never opens the payload
8967
+ # must not take a truncated ranking for the repository's risk.
8968
+ + (" — PARTIAL, budget exhausted" if data.get("partial") else "")
8972
8969
  ),
8973
8970
  )
8974
8971
  # The complete answer is on disk; the checkpoint that stood in for it is not
@@ -131,8 +131,29 @@ class PhasedRun:
131
131
  done = set(self.completed)
132
132
  return [p for p in self.phases if p not in done]
133
133
 
134
- def status(self, why: str) -> dict:
135
- """The block an incomplete answer carries, and nothing else does."""
134
+ def status(
135
+ self,
136
+ why: str,
137
+ *,
138
+ rules_partially_run: "Optional[list[dict]]" = None,
139
+ rules_not_run: "Optional[list[str]]" = None,
140
+ phases_stopped_inside: "Optional[list[str]]" = None,
141
+ ) -> dict:
142
+ """The block an incomplete answer carries, and nothing else does.
143
+
144
+ **C3-87.** The three rule-pass keys used to be assembled by the caller, and
145
+ for four releases there was exactly one caller, so "the caller" and "the
146
+ authority" were the same object by accident. When `risk` was bound to the
147
+ same budget it became clear what that costs: a second consumer either
148
+ duplicates thirty lines of `how_to_read` composition or publishes a
149
+ different vocabulary for the same fact. They live here now, so a command
150
+ that stops inside its rule pass describes it the one way — and a *third*
151
+ consumer gets it by construction.
152
+
153
+ Each key is present only when there is something to say. A run stopped
154
+ between phases, with no family cut, produces exactly the block it produced
155
+ before this seam existed.
156
+ """
136
157
  out: dict[str, Any] = {
137
158
  "schema_version": SCHEMA_VERSION,
138
159
  "partial": True,
@@ -147,6 +168,38 @@ class PhasedRun:
147
168
  out["budget_seconds"] = float(self.budget_seconds)
148
169
  if self.budget_source:
149
170
  out["budget_source"] = self.budget_source
171
+ if rules_partially_run:
172
+ # C3-79: neither run nor not-run. How far it got is what a reader needs
173
+ # in order to decide whether to re-run with more time, and the findings
174
+ # it produced before the cut are in the payload.
175
+ out["rules_partially_run"] = list(rules_partially_run)
176
+ out["how_to_read"] += (
177
+ " One rule family was stopped part-way through its own "
178
+ "population: "
179
+ + "; ".join(
180
+ f"{'/'.join(entry['rule_ids'])} after "
181
+ f"{entry['units_done']} of {entry['units_total']} "
182
+ f"{entry['unit']}"
183
+ for entry in rules_partially_run
184
+ )
185
+ + ". What it found before the cut is reported and is a floor."
186
+ )
187
+ if phases_stopped_inside:
188
+ # C3-71: a phase stopped *inside* is neither completed nor pending, and
189
+ # putting it in `phases_pending` alone would claim none of it ran when
190
+ # most of it did. Independent of the rule keys: a phase can be cut in a
191
+ # walk that evaluates no rules at all, which is how `risk` loses its
192
+ # composition join (C3-87).
193
+ out["phases_stopped_inside"] = list(phases_stopped_inside)
194
+ if rules_not_run:
195
+ # The families it did not reach are named, so the answer states a gap
196
+ # rather than an absence of findings.
197
+ out["rules_not_run"] = list(rules_not_run)
198
+ out["how_to_read"] += (
199
+ " The rule pass was stopped between families: "
200
+ f"{', '.join(rules_not_run)} did not run, so every count here "
201
+ "is a floor over the families that did."
202
+ )
150
203
  return out
151
204
 
152
205
  # -- checkpoints -------------------------------------------------------
@@ -178,6 +178,10 @@ class Progress:
178
178
  self._unit = "files"
179
179
  self._stage = ""
180
180
  self._stage_t0 = time.monotonic()
181
+ #: When the counted state last *advanced* (C3-86). Distinct from
182
+ #: `_stage_t0`, which is when the stage began: a stage can begin, count to
183
+ #: 3 340 of 3 342, and then stand still while an uncounted walk runs.
184
+ self._count_t0 = time.monotonic()
181
185
  #: When a heartbeat line was last written, by either emitter (C3-80). One
182
186
  #: stamp is what keeps the loop and the in-band beat from double-printing.
183
187
  self._last_emit = 0.0
@@ -227,6 +231,7 @@ class Progress:
227
231
  self._total = 0
228
232
  self._stage = ""
229
233
  self._estimate = True
234
+ self._count_t0 = time.monotonic()
230
235
  # C3-80: a new phase is the most informative thing this line can say, and
231
236
  # the phase that follows it may be the long one.
232
237
  self._beat()
@@ -262,6 +267,11 @@ class Progress:
262
267
  rule `_eta_seconds` already applies to a stage that has barely started.
263
268
  """
264
269
  with self._lock:
270
+ # C3-86: read before anything is mutated — the clock the *staleness* of
271
+ # a line is measured against only restarts when the state it reports
272
+ # actually changed.
273
+ if (done, total, stage) != (self._done, self._total, self._stage):
274
+ self._count_t0 = time.monotonic()
265
275
  if stage != self._stage or done < self._done:
266
276
  self._stage = stage
267
277
  self._stage_t0 = time.monotonic()
@@ -407,6 +417,19 @@ class Progress:
407
417
  self._done, self._total, self._unit, self._stage
408
418
  )
409
419
  eta = self._eta_seconds(done, total)
420
+ # C3-86: how long this count has stood still. The field read
421
+ # `3340/3342 java files eta=0.5s` **fourteen minutes** after the
422
+ # counter last advanced, and both halves of that line are claims the
423
+ # run cannot support: it asserts the stage is 99,9 % done *now*, and
424
+ # the rate behind the ETA is computed over a window in which nothing
425
+ # happened — so the longer the stall lasts, the more imminent the
426
+ # finish looks. A count that has not moved for a whole interval says
427
+ # so and withholds the estimate, which is the rule `_eta_seconds`
428
+ # already applies to a stage too young to extrapolate from.
429
+ stalled = time.monotonic() - self._count_t0
430
+ stale = total > 0 and stalled >= self._interval
431
+ if stale:
432
+ eta = None
410
433
  counted = f"{done}/{total} {unit}" if total > 0 else ""
411
434
  if self._line_mode:
412
435
  line = f"[ask] progress phase={phase}"
@@ -415,6 +438,8 @@ class Progress:
415
438
  if counted:
416
439
  line += f" {counted}"
417
440
  line += f" elapsed={_duration(elapsed)}"
441
+ if stale:
442
+ line += f" unchanged_for={_duration(stalled)}"
418
443
  if eta is not None:
419
444
  # Named, not implied: the estimate covers the counted stage.
420
445
  # Nothing here knows the cost of the stages after it, and a
@@ -426,6 +451,8 @@ class Progress:
426
451
  if counted:
427
452
  shown += f" {counted}"
428
453
  tail = f"{elapsed:.1f}s"
454
+ if stale:
455
+ tail += f", unchanged for {_duration(stalled)}"
429
456
  if eta is not None:
430
457
  tail += f", eta {_duration(eta)}"
431
458
  line = f"\r\033[K{frame} {shown} ({tail})"
@@ -5538,6 +5538,14 @@ def build_repo_ir(
5538
5538
  )
5539
5539
  )
5540
5540
 
5541
+ # C3-85: from here to the end of the build there is no per-file loop, so the
5542
+ # heartbeat kept `linking n/n` on screen — a counter that finished, reading as
5543
+ # a stage still running. Measured on keycloak (5 486 files, warm), the stretch
5544
+ # between the last `linking` tick and the first rule tick was **8,2 s of a 24 s
5545
+ # run**. These stages have no population to count; they are named, which is
5546
+ # what C3-56 does rather than invent a denominator.
5547
+ if progress is not None:
5548
+ progress(0, 0, "assembling the IR")
5541
5549
  spring_summary = _build_spring_summary(all_symbols)
5542
5550
 
5543
5551
  # Deduplicate relations
@@ -5570,6 +5578,8 @@ def build_repo_ir(
5570
5578
  # when `since` is set). Done here, where `root` is available; placed before the
5571
5579
  # XML-security retag so spec routes lacking security get the same treatment.
5572
5580
  if route_diffs_arg is None:
5581
+ if progress is not None:
5582
+ progress(0, 0, "recovering spec-declared routes")
5573
5583
  _routed_fqns_ir = {
5574
5584
  r.get("effective_class")
5575
5585
  for r in (ir.get("route_surface") or [])
@@ -5597,6 +5607,8 @@ def build_repo_ir(
5597
5607
  r'|xmlns:security="http://www\.springframework\.org/schema/security")',
5598
5608
  re.IGNORECASE,
5599
5609
  )
5610
+ if progress is not None:
5611
+ progress(0, 0, "scanning XML security configuration")
5600
5612
  _xml_sec_detected = False
5601
5613
  for _xml_glob in (
5602
5614
  "*security*.xml", "*Security*.xml",
@@ -788,6 +788,14 @@ class RiskComposer:
788
788
  # caller owns — `(stage, unit) -> RulePassProgress | None` — so this module
789
789
  # never learns what a spinner is and a run nobody watches pays nothing.
790
790
  self._progress = progress
791
+ # C3-87: the observers are kept, not discarded. Each one holds what the
792
+ # operator's budget cut out of the pass it watched, and until this command
793
+ # was bound to the budget there was nothing to keep: `stop_when` was never
794
+ # passed, so `should_stop()` answered False for the life of the run and the
795
+ # deadline could only ever be observed on the boundary between two phases.
796
+ # The field measured what that costs on one machine under one 420 s
797
+ # budget — `spring-audit` 1,94× over, `risk` 6,8× over.
798
+ self._rule_watchers: "list[Any]" = []
791
799
  if findings is None:
792
800
  findings = list(run_tx_audit(
793
801
  self.cir, root=self.root, model=self.model,
@@ -801,7 +809,21 @@ class RiskComposer:
801
809
  )
802
810
  self.findings = findings
803
811
 
812
+ # C3-85: everything from here to the composition is repository-wide work
813
+ # with no population of its own, and it reported nothing — so the line held
814
+ # the last rule family's count while it ran. Each walk is named as it
815
+ # starts, which is what the operator needs in order to tell a slow stage
816
+ # from a hung one, and which is also how the next round gets to say *which*
817
+ # of these walks the wall time is in.
818
+ _setup = self._rule_progress("composing risk factors", "walks")
819
+
820
+ def _named(label: str) -> None:
821
+ if _setup is not None:
822
+ _setup.step(label)
823
+
824
+ _named("inferring the security posture")
804
825
  posture = infer_security_posture(self.cir, root=self.root)
826
+ _named("resolving the endpoint security surface")
805
827
  self.surface = endpoint_security_surface(self.cir, root=self.root, posture=posture)
806
828
  access = (
807
829
  posture.to_dict().get("endpoints", []) if hasattr(posture, "to_dict") else []
@@ -815,8 +837,11 @@ class RiskComposer:
815
837
  # F.4-1. One pass over the α argument-shape atoms and one over the validation
816
838
  # surface: both are repository-wide facts, read once and joined by symbol and
817
839
  # by route, never re-derived per row.
840
+ _named("reading concatenated-query owners")
818
841
  self.concat_owners = _concatenated_query_owners(self.cir)
842
+ _named("reading HTTP-input query sinks")
819
843
  self.http_input_query_sinks = _http_input_query_sinks(self.root, self.cir)
844
+ _named("indexing the validation surface")
820
845
  self.constraint_index = _constraint_index(
821
846
  self.root, self.cir, extract_java_endpoints(self.root)
822
847
  )
@@ -843,6 +868,7 @@ class RiskComposer:
843
868
 
844
869
  # One pass answers both: the access axis (C1-26) and how much of the
845
870
  # request surface each chain configuration file decides (C1-25).
871
+ _named("resolving the conditional bean graph")
846
872
  self.profile_access, self.chain_reach = access_resolution(
847
873
  self.root,
848
874
  self.profiles if self.profiles is not None else set(),
@@ -887,13 +913,58 @@ class RiskComposer:
887
913
  return symbol, "method" if "#" in symbol else "class"
888
914
 
889
915
  def _rule_progress(self, stage: str, unit: str = "rule families") -> "Any":
890
- """The observer for one counted stage, or None when nobody is watching."""
916
+ """The observer for one counted stage, or None when nobody is watching.
917
+
918
+ C3-87: "nobody is watching" now includes the budget. The factory returns an
919
+ observer whenever a heartbeat *or* a deadline exists, so a CI run with no
920
+ TTY — precisely where a budget matters — gets the same stop the terminal
921
+ does. The observer is registered here because this is the one place that
922
+ makes them, so `budget_gap()` cannot miss a pass.
923
+ """
891
924
  if self._progress is None:
892
925
  return None
893
926
  try:
894
- return self._progress(stage, unit)
927
+ watcher = self._progress(stage, unit)
895
928
  except Exception:
896
929
  return None
930
+ if watcher is not None:
931
+ self._rule_watchers.append((stage, watcher))
932
+ return watcher
933
+
934
+ def budget_gap(self) -> dict:
935
+ """What the operator's budget cut out of the rule passes, or `{}`.
936
+
937
+ The two audits report through observers this class made, and each records
938
+ its own cut in the vocabulary `spring-audit` already publishes: families
939
+ that never started (`rules_not_run`) and a family stopped part-way through
940
+ one of its stages (`rules_partially_run`, with `stage_stopped_in`). Read
941
+ back here so the composition can publish the gap rather than a total that
942
+ silently shrank.
943
+
944
+ Only the *rule* passes are read. The composition observer counts defects,
945
+ not rule families, and folding its ids into a rule inventory would be the
946
+ two-populations-under-one-name defect this ledger already carries twice.
947
+ """
948
+ not_run: "list[str]" = []
949
+ partially_run: "list[dict]" = []
950
+ stages: "list[str]" = []
951
+ for stage, watcher in self._rule_watchers:
952
+ if not stage.endswith("rules"):
953
+ continue
954
+ _cut = list(getattr(watcher, "not_run", []) or [])
955
+ _partial = list(getattr(watcher, "partially_run", []) or [])
956
+ if not _cut and not _partial:
957
+ continue
958
+ not_run.extend(_cut)
959
+ partially_run.extend(_partial)
960
+ stages.append(stage)
961
+ if not not_run and not partially_run:
962
+ return {}
963
+ return {
964
+ "rules_not_run": not_run,
965
+ "rules_partially_run": partially_run,
966
+ "stages_stopped_inside": stages,
967
+ }
897
968
 
898
969
  def compose(
899
970
  self,
@@ -1157,6 +1228,7 @@ def _assemble_payload(
1157
1228
  limit: int,
1158
1229
  min_band: str,
1159
1230
  profiles: "Optional[set[str]]",
1231
+ budget_cut: "Optional[dict]" = None,
1160
1232
  ) -> dict:
1161
1233
  """The `risk` answer document for `rows`. One construction, two callers.
1162
1234
 
@@ -1199,6 +1271,21 @@ def _assemble_payload(
1199
1271
  # that sentence, and it belongs where the composition is, not one command away.
1200
1272
  "non_coverage": non_coverage.block("security_surface"),
1201
1273
  }
1274
+ # C3-87 (and C2-31's rule, applied to the second consumer): the counts above
1275
+ # are arithmetic over what was composed, and on a cut run that is a truncated
1276
+ # population. The mark goes where the numbers are — a reader who never opens
1277
+ # `_partial` must not read `total_defects` as a census. A complete run gains
1278
+ # no fields and stays byte-identical.
1279
+ if budget_cut:
1280
+ payload["partial"] = True
1281
+ payload["counts_are_floor"] = True
1282
+ payload["counts_basis"] = (
1283
+ "This run was cut short by the analysis budget, so every count above "
1284
+ "is a FLOOR over what did run — the absence of a risk composed from a "
1285
+ "rule family that never ran is not evidence there is none, and a "
1286
+ "composition stopped part-way leaves defects unranked rather than "
1287
+ "unfound. `_partial` names what was not measured."
1288
+ )
1202
1289
  if len(kept) > limit:
1203
1290
  from sourcecode.degradation import cap_effect
1204
1291
 
@@ -1238,6 +1325,7 @@ def build_risk(
1238
1325
  profiles: "Optional[set[str]]" = None,
1239
1326
  progress: "Optional[Callable[..., Any]]" = None,
1240
1327
  checkpoint: "Optional[Callable[[str, dict], None]]" = None,
1328
+ partial_status: "Optional[Callable[..., dict]]" = None,
1241
1329
  ) -> dict:
1242
1330
  """Compose this build's own audit with reach, access, write effect and shape.
1243
1331
 
@@ -1267,11 +1355,47 @@ def build_risk(
1267
1355
  # C3-73: the join is the other half of this command's wall time, and it has a
1268
1356
  # denominator of its own — one row per defect. Counted with the same seam as
1269
1357
  # the rule passes, so there is one place that knows how to report counted work.
1358
+ # C3-87: what the budget cut out of the two rule passes, read back from the
1359
+ # observers this composition made. Empty on a run that was not cut, which is
1360
+ # every run without `ASK_MAX_ANALYSIS_SECONDS` set.
1361
+ _cut: dict = dict(composer.budget_gap())
1362
+
1270
1363
  def _assemble(_rows: "list[Any]") -> dict:
1271
- return _assemble_payload(
1364
+ payload = _assemble_payload(
1272
1365
  _rows, findings=findings, composer=composer, root=root,
1273
1366
  limit=limit, min_band=min_band, profiles=profiles,
1367
+ budget_cut=_cut or None,
1274
1368
  )
1369
+ if _cut and partial_status is not None:
1370
+ # A phase stopped *inside* is neither completed nor pending: `audit`
1371
+ # when a rule family was cut, `compose` when the join was.
1372
+ _inside = [
1373
+ name
1374
+ for name, hit in (
1375
+ ("audit", bool(_cut.get("rules_not_run"))),
1376
+ ("compose", _cut.get("defects_composed") is not None),
1377
+ )
1378
+ if hit
1379
+ ]
1380
+ payload["_partial"] = partial_status(
1381
+ rules_partially_run=_cut.get("rules_partially_run") or None,
1382
+ rules_not_run=_cut.get("rules_not_run") or None,
1383
+ phases_stopped_inside=_inside or None,
1384
+ )
1385
+ if _cut.get("defects_composed") is not None:
1386
+ # The join is this command's own population, and a run cut inside
1387
+ # it did rank some defects and not others. Neither `phases_completed`
1388
+ # nor `phases_pending` can say that, so it is said here.
1389
+ payload["_partial"]["composition_stopped_after"] = {
1390
+ "defects_composed": _cut["defects_composed"],
1391
+ "defects_total": _cut["defects_total"],
1392
+ "basis": (
1393
+ "The audit ran; the join that ranks its defects was cut. "
1394
+ "The defects not composed were measured and are counted in "
1395
+ "total_defects — they carry no band and are not in `risks`."
1396
+ ),
1397
+ }
1398
+ return payload
1275
1399
 
1276
1400
  if checkpoint is not None:
1277
1401
  # The audit and the posture are paid for; the join has not started. A
@@ -1286,6 +1410,15 @@ def build_risk(
1286
1410
  rows = []
1287
1411
  for _index, defect in enumerate(_defects, start=1):
1288
1412
  if _rows_progress is not None:
1413
+ # C3-87: the join is the other half of this command's wall time, so the
1414
+ # deadline is asked here too — on a whole composed row, which is this
1415
+ # loop's unit, and sampled by the same seam the rule passes use.
1416
+ if _rows_progress.tick(
1417
+ _index - 1, len(_defects), label="composing defects", unit="defects"
1418
+ ):
1419
+ _cut["defects_composed"] = _index - 1
1420
+ _cut["defects_total"] = len(_defects)
1421
+ break
1289
1422
  _rows_progress.rule(_index, len(_defects), str(defect["defect_id"]))
1290
1423
  rows.append(composer.compose(
1291
1424
  defect_id=str(defect["defect_id"]),