sourcecode 4.9.1__tar.gz → 4.10.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 (218) hide show
  1. {sourcecode-4.9.1 → sourcecode-4.10.0}/CHANGELOG.md +28 -0
  2. {sourcecode-4.9.1 → sourcecode-4.10.0}/PKG-INFO +9 -7
  3. {sourcecode-4.9.1 → sourcecode-4.10.0}/README.md +8 -6
  4. {sourcecode-4.9.1 → sourcecode-4.10.0}/pyproject.toml +1 -1
  5. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/audit_report.py +10 -2
  7. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/cache_model.py +6 -0
  8. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/canonical_ir.py +19 -0
  9. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/cli.py +114 -4
  10. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/progress.py +21 -6
  11. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/release_info.py +1 -1
  12. sourcecode-4.10.0/src/sourcecode/runs.py +136 -0
  13. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/security_config_scan.py +55 -0
  14. {sourcecode-4.9.1 → sourcecode-4.10.0}/.github/workflows/build-windows.yml +0 -0
  15. {sourcecode-4.9.1 → sourcecode-4.10.0}/.gitignore +0 -0
  16. {sourcecode-4.9.1 → sourcecode-4.10.0}/.ruff.toml +0 -0
  17. {sourcecode-4.9.1 → sourcecode-4.10.0}/CLAUDE.md +0 -0
  18. {sourcecode-4.9.1 → sourcecode-4.10.0}/CONTRIBUTING.md +0 -0
  19. {sourcecode-4.9.1 → sourcecode-4.10.0}/LICENSE +0 -0
  20. {sourcecode-4.9.1 → sourcecode-4.10.0}/SECURITY.md +0 -0
  21. {sourcecode-4.9.1 → sourcecode-4.10.0}/raw +0 -0
  22. {sourcecode-4.9.1 → sourcecode-4.10.0}/scripts/compare_integration_engines.py +0 -0
  23. {sourcecode-4.9.1 → sourcecode-4.10.0}/scripts/customer_smoke_test.sh +0 -0
  24. {sourcecode-4.9.1 → sourcecode-4.10.0}/scripts/generate_jdk_exports.py +0 -0
  25. {sourcecode-4.9.1 → sourcecode-4.10.0}/scripts/perf_harness.py +0 -0
  26. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/adaptive_scanner.py +0 -0
  27. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/archetype.py +0 -0
  28. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/architectural_baseline.py +0 -0
  29. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/architectural_delta.py +0 -0
  30. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/architecture_analyzer.py +0 -0
  31. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/architecture_summary.py +0 -0
  32. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/ast_extractor.py +0 -0
  33. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/baseline_autocapture.py +0 -0
  34. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/cache.py +0 -0
  35. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/call_surface.py +0 -0
  36. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/caller_metrics.py +0 -0
  37. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/caller_reach.py +0 -0
  38. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/chain_rules.py +0 -0
  39. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/change_plan.py +0 -0
  40. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/cir_graphs.py +0 -0
  41. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/classifier.py +0 -0
  42. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/client_calls.py +0 -0
  43. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  44. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/compare.py +0 -0
  45. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/confidence_analyzer.py +0 -0
  46. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/constraint_diff.py +0 -0
  47. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/context_cache.py +0 -0
  48. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/context_graph.py +0 -0
  49. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/context_scorer.py +0 -0
  50. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/context_summarizer.py +0 -0
  51. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/contract_diff.py +0 -0
  52. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/contract_init.py +0 -0
  53. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/contract_model.py +0 -0
  54. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/contract_pipeline.py +0 -0
  55. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/coverage_parser.py +0 -0
  56. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/data_exposure.py +0 -0
  57. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/data_labels.py +0 -0
  58. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/defect_identity.py +0 -0
  59. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/degradation.py +0 -0
  60. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/dependency_analyzer.py +0 -0
  61. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/deployment_prefix.py +0 -0
  62. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/__init__.py +0 -0
  63. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/base.py +0 -0
  64. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  65. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/dart.py +0 -0
  66. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/dotnet.py +0 -0
  67. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/elixir.py +0 -0
  68. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/go.py +0 -0
  69. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/heuristic.py +0 -0
  70. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/hybrid.py +0 -0
  71. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/java.py +0 -0
  72. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  73. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/nodejs.py +0 -0
  74. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/parsers.py +0 -0
  75. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/php.py +0 -0
  76. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/project.py +0 -0
  77. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/python.py +0 -0
  78. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/ruby.py +0 -0
  79. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/rust.py +0 -0
  80. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/systems.py +0 -0
  81. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/terraform.py +0 -0
  82. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/detectors/tooling.py +0 -0
  83. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/doc_analyzer.py +0 -0
  84. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  85. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/endpoint_literals.py +0 -0
  86. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/endpoint_metrics.py +0 -0
  87. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  88. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/env_analyzer.py +0 -0
  89. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/envelope.py +0 -0
  90. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/environment_resolution.py +0 -0
  91. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/error_schema.py +0 -0
  92. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/evidence_provider.py +0 -0
  93. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/explain.py +0 -0
  94. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/facts/__init__.py +0 -0
  95. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/facts/registry.json +0 -0
  96. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/file_chunker.py +0 -0
  97. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/file_classifier.py +0 -0
  98. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/filter_surface.py +0 -0
  99. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/format_contract.py +0 -0
  100. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/fqn_utils.py +0 -0
  101. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/git_analyzer.py +0 -0
  102. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/git_checkout.py +0 -0
  103. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/graph_analyzer.py +0 -0
  104. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/graph_evidence.py +0 -0
  105. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/hibernate_strat.py +0 -0
  106. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/identity_fallback.py +0 -0
  107. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/integration_coordinates.py +0 -0
  108. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/jdk_exports.py +0 -0
  109. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/license.py +0 -0
  110. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/__init__.py +0 -0
  111. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  112. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  113. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  114. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  115. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  116. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  117. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/registry.py +0 -0
  118. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/runner.py +0 -0
  119. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp/server.py +0 -0
  120. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/mcp_nudge.py +0 -0
  121. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/metrics_analyzer.py +0 -0
  122. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/migrate_check.py +0 -0
  123. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/migration_blast.py +0 -0
  124. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/non_coverage.py +0 -0
  125. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/openapi_surface.py +0 -0
  126. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/openrewrite_recipe.py +0 -0
  127. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/output_budget.py +0 -0
  128. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/parse_cache.py +0 -0
  129. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/path_admission.py +0 -0
  130. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/path_filters.py +0 -0
  131. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/perf.py +0 -0
  132. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/pipe_contract.py +0 -0
  133. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/posture.py +0 -0
  134. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  135. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/pr_impact.py +0 -0
  136. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/prepare_context.py +0 -0
  137. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/provenance.py +0 -0
  138. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/ranking_engine.py +0 -0
  139. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/readiness_timeline.py +0 -0
  140. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/reconciliation.py +0 -0
  141. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/redactor.py +0 -0
  142. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/reference_facts.py +0 -0
  143. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/relevance_scorer.py +0 -0
  144. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/remedies.py +0 -0
  145. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/rename_refactor.py +0 -0
  146. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/repo_classifier.py +0 -0
  147. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/repository_ir.py +0 -0
  148. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/__init__.py +0 -0
  149. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/context.py +0 -0
  150. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/errors.py +0 -0
  151. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/executor.py +0 -0
  152. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/planner.py +0 -0
  153. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/query.py +0 -0
  154. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/request.py +0 -0
  155. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/resolution.py +0 -0
  156. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/result.py +0 -0
  157. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/retriever.py +0 -0
  158. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/runtime.py +0 -0
  159. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/steps.py +0 -0
  160. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  161. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
  162. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
  163. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
  164. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
  165. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  166. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/ris.py +0 -0
  167. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/risk.py +0 -0
  168. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/rule_catalog.py +0 -0
  169. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/runtime_classifier.py +0 -0
  170. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/sarif.py +0 -0
  171. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/scanner.py +0 -0
  172. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/schema.py +0 -0
  173. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  174. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/security_config.py +0 -0
  175. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/security_posture.py +0 -0
  176. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/semantic_analyzer.py +0 -0
  177. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  178. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/semantic_integration_engine.py +0 -0
  179. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/semantic_services.py +0 -0
  180. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/serializer.py +0 -0
  181. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/servlet_surface.py +0 -0
  182. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/source_text.py +0 -0
  183. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_event_topology.py +0 -0
  184. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_findings.py +0 -0
  185. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_impact.py +0 -0
  186. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_model.py +0 -0
  187. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_profiles.py +0 -0
  188. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_properties.py +0 -0
  189. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_security_audit.py +0 -0
  190. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_semantic.py +0 -0
  191. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  192. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/summarizer.py +0 -0
  193. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/target_admission.py +0 -0
  194. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/telemetry/__init__.py +0 -0
  195. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/telemetry/config.py +0 -0
  196. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/telemetry/consent.py +0 -0
  197. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/telemetry/events.py +0 -0
  198. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/telemetry/filters.py +0 -0
  199. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/telemetry/transport.py +0 -0
  200. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/test_gap_ranking.py +0 -0
  201. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/test_sources.py +0 -0
  202. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/token_estimate.py +0 -0
  203. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/tree_utils.py +0 -0
  204. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/type_usage_surface.py +0 -0
  205. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/validation_inference.py +0 -0
  206. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/validation_surface.py +0 -0
  207. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/verify_edit.py +0 -0
  208. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/verify_repo.py +0 -0
  209. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/verify_rules.py +0 -0
  210. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/version_check.py +0 -0
  211. {sourcecode-4.9.1 → sourcecode-4.10.0}/src/sourcecode/workspace.py +0 -0
  212. {sourcecode-4.9.1 → sourcecode-4.10.0}/supabase/functions/README.md +0 -0
  213. {sourcecode-4.9.1 → sourcecode-4.10.0}/supabase/functions/get-license/index.ts +0 -0
  214. {sourcecode-4.9.1 → sourcecode-4.10.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  215. {sourcecode-4.9.1 → sourcecode-4.10.0}/supabase/functions/telemetry/index.ts +0 -0
  216. {sourcecode-4.9.1 → sourcecode-4.10.0}/supabase/sql/license_event_ordering.sql +0 -0
  217. {sourcecode-4.9.1 → sourcecode-4.10.0}/supabase/sql/licensing_schema.sql +0 -0
  218. {sourcecode-4.9.1 → sourcecode-4.10.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.10.0] — 2026-08-05
6
+
7
+ ### Fixed
8
+
9
+ - **Canonical endpoint projection rejects parser-corrupt Java handler names.**
10
+ Reserved Java keywords such as `if` can no longer reach CIR/risk as synthetic
11
+ handler symbols.
12
+ - **`DEAD-001` reports commented Java security annotations.** Java comments that
13
+ contain Spring/Jakarta/custom security annotations are now treated as disabled
14
+ controls, while active annotations stay out of dead-code findings.
15
+ - **`SEC-007` reports JavaMelody descriptor credentials.** `authorized-users`
16
+ values in deployment descriptors are detected and redacted even when they are
17
+ not written as `{SHA-*}` digest literals.
18
+ - **`audit-report` can reuse existing risk evidence.** `--from-risk risk.json`
19
+ packages a previous `ask risk` payload instead of recomputing repo-wide risk.
20
+ - **Long analysis observability improved.** `ASK_PROGRESS=1` enables non-TTY
21
+ heartbeat lines, long commands write durable `.ask/runs` records, and file
22
+ outputs are written atomically.
23
+ - **Large-repo CI budgets are explicit.** `ASK_MAX_ANALYSIS_SECONDS` can fail a
24
+ repo-wide/deep command before work starts when the configured budget is below
25
+ the command's safe floor.
26
+
27
+ ### Documentation
28
+
29
+ - README, user guide and root `ask --help` now distinguish per-symbol,
30
+ inventory and repo-wide/deep command costs instead of treating cache
31
+ benchmarks as universal runtime promises.
32
+
5
33
  ## [4.9.1] — 2026-08-04
6
34
 
7
35
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 4.9.1
3
+ Version: 4.10.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.9.1-blue)
45
+ ![Version](https://img.shields.io/badge/version-4.10.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`**
@@ -54,9 +54,9 @@ Description-Content-Type: text/markdown
54
54
 
55
55
  ## The problem
56
56
 
57
- Every time an AI coding agent starts a new session, it has to re-parse the repository from scratch. For a large Java or TypeScript monolith, that means 5–15 seconds per invocation. Multiply by dozens of agent turns per hour, and repo context acquisition becomes a real bottleneck — not just latency, but tokens, compute, and iteration velocity.
57
+ Every time an AI coding agent starts a new session, it has to re-parse the repository from scratch. On small and mid-sized repos that cost is seconds; on large Java/Spring monoliths, repo-wide semantic composition can be minutes. Multiply that by dozens of agent turns per hour, and repo context acquisition becomes a real bottleneck — not just latency, but tokens, compute, and iteration velocity.
58
58
 
59
- ASK Engine solves this with a persistent structural cache keyed on file content hashes. After the first scan, every subsequent invocation returns pre-built context in milliseconds. The repo doesn't change? The cache doesn't expire.
59
+ ASK Engine solves this with a persistent structural cache keyed on file content hashes. After the first scan, later invocations reuse pre-built context whenever their command can consume the warmed layers. The repo doesn't change? The cache doesn't expire.
60
60
 
61
61
  **The cache is not a performance optimization. It is what makes ASK Engine usable as infrastructure rather than a one-off tool.**
62
62
 
@@ -69,9 +69,11 @@ ASK Engine solves this with a persistent structural cache keyed on file content
69
69
  | Keycloak | 7,885 Java files | 10.5s | 0.6s | **~17x** |
70
70
  | BroadleafCommerce | 2,985 Java files | 2.7s | 0.3s | **~9x** |
71
71
 
72
- Cache keyed on content hashes invalidated only when source changes. On repeated agent sessions against the same codebase, nearly every invocation is a cache hit.
72
+ These numbers are cache-layer benchmarks, not a promise for every command. Per-symbol queries and compact context benefit most. Inventory commands scale with file count. Repo-wide/deep compositions such as `risk`, `audit-report` and large `spring-audit` runs can take minutes on multi-thousand-endpoint repositories and belong in a supervised or nightly job unless CI sets an explicit budget.
73
73
 
74
- At 0.3s per call, ASK Engine becomes **constant infrastructure** inside agent loops — call it before every edit, every PR review, every test run, without batching or caching manually.
74
+ With the right command class, ASK Engine becomes **constant infrastructure** inside agent loops — call the fast/context surfaces before edits and reserve deep compositions for the points where their evidence is worth the runtime.
75
+
76
+ **Large-repo controls.** Set `ASK_PROGRESS=1` to get line heartbeats in non-TTY supervised runs. Set `ASK_MAX_ANALYSIS_SECONDS=<seconds>` to fail before starting a repo-wide/deep command whose configured budget is below its safe floor. Use `ask audit-report --from-risk risk.json` to package an existing risk run instead of recomputing it.
75
77
 
76
78
  **What a warm actually covers.** `ask cache warm` runs the compact analysis: it rebuilds the
77
79
  shared structural layers (L1/L2 + the Repository Intelligence Snapshot + the shared Canonical
@@ -97,7 +99,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
97
99
  # pip / pipx
98
100
  pipx install sourcecode # or: pip install sourcecode
99
101
 
100
- ask version # ask 4.9.1 — and, on a build that has aged,
102
+ ask version # ask 4.10.0 — and, on a build that has aged,
101
103
  # how many releases have probably shipped since
102
104
  ```
103
105
 
@@ -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.9.1-blue)
7
+ ![Version](https://img.shields.io/badge/version-4.10.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`**
@@ -16,9 +16,9 @@
16
16
 
17
17
  ## The problem
18
18
 
19
- Every time an AI coding agent starts a new session, it has to re-parse the repository from scratch. For a large Java or TypeScript monolith, that means 5–15 seconds per invocation. Multiply by dozens of agent turns per hour, and repo context acquisition becomes a real bottleneck — not just latency, but tokens, compute, and iteration velocity.
19
+ Every time an AI coding agent starts a new session, it has to re-parse the repository from scratch. On small and mid-sized repos that cost is seconds; on large Java/Spring monoliths, repo-wide semantic composition can be minutes. Multiply that by dozens of agent turns per hour, and repo context acquisition becomes a real bottleneck — not just latency, but tokens, compute, and iteration velocity.
20
20
 
21
- ASK Engine solves this with a persistent structural cache keyed on file content hashes. After the first scan, every subsequent invocation returns pre-built context in milliseconds. The repo doesn't change? The cache doesn't expire.
21
+ ASK Engine solves this with a persistent structural cache keyed on file content hashes. After the first scan, later invocations reuse pre-built context whenever their command can consume the warmed layers. The repo doesn't change? The cache doesn't expire.
22
22
 
23
23
  **The cache is not a performance optimization. It is what makes ASK Engine usable as infrastructure rather than a one-off tool.**
24
24
 
@@ -31,9 +31,11 @@ ASK Engine solves this with a persistent structural cache keyed on file content
31
31
  | Keycloak | 7,885 Java files | 10.5s | 0.6s | **~17x** |
32
32
  | BroadleafCommerce | 2,985 Java files | 2.7s | 0.3s | **~9x** |
33
33
 
34
- Cache keyed on content hashes invalidated only when source changes. On repeated agent sessions against the same codebase, nearly every invocation is a cache hit.
34
+ These numbers are cache-layer benchmarks, not a promise for every command. Per-symbol queries and compact context benefit most. Inventory commands scale with file count. Repo-wide/deep compositions such as `risk`, `audit-report` and large `spring-audit` runs can take minutes on multi-thousand-endpoint repositories and belong in a supervised or nightly job unless CI sets an explicit budget.
35
35
 
36
- At 0.3s per call, ASK Engine becomes **constant infrastructure** inside agent loops — call it before every edit, every PR review, every test run, without batching or caching manually.
36
+ With the right command class, ASK Engine becomes **constant infrastructure** inside agent loops — call the fast/context surfaces before edits and reserve deep compositions for the points where their evidence is worth the runtime.
37
+
38
+ **Large-repo controls.** Set `ASK_PROGRESS=1` to get line heartbeats in non-TTY supervised runs. Set `ASK_MAX_ANALYSIS_SECONDS=<seconds>` to fail before starting a repo-wide/deep command whose configured budget is below its safe floor. Use `ask audit-report --from-risk risk.json` to package an existing risk run instead of recomputing it.
37
39
 
38
40
  **What a warm actually covers.** `ask cache warm` runs the compact analysis: it rebuilds the
39
41
  shared structural layers (L1/L2 + the Repository Intelligence Snapshot + the shared Canonical
@@ -59,7 +61,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
59
61
  # pip / pipx
60
62
  pipx install sourcecode # or: pip install sourcecode
61
63
 
62
- ask version # ask 4.9.1 — and, on a build that has aged,
64
+ ask version # ask 4.10.0 — and, on a build that has aged,
63
65
  # how many releases have probably shipped since
64
66
  ```
65
67
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "4.9.1"
7
+ version = "4.10.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.9.1"
7
+ __version__ = "4.10.0"
@@ -39,13 +39,19 @@ def build_audit_report(
39
39
  profiles: Optional[set[str]] = None,
40
40
  sign_key: Optional[bytes] = None,
41
41
  risk_limit: int = 10,
42
+ risk_payload: Optional[dict[str, Any]] = None,
42
43
  ) -> dict[str, Any]:
43
44
  """Build the buyer-readable audit bundle from existing command payloads."""
44
45
  from sourcecode.posture import build_posture
45
- from sourcecode.risk import build_risk
46
46
 
47
47
  root = Path(root).resolve()
48
- risk = build_risk(root, limit=risk_limit, min_band="low", profiles=profiles)
48
+ if risk_payload is None:
49
+ from sourcecode.risk import build_risk
50
+ risk = build_risk(root, limit=risk_limit, min_band="low", profiles=profiles)
51
+ risk_source = "computed"
52
+ else:
53
+ risk = risk_payload
54
+ risk_source = "from-risk"
49
55
  posture = build_posture(root, profiles or set())
50
56
  top_risks = risk.get("risks", [])[:risk_limit]
51
57
  access = ((posture.get("endpoints") or {}).get("effective_access") or {})
@@ -58,6 +64,7 @@ def build_audit_report(
58
64
  **({"profile_set": sorted(profiles)} if profiles else {}),
59
65
  "summary": {
60
66
  "risk_model": risk.get("model"),
67
+ "risk_source": risk_source,
61
68
  "total_defects": risk.get("total_defects"),
62
69
  "total_findings": risk.get("total_findings"),
63
70
  "risk_bands": risk.get("by_band", {}),
@@ -86,6 +93,7 @@ def build_audit_report(
86
93
  inputs={
87
94
  "profiles": sorted(profiles) if profiles else None,
88
95
  "risk_limit": risk_limit,
96
+ "risk_source": risk_source,
89
97
  "signed": bool(sign_key),
90
98
  },
91
99
  )
@@ -144,6 +144,12 @@ COMMANDS: tuple[CommandCache, ...] = (
144
144
  "repository side, so what it buys is what it buys `risk`.",
145
145
  "not measured on the battery yet — bounded by the `risk` composition, plus "
146
146
  "reading one JSON file"),
147
+ CommandCache("audit-report", ("cir", "parse"), "shared", False,
148
+ "Packages `risk` and `posture` evidence. A warm helps the repository side; "
149
+ "`--from-risk risk.json` skips risk recomputation entirely and only builds "
150
+ "the posture/report projection.",
151
+ "not measured on the battery yet — bounded by `risk` + `posture`, or by "
152
+ "`posture` when `--from-risk` is supplied"),
147
153
  CommandCache("migrate-recipe", ("parse",), "shared", False,
148
154
  "Runs the same scan as `migrate-check` and projects its findings into an "
149
155
  "OpenRewrite recipe, so it buys exactly what a warm buys `migrate-check`: "
@@ -43,6 +43,23 @@ _REVERSE_EXCLUDE: frozenset[str] = frozenset({"annotated_with", "mapped_to"})
43
43
  # the CIR endpoint count reconciles with the `endpoints` command). See BUG #7.
44
44
  _FQN_PATH_RE: re.Pattern = re.compile(r"/(org|com|net|io|edu)\.[a-z][a-z0-9]*\.[a-zA-Z]")
45
45
 
46
+ _JAVA_RESERVED_HANDLER_NAMES: frozenset[str] = frozenset({
47
+ "if", "else", "for", "while", "do", "switch", "case", "break", "continue",
48
+ "return", "new", "throw", "try", "catch", "finally", "instanceof",
49
+ "this", "super", "void", "class", "interface", "enum", "extends", "implements",
50
+ "import", "package", "static", "final", "abstract", "synchronized", "native",
51
+ "true", "false", "null",
52
+ })
53
+
54
+
55
+ def _route_has_reserved_handler(route: dict) -> bool:
56
+ """Reject parser-corrupt endpoint symbols such as ``pkg.Controller#if``."""
57
+ symbol = str(route.get("symbol") or "")
58
+ if "#" not in symbol:
59
+ return False
60
+ handler = symbol.rsplit("#", 1)[1].split("(", 1)[0].strip()
61
+ return handler in _JAVA_RESERVED_HANDLER_NAMES
62
+
46
63
 
47
64
  # ---------------------------------------------------------------------------
48
65
  # CanonicalSecurity
@@ -438,6 +455,8 @@ def ir_dict_to_canonical(
438
455
  for r in route_surface:
439
456
  if r.get("scope") == "test_util":
440
457
  continue
458
+ if _route_has_reserved_handler(r):
459
+ continue
441
460
  if _FQN_PATH_RE.search(r.get("path", "") or ""):
442
461
  continue
443
462
  ep = _route_to_canonical_endpoint(r)
@@ -338,8 +338,10 @@ def _build_help_text() -> str:
338
338
  Deterministic Java/Spring semantics and reusable structural context for AI coding agents.
339
339
 
340
340
  Cache warms on first scan; later calls reuse pre-built context instead of rescanning.
341
- Scan and warm time scale with repo size small repos in seconds, large repos (thousands
342
- of files) in minutes. Semantic analysis itself is sub-second; repo indexing dominates.
341
+ Performance depends on command class and repo size: per-symbol queries are the fast path,
342
+ inventory commands scale with files, and repo-wide/deep compositions can take
343
+ minutes on multi-thousand-endpoint repositories. Use deep jobs nightly or with
344
+ ASK_MAX_ANALYSIS_SECONDS/ASK_PROGRESS when CI needs an explicit budget.
343
345
 
344
346
  [bold]Start here — Java/Spring analysis:[/bold]
345
347
  posture . --diff dev:prod [dim]# effective access, two profile sets (exp.)[/dim]
@@ -881,9 +883,24 @@ def _enforce_format(command: str, fmt: str) -> None:
881
883
 
882
884
 
883
885
  def _safe_write_file(path: "Path", content: str) -> None:
884
- """Write content to path, emitting a clean JSON error on I/O failure."""
886
+ """Atomically write content to path, emitting a clean JSON error on I/O failure."""
885
887
  try:
886
- path.write_text(content, encoding="utf-8")
888
+ path = Path(path)
889
+ path.parent.mkdir(parents=True, exist_ok=True)
890
+ tmp = path.with_name(f".{path.name}.tmp-{os.getpid()}-{time.time_ns()}")
891
+ tmp.write_text(content, encoding="utf-8")
892
+ tmp.replace(path)
893
+ try:
894
+ from sourcecode.runs import active_run
895
+ run = active_run()
896
+ if run is not None:
897
+ run.write(
898
+ status="running",
899
+ output_path=path,
900
+ output_size=path.stat().st_size,
901
+ )
902
+ except Exception:
903
+ pass
887
904
  except OSError as _exc:
888
905
  _emit_error_json(
889
906
  INVALID_INPUT_CODE,
@@ -968,6 +985,50 @@ def _active_command_context() -> "tuple[str, Optional[Path]]":
968
985
 
969
986
 
970
987
  _EXPENSIVE_ANALYSIS_TTL_SECONDS = 6 * 60 * 60
988
+ _ANALYSIS_CLASSES: dict[str, tuple[str, int, str]] = {
989
+ "spring-audit": ("repo-wide", 600, "use nightly/deep tier on large repositories"),
990
+ "risk": ("deep", 1200, "run `ask spring-audit` or a saved `risk.json` first"),
991
+ "audit-report": ("deep", 1200, "reuse `--from-risk risk.json` or schedule nightly"),
992
+ "verify --init": ("repo-wide", 600, "derive contracts outside pre-commit"),
993
+ "verify": ("core", 60, "run with a larger CI budget or narrow the contract set"),
994
+ "migrate-check": ("repo-wide", 600, "run inventory in CI and deep migration checks nightly"),
995
+ }
996
+
997
+
998
+ def _analysis_budget(command: str) -> dict[str, Any]:
999
+ cls, recommended, fallback = _ANALYSIS_CLASSES.get(
1000
+ command, ("repo-wide", 600, "run this command in a supervised or nightly job")
1001
+ )
1002
+ configured: Optional[float] = None
1003
+ raw = os.environ.get("ASK_MAX_ANALYSIS_SECONDS")
1004
+ if raw:
1005
+ try:
1006
+ configured = float(raw)
1007
+ except ValueError:
1008
+ configured = None
1009
+ return {
1010
+ "class": cls,
1011
+ "recommended_min_seconds": recommended,
1012
+ "configured_max_seconds": configured,
1013
+ "fallback": fallback,
1014
+ }
1015
+
1016
+
1017
+ def _enforce_analysis_budget(command: str, budget: dict[str, Any]) -> None:
1018
+ configured = budget.get("configured_max_seconds")
1019
+ recommended = float(budget.get("recommended_min_seconds") or 0)
1020
+ if configured is None or float(configured) >= recommended:
1021
+ return
1022
+ _emit_error_json(
1023
+ INVALID_INPUT_CODE,
1024
+ (
1025
+ f"{command} is a {budget.get('class')} analysis and the configured "
1026
+ f"budget ({configured:g}s) is below its safe floor ({recommended:g}s)."
1027
+ ),
1028
+ hint=str(budget.get("fallback") or "raise ASK_MAX_ANALYSIS_SECONDS"),
1029
+ expected=f"ASK_MAX_ANALYSIS_SECONDS >= {recommended:g}",
1030
+ )
1031
+ raise typer.Exit(code=2)
971
1032
 
972
1033
 
973
1034
  def _analysis_lock_path(path: Path) -> Path:
@@ -1051,8 +1112,14 @@ def _emit_analysis_contention_warning(command: str, path: Path, phase: str) -> N
1051
1112
  @contextlib.contextmanager
1052
1113
  def _expensive_analysis_scope(command: str, path: Path, phase: str):
1053
1114
  """Register a long repository-wide analysis and warn on concurrent runs."""
1115
+ from sourcecode.runs import RunRecord, reset_active_run, set_active_run
1116
+
1054
1117
  lock_path = _analysis_lock_path(path)
1055
1118
  _emit_analysis_contention_warning(command, path, phase)
1119
+ budget = _analysis_budget(command)
1120
+ _enforce_analysis_budget(command, budget)
1121
+ run_record = RunRecord.start(path, command, phase, budget=budget)
1122
+ run_token = set_active_run(run_record)
1056
1123
  token = f"{os.getpid()}:{time.time_ns()}"
1057
1124
  owns_lock = False
1058
1125
  if _active_analysis_record(lock_path) is None:
@@ -1077,6 +1144,12 @@ def _expensive_analysis_scope(command: str, path: Path, phase: str):
1077
1144
  owns_lock = False
1078
1145
  try:
1079
1146
  yield
1147
+ if run_record is not None:
1148
+ run_record.write(status="complete", exit_code=0)
1149
+ except BaseException as exc:
1150
+ if run_record is not None:
1151
+ run_record.write(status="failed", exit_code=1, error=str(exc))
1152
+ raise
1080
1153
  finally:
1081
1154
  if owns_lock:
1082
1155
  try:
@@ -1088,6 +1161,7 @@ def _expensive_analysis_scope(command: str, path: Path, phase: str):
1088
1161
  lock_path.unlink()
1089
1162
  except OSError:
1090
1163
  pass
1164
+ reset_active_run(run_token)
1091
1165
 
1092
1166
 
1093
1167
  def _emit_command_output(
@@ -8026,6 +8100,11 @@ def audit_report_cmd(
8026
8100
  format: str = typer.Option(
8027
8101
  "json", "--format", "-f", help="Output format: json, yaml or markdown."
8028
8102
  ),
8103
+ from_risk: Optional[Path] = typer.Option(
8104
+ None,
8105
+ "--from-risk",
8106
+ help="Reuse an existing `ask risk -o risk.json` payload instead of recomputing risk.",
8107
+ ),
8029
8108
  copy: bool = _copy_option(),
8030
8109
  ) -> None:
8031
8110
  """[EXPERIMENTAL] Human audit bundle over existing ASK evidence, optionally signed.
@@ -8044,6 +8123,7 @@ def audit_report_cmd(
8044
8123
  \b
8045
8124
  Examples:
8046
8125
  ask audit-report .
8126
+ ask risk . -o risk.json && ask audit-report . --from-risk risk.json
8047
8127
  ask audit-report . --profile prod --format markdown
8048
8128
  ask audit-report . --sign-key audit.key -o audit-report.json
8049
8129
  """
@@ -8079,12 +8159,42 @@ def audit_report_cmd(
8079
8159
  expected="a non-empty key file",
8080
8160
  )
8081
8161
  raise typer.Exit(code=1)
8162
+ risk_payload = None
8163
+ if from_risk is not None:
8164
+ risk_path = Path(from_risk).expanduser()
8165
+ if not risk_path.is_file():
8166
+ _emit_error_json(
8167
+ INVALID_INPUT_CODE,
8168
+ f"--from-risk must name a readable risk JSON file (got {str(from_risk)!r}).",
8169
+ hint="Example: ask risk . -o risk.json && ask audit-report . --from-risk risk.json",
8170
+ expected="an existing JSON file produced by `ask risk`",
8171
+ )
8172
+ raise typer.Exit(code=1)
8173
+ try:
8174
+ risk_payload = json.loads(risk_path.read_text(encoding="utf-8"))
8175
+ except Exception as exc:
8176
+ _emit_error_json(
8177
+ INVALID_INPUT_CODE,
8178
+ f"--from-risk is not valid JSON: {exc}",
8179
+ hint="Regenerate it with: ask risk . -o risk.json",
8180
+ expected="a JSON object produced by `ask risk`",
8181
+ )
8182
+ raise typer.Exit(code=1)
8183
+ if not isinstance(risk_payload, dict) or "risks" not in risk_payload:
8184
+ _emit_error_json(
8185
+ INVALID_INPUT_CODE,
8186
+ "--from-risk does not look like an `ask risk` payload.",
8187
+ hint="Regenerate it with: ask risk . -o risk.json",
8188
+ expected="a JSON object with a `risks` array",
8189
+ )
8190
+ raise typer.Exit(code=1)
8082
8191
 
8083
8192
  _prog = Progress()
8084
8193
  _prog.start("packaging audit evidence")
8085
8194
  try:
8086
8195
  data = build_audit_report(
8087
8196
  path, profiles=_profile_set(profile), sign_key=key_bytes,
8197
+ risk_payload=risk_payload,
8088
8198
  )
8089
8199
  finally:
8090
8200
  _prog.stop()
@@ -1,6 +1,7 @@
1
- """Lightweight terminal progress indicator for long-running commands.
1
+ """Lightweight progress indicator for long-running commands.
2
2
 
3
- Writes only to stderr. Zero-cost when stderr is not a TTY or CI is detected.
3
+ Writes only to stderr. Zero-cost when stderr is not a TTY or CI is detected,
4
+ unless ASK_PROGRESS=1 explicitly requests line heartbeats for supervised jobs.
4
5
  Thread-safe; stop() is idempotent.
5
6
  """
6
7
  from __future__ import annotations
@@ -15,6 +16,8 @@ _FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
15
16
 
16
17
 
17
18
  def _terminal_capable() -> bool:
19
+ if os.environ.get("ASK_PROGRESS", "").lower() in ("1", "true", "yes", "on"):
20
+ return True
18
21
  if not sys.stderr.isatty():
19
22
  return False
20
23
  if os.environ.get("TERM") in ("dumb", ""):
@@ -43,6 +46,11 @@ class Progress:
43
46
 
44
47
  def __init__(self) -> None:
45
48
  self._enabled = _terminal_capable()
49
+ self._line_mode = self._enabled and not sys.stderr.isatty()
50
+ try:
51
+ self._interval = max(0.5, float(os.environ.get("ASK_PROGRESS_INTERVAL", "5")))
52
+ except ValueError:
53
+ self._interval = 5.0
46
54
  self._phase = ""
47
55
  self._t0 = time.monotonic()
48
56
  self._lock = threading.Lock()
@@ -81,7 +89,8 @@ class Progress:
81
89
  self._stop_event.set()
82
90
  if self._thread:
83
91
  self._thread.join(timeout=1.0)
84
- sys.stderr.write("\r\033[K")
92
+ if not self._line_mode:
93
+ sys.stderr.write("\r\033[K")
85
94
  sys.stderr.flush()
86
95
  return elapsed
87
96
 
@@ -91,17 +100,23 @@ class Progress:
91
100
  if not self._enabled:
92
101
  return
93
102
  t = f"{elapsed:.1f}s" if elapsed < 60 else f"{elapsed / 60:.1f}m"
94
- sys.stderr.write(f"✓ done ({t})\n")
103
+ prefix = "[ask] " if self._line_mode else "✓ "
104
+ sys.stderr.write(f"{prefix}done ({t})\n")
95
105
  sys.stderr.flush()
96
106
 
97
107
  def _loop(self) -> None:
98
108
  idx = 0
99
- while not self._stop_event.wait(timeout=0.08):
109
+ interval = self._interval if self._line_mode else 0.08
110
+ while not self._stop_event.wait(timeout=interval):
100
111
  frame = _FRAMES[idx % len(_FRAMES)]
101
112
  elapsed = time.monotonic() - self._t0
102
113
  with self._lock:
103
114
  phase = self._phase
104
- line = f"\r{frame} {phase} ({elapsed:.1f}s)"
115
+ if self._line_mode:
116
+ t = f"{elapsed:.1f}s" if elapsed < 60 else f"{elapsed / 60:.1f}m"
117
+ line = f"[ask] progress phase={phase} elapsed={t}\n"
118
+ else:
119
+ line = f"\r{frame} {phase} ({elapsed:.1f}s)"
105
120
  try:
106
121
  sys.stderr.write(line)
107
122
  sys.stderr.flush()
@@ -27,7 +27,7 @@ from typing import Any, Optional
27
27
  #: The date `__version__` was released, from that version's CHANGELOG entry.
28
28
  #: `tests/test_release_info.py` fails the build if the two disagree, so this is
29
29
  #: a copy of a fact rather than a second authority for it.
30
- RELEASE_DATE = "2026-08-04"
30
+ RELEASE_DATE = "2026-08-05"
31
31
 
32
32
  #: Releases per day, measured over the 30 releases before this one (the same
33
33
  #: computation the test re-runs against the CHANGELOG). Stable across window
@@ -0,0 +1,136 @@
1
+ """Durable run records for long ASK analyses."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import time
7
+ import uuid
8
+ from contextvars import ContextVar
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ _ACTIVE_RUN: ContextVar[Optional["RunRecord"]] = ContextVar("ask_active_run", default=None)
14
+
15
+
16
+ def _now() -> float:
17
+ return time.time()
18
+
19
+
20
+ def _runs_dir(root: Path) -> Path:
21
+ return Path(root).resolve() / ".ask" / "runs"
22
+
23
+
24
+ @dataclass
25
+ class RunRecord:
26
+ root: Path
27
+ command: str
28
+ phase: str
29
+ run_id: str
30
+ path: Path
31
+ budget: Optional[dict[str, Any]] = None
32
+
33
+ @classmethod
34
+ def start(
35
+ cls,
36
+ root: Path,
37
+ command: str,
38
+ phase: str,
39
+ budget: Optional[dict[str, Any]] = None,
40
+ ) -> Optional["RunRecord"]:
41
+ try:
42
+ directory = _runs_dir(root)
43
+ directory.mkdir(parents=True, exist_ok=True)
44
+ run_id = f"{int(_now())}-{os.getpid()}-{uuid.uuid4().hex[:8]}"
45
+ path = directory / f"{run_id}.json"
46
+ record = cls(
47
+ root=Path(root).resolve(), command=command, phase=phase,
48
+ run_id=run_id, path=path, budget=budget,
49
+ )
50
+ record.write(status="running")
51
+ return record
52
+ except OSError:
53
+ return None
54
+
55
+ def write(
56
+ self,
57
+ *,
58
+ status: str,
59
+ exit_code: Optional[int] = None,
60
+ output_path: Optional[Path] = None,
61
+ output_size: Optional[int] = None,
62
+ error: Optional[str] = None,
63
+ ) -> None:
64
+ payload: dict[str, Any] = {
65
+ "schema_version": "ask-run-v1",
66
+ "run_id": self.run_id,
67
+ "pid": os.getpid(),
68
+ "command": self.command,
69
+ "phase": self.phase,
70
+ "repo": str(self.root),
71
+ "status": status,
72
+ "heartbeat_at": _now(),
73
+ }
74
+ if self.budget:
75
+ payload["budget"] = self.budget
76
+ if status == "running":
77
+ payload["started_at"] = payload["heartbeat_at"]
78
+ else:
79
+ try:
80
+ previous = json.loads(self.path.read_text(encoding="utf-8"))
81
+ except Exception:
82
+ previous = {}
83
+ payload["started_at"] = previous.get("started_at", payload["heartbeat_at"])
84
+ payload["finished_at"] = payload["heartbeat_at"]
85
+ if output_path is None and previous.get("output_path"):
86
+ output_path = Path(str(previous["output_path"]))
87
+ if output_size is None and previous.get("output_size_bytes") is not None:
88
+ try:
89
+ output_size = int(previous["output_size_bytes"])
90
+ except Exception:
91
+ output_size = None
92
+ if exit_code is not None:
93
+ payload["exit_code"] = exit_code
94
+ if output_path is not None:
95
+ payload["output_path"] = str(output_path)
96
+ if output_size is not None:
97
+ payload["output_size_bytes"] = output_size
98
+ if error:
99
+ payload["error"] = error
100
+ tmp = self.path.with_suffix(".tmp")
101
+ tmp.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
102
+ tmp.replace(self.path)
103
+
104
+
105
+ def classify_run(path: Path) -> dict[str, Any]:
106
+ """Read one run record and classify recovery state."""
107
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
108
+ status = str(data.get("status") or "unknown")
109
+ out = data.get("output_path")
110
+ if status == "running":
111
+ pid = int(data.get("pid") or 0)
112
+ alive = False
113
+ if pid > 0:
114
+ try:
115
+ os.kill(pid, 0)
116
+ alive = True
117
+ except Exception:
118
+ alive = False
119
+ data["recovery_state"] = "running" if alive else "stale"
120
+ elif out and Path(str(out)).exists() and Path(str(out)).stat().st_size == 0:
121
+ data["recovery_state"] = "zero_byte_output"
122
+ else:
123
+ data["recovery_state"] = status
124
+ return data
125
+
126
+
127
+ def set_active_run(record: Optional[RunRecord]):
128
+ return _ACTIVE_RUN.set(record)
129
+
130
+
131
+ def reset_active_run(token: Any) -> None:
132
+ _ACTIVE_RUN.reset(token)
133
+
134
+
135
+ def active_run() -> Optional[RunRecord]:
136
+ return _ACTIVE_RUN.get()