sourcecode 4.10.4__tar.gz → 4.10.5__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 (220) hide show
  1. {sourcecode-4.10.4 → sourcecode-4.10.5}/CHANGELOG.md +64 -0
  2. {sourcecode-4.10.4 → sourcecode-4.10.5}/PKG-INFO +3 -3
  3. {sourcecode-4.10.4 → sourcecode-4.10.5}/README.md +2 -2
  4. {sourcecode-4.10.4 → sourcecode-4.10.5}/pyproject.toml +1 -1
  5. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/cli.py +50 -9
  7. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/pr_impact.py +38 -4
  8. sourcecode-4.10.5/src/sourcecode/progress.py +218 -0
  9. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/repository_ir.py +38 -1
  10. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/security_config_scan.py +30 -22
  11. sourcecode-4.10.5/src/sourcecode/text_input.py +93 -0
  12. sourcecode-4.10.4/src/sourcecode/progress.py +0 -125
  13. {sourcecode-4.10.4 → sourcecode-4.10.5}/.github/workflows/build-windows.yml +0 -0
  14. {sourcecode-4.10.4 → sourcecode-4.10.5}/.gitignore +0 -0
  15. {sourcecode-4.10.4 → sourcecode-4.10.5}/.ruff.toml +0 -0
  16. {sourcecode-4.10.4 → sourcecode-4.10.5}/CLAUDE.md +0 -0
  17. {sourcecode-4.10.4 → sourcecode-4.10.5}/CONTRIBUTING.md +0 -0
  18. {sourcecode-4.10.4 → sourcecode-4.10.5}/LICENSE +0 -0
  19. {sourcecode-4.10.4 → sourcecode-4.10.5}/SECURITY.md +0 -0
  20. {sourcecode-4.10.4 → sourcecode-4.10.5}/raw +0 -0
  21. {sourcecode-4.10.4 → sourcecode-4.10.5}/scripts/compare_integration_engines.py +0 -0
  22. {sourcecode-4.10.4 → sourcecode-4.10.5}/scripts/customer_smoke_test.sh +0 -0
  23. {sourcecode-4.10.4 → sourcecode-4.10.5}/scripts/generate_jdk_exports.py +0 -0
  24. {sourcecode-4.10.4 → sourcecode-4.10.5}/scripts/perf_harness.py +0 -0
  25. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/adaptive_scanner.py +0 -0
  26. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/archetype.py +0 -0
  27. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/architectural_baseline.py +0 -0
  28. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/architectural_delta.py +0 -0
  29. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/architecture_analyzer.py +0 -0
  30. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/architecture_summary.py +0 -0
  31. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/ast_extractor.py +0 -0
  32. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/audit_report.py +0 -0
  33. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/baseline_autocapture.py +0 -0
  34. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/cache.py +0 -0
  35. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/cache_model.py +0 -0
  36. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/call_surface.py +0 -0
  37. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/caller_metrics.py +0 -0
  38. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/caller_reach.py +0 -0
  39. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/canonical_ir.py +0 -0
  40. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/chain_rules.py +0 -0
  41. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/change_plan.py +0 -0
  42. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/cir_graphs.py +0 -0
  43. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/classifier.py +0 -0
  44. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/client_calls.py +0 -0
  45. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/code_notes_analyzer.py +0 -0
  46. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/compare.py +0 -0
  47. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/confidence_analyzer.py +0 -0
  48. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/constraint_diff.py +0 -0
  49. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/context_cache.py +0 -0
  50. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/context_graph.py +0 -0
  51. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/context_scorer.py +0 -0
  52. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/context_summarizer.py +0 -0
  53. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/contract_diff.py +0 -0
  54. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/contract_init.py +0 -0
  55. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/contract_model.py +0 -0
  56. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/contract_pipeline.py +0 -0
  57. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/coverage_parser.py +0 -0
  58. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/data_exposure.py +0 -0
  59. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/data_labels.py +0 -0
  60. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/defect_identity.py +0 -0
  61. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/degradation.py +0 -0
  62. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/dependency_analyzer.py +0 -0
  63. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/deployment_prefix.py +0 -0
  64. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/__init__.py +0 -0
  65. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/base.py +0 -0
  66. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/csproj_parser.py +0 -0
  67. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/dart.py +0 -0
  68. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/dotnet.py +0 -0
  69. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/elixir.py +0 -0
  70. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/go.py +0 -0
  71. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/heuristic.py +0 -0
  72. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/hybrid.py +0 -0
  73. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/java.py +0 -0
  74. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/jvm_ext.py +0 -0
  75. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/nodejs.py +0 -0
  76. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/parsers.py +0 -0
  77. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/php.py +0 -0
  78. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/project.py +0 -0
  79. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/python.py +0 -0
  80. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/ruby.py +0 -0
  81. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/rust.py +0 -0
  82. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/systems.py +0 -0
  83. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/terraform.py +0 -0
  84. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/detectors/tooling.py +0 -0
  85. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/doc_analyzer.py +0 -0
  86. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/dynamic_argument_surface.py +0 -0
  87. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/endpoint_literals.py +0 -0
  88. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/endpoint_metrics.py +0 -0
  89. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/entrypoint_classifier.py +0 -0
  90. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/env_analyzer.py +0 -0
  91. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/envelope.py +0 -0
  92. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/environment_resolution.py +0 -0
  93. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/error_schema.py +0 -0
  94. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/evidence_provider.py +0 -0
  95. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/explain.py +0 -0
  96. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/facts/__init__.py +0 -0
  97. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/facts/registry.json +0 -0
  98. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/file_chunker.py +0 -0
  99. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/file_classifier.py +0 -0
  100. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/filter_surface.py +0 -0
  101. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/format_contract.py +0 -0
  102. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/fqn_utils.py +0 -0
  103. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/git_analyzer.py +0 -0
  104. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/git_checkout.py +0 -0
  105. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/graph_analyzer.py +0 -0
  106. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/graph_evidence.py +0 -0
  107. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/hibernate_strat.py +0 -0
  108. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/identity_fallback.py +0 -0
  109. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/integration_coordinates.py +0 -0
  110. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/jdk_exports.py +0 -0
  111. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/license.py +0 -0
  112. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/__init__.py +0 -0
  113. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  114. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  115. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  116. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  117. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  118. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/orchestrator.py +0 -0
  119. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/registry.py +0 -0
  120. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/runner.py +0 -0
  121. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp/server.py +0 -0
  122. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/mcp_nudge.py +0 -0
  123. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/metrics_analyzer.py +0 -0
  124. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/migrate_check.py +0 -0
  125. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/migration_blast.py +0 -0
  126. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/non_coverage.py +0 -0
  127. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/openapi_surface.py +0 -0
  128. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/openrewrite_recipe.py +0 -0
  129. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/output_budget.py +0 -0
  130. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/parse_cache.py +0 -0
  131. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/path_admission.py +0 -0
  132. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/path_filters.py +0 -0
  133. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/perf.py +0 -0
  134. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/pipe_contract.py +0 -0
  135. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/posture.py +0 -0
  136. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/pr_comment_renderer.py +0 -0
  137. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/prepare_context.py +0 -0
  138. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/provenance.py +0 -0
  139. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/ranking_engine.py +0 -0
  140. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/readiness_timeline.py +0 -0
  141. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/reconciliation.py +0 -0
  142. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/redactor.py +0 -0
  143. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/reference_facts.py +0 -0
  144. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/release_info.py +0 -0
  145. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/relevance_scorer.py +0 -0
  146. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/remedies.py +0 -0
  147. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/rename_refactor.py +0 -0
  148. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/repo_classifier.py +0 -0
  149. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/__init__.py +0 -0
  150. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/context.py +0 -0
  151. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/errors.py +0 -0
  152. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/executor.py +0 -0
  153. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/planner.py +0 -0
  154. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/query.py +0 -0
  155. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/request.py +0 -0
  156. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/resolution.py +0 -0
  157. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/result.py +0 -0
  158. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/retriever.py +0 -0
  159. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/runtime.py +0 -0
  160. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps.py +0 -0
  161. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  162. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_graph.py +0 -0
  163. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_impact.py +0 -0
  164. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_intf.py +0 -0
  165. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_struct.py +0 -0
  166. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  167. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/ris.py +0 -0
  168. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/risk.py +0 -0
  169. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/rule_catalog.py +0 -0
  170. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/runs.py +0 -0
  171. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/runtime_classifier.py +0 -0
  172. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/sarif.py +0 -0
  173. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/scanner.py +0 -0
  174. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/schema.py +0 -0
  175. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  176. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/security_config.py +0 -0
  177. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/security_posture.py +0 -0
  178. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/semantic_analyzer.py +0 -0
  179. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/semantic_impact_engine.py +0 -0
  180. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/semantic_integration_engine.py +0 -0
  181. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/semantic_services.py +0 -0
  182. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/serializer.py +0 -0
  183. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/servlet_surface.py +0 -0
  184. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/source_text.py +0 -0
  185. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_event_topology.py +0 -0
  186. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_findings.py +0 -0
  187. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_impact.py +0 -0
  188. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_model.py +0 -0
  189. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_profiles.py +0 -0
  190. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_properties.py +0 -0
  191. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_security_audit.py +0 -0
  192. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_semantic.py +0 -0
  193. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/spring_tx_analyzer.py +0 -0
  194. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/summarizer.py +0 -0
  195. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/target_admission.py +0 -0
  196. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/__init__.py +0 -0
  197. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/config.py +0 -0
  198. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/consent.py +0 -0
  199. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/events.py +0 -0
  200. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/filters.py +0 -0
  201. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/telemetry/transport.py +0 -0
  202. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/test_gap_ranking.py +0 -0
  203. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/test_sources.py +0 -0
  204. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/token_estimate.py +0 -0
  205. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/tree_utils.py +0 -0
  206. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/type_usage_surface.py +0 -0
  207. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/validation_inference.py +0 -0
  208. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/validation_surface.py +0 -0
  209. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/verify_edit.py +0 -0
  210. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/verify_repo.py +0 -0
  211. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/verify_rules.py +0 -0
  212. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/version_check.py +0 -0
  213. {sourcecode-4.10.4 → sourcecode-4.10.5}/src/sourcecode/workspace.py +0 -0
  214. {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/functions/README.md +0 -0
  215. {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/functions/get-license/index.ts +0 -0
  216. {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  217. {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/functions/telemetry/index.ts +0 -0
  218. {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/sql/license_event_ordering.sql +0 -0
  219. {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/sql/licensing_schema.sql +0 -0
  220. {sourcecode-4.10.4 → sourcecode-4.10.5}/supabase/sql/telemetry_events.sql +0 -0
@@ -2,6 +2,70 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.10.5] — 2026-08-07
6
+
7
+ Second release of the field-evaluation #13 queue. Three rows close and one closes
8
+ in part; the suite is 6 691 tests green.
9
+
10
+ ⚠ **Two behaviour changes to read before upgrading a pipeline.** Both are the fix
11
+ working, and both can move a gate:
12
+
13
+ - On Windows, `pr-impact --files -` now resolves paths it previously mangled, so a
14
+ pipeline that was passing because its diff decoded to nothing can start failing —
15
+ with the verdict the diff always deserved (C3-58).
16
+ - A security finding that existed only in build output (`target/classes`) is no
17
+ longer reported, so a repository audited mid-build can report fewer findings
18
+ (C3-64).
19
+
20
+ ### Performance
21
+
22
+ - **The security configuration scan discovers its files 8.7× faster** (C3-64):
23
+ 0.61 s → 0.07 s on keycloak, 0.45 s → 0.05 s on alfresco. Five `rglob` calls
24
+ became one pruned walk. `rglob` cannot skip a subtree, so the old cost was set by
25
+ the build output and the VCS metadata rather than by the source: 48.4 % of
26
+ openmrs-core's tree entries and 53.3 % of this repository's live under directories
27
+ that hold no source at all. On a clean checkout the end-to-end scan moves 1.73 s →
28
+ 1.43 s (the rest is reading Java sources); the win grows with how built the tree
29
+ is, which is the state a developer's repository is normally in.
30
+
31
+ ### Fixed
32
+
33
+ - **A path list is decoded by its bytes, never by the platform locale** (C3-58).
34
+ `ask pr-impact --files -` read stdin through Python's locale decoding, which on
35
+ Windows is the ANSI codepage: `git diff --name-only | ask pr-impact --files -`
36
+ returned `risk: UNKNOWN` and 0 endpoints where the same list without the shell's
37
+ default byte-order mark returned `CRITICAL` and 1 144 — a green pipeline on the
38
+ gating command. The BOM was the visible half; `AñoService.java` arriving as
39
+ `AñoService.java` was the cause. The new authority `sourcecode/text_input.py`
40
+ reads `sys.stdin.buffer` and lets the bytes name the encoding, and the list-file
41
+ shape goes through it too: PowerShell 5.1 writes UTF-16-LE by default, which the
42
+ previous UTF-8 read raised on. Undecodable bytes now yield an unusable path with
43
+ encoding named as a cause, never a traceback.
44
+ - **No command claims `done` before its answer exists** (C3-54, part 2). The
45
+ completion line moved out of `Progress.finish()` entirely: `finish()` claims the
46
+ work is done, and the line is printed when the answer has actually been handed
47
+ over. A run killed in between prints nothing, and a run that raised prints
48
+ nothing — previously ~20 commands emitted `done (Xs)` while their `--output` file
49
+ did not exist yet, which in the field meant 6.8 minutes of analysis reported as
50
+ success over an absent file. Output files are also durable now: the shared writer
51
+ fsyncs data and directory entry before anything reports a write, and `repo-ir` —
52
+ the one `-o` path that bypassed that writer — goes through it.
53
+ - **A rejected progress write no longer silences progress for the rest of the run**
54
+ (C3-56, part). The heartbeat treated any write failure as terminal; it now retries
55
+ and gives up only after five consecutive failures. The reported 393 s of silence
56
+ did not reproduce: the cadence is timer-driven and measured exact on a cold
57
+ 29.9 s run (5.0 / 10.0 / 15.0 / 20.0 / 25.1 s). The progress denominator and ETA
58
+ the row also asks for are still open.
59
+ - **A build copy is no longer a second security finding** (C3-64). The configuration
60
+ scan walked the whole tree five times and could not skip a subtree, so a compiled
61
+ copy of `application.properties` under `target/classes` was reported beside the
62
+ source file it came from. It now walks once, pruning build output and VCS metadata
63
+ with the same ignore set Java discovery uses.
64
+ - **An unresolved changed file names the right thing to check** (C3-58). One message
65
+ covered both causes and pointed at the wrong one: a path that is *not on disk* is a
66
+ path problem (form and encoding), while a path that *is* there but maps to no class
67
+ is a fact about the file. They now say so separately.
68
+
5
69
  ## [4.10.4] — 2026-08-07
6
70
 
7
71
  First release of the queue opened by field evaluation #13. Four of its rows close,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 4.10.4
3
+ Version: 4.10.5
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.10.4-blue)
45
+ ![Version](https://img.shields.io/badge/version-4.10.5-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`**
@@ -106,7 +106,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
106
106
  # pip / pipx
107
107
  pipx install sourcecode # or: pip install sourcecode
108
108
 
109
- ask version # ask 4.10.4 — and, on a build that has aged,
109
+ ask version # ask 4.10.5 — and, on a build that has aged,
110
110
  # how many releases have probably shipped since
111
111
  ```
112
112
 
@@ -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.10.4-blue)
7
+ ![Version](https://img.shields.io/badge/version-4.10.5-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`**
@@ -68,7 +68,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
68
68
  # pip / pipx
69
69
  pipx install sourcecode # or: pip install sourcecode
70
70
 
71
- ask version # ask 4.10.4 — and, on a build that has aged,
71
+ ask version # ask 4.10.5 — and, on a build that has aged,
72
72
  # how many releases have probably shipped since
73
73
  ```
74
74
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "4.10.4"
7
+ version = "4.10.5"
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.10.4"
7
+ __version__ = "4.10.5"
@@ -18,6 +18,7 @@ from sourcecode import __version__
18
18
  from sourcecode.error_schema import EXECUTION_FAILED_CODE, INVALID_INPUT_CODE, build_error_envelope
19
19
  from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
20
20
  from sourcecode.progress import Progress
21
+ from sourcecode.progress import output_emitted as _progress_output_emitted
21
22
  from sourcecode import perf
22
23
  from sourcecode.caller_metrics import (
23
24
  CALLER_METRIC_RECONCILIATION,
@@ -888,8 +889,27 @@ def _safe_write_file(path: "Path", content: str) -> None:
888
889
  path = Path(path)
889
890
  path.parent.mkdir(parents=True, exist_ok=True)
890
891
  tmp = path.with_name(f".{path.name}.tmp-{os.getpid()}-{time.time_ns()}")
891
- tmp.write_text(content, encoding="utf-8")
892
+ # C3-54 part 2: the rename is atomic, the bytes were not durable. A
893
+ # process torn down between `write_text` and the OS flushing its page
894
+ # cache leaves a file that exists at full length and holds nothing —
895
+ # the same total loss the false `done` line reported as success, one
896
+ # layer down. fsync the data before the rename, then the directory
897
+ # entry, so a run that reports an output file has one.
898
+ with open(tmp, "w", encoding="utf-8") as fh:
899
+ fh.write(content)
900
+ fh.flush()
901
+ os.fsync(fh.fileno())
892
902
  tmp.replace(path)
903
+ try:
904
+ dir_fd = os.open(str(path.parent), os.O_RDONLY)
905
+ try:
906
+ os.fsync(dir_fd)
907
+ finally:
908
+ os.close(dir_fd)
909
+ except (OSError, AttributeError):
910
+ # Directory fsync is POSIX-only; on Windows the rename is already
911
+ # ordered against the data write. Never fail a good run over it.
912
+ pass
893
913
  try:
894
914
  from sourcecode.runs import active_run
895
915
  run = active_run()
@@ -1213,6 +1233,9 @@ def _emit_command_output(
1213
1233
  # The payload is on stdout here, so this cannot join it; it is a
1214
1234
  # notice and takes the terminal gate rather than dirtying a capture.
1215
1235
  _notice("✓ copied to clipboard")
1236
+ # The answer has been handed over. Any `done (Xs)` a command deferred is now
1237
+ # a true claim and prints here — never before this line (C3-54).
1238
+ _progress_output_emitted()
1216
1239
 
1217
1240
 
1218
1241
  def _emit_text_output(text: str, copy: bool) -> None:
@@ -1228,6 +1251,7 @@ def _emit_text_output(text: str, copy: bool) -> None:
1228
1251
  typer.echo(text)
1229
1252
  if copy and _copy_to_clipboard(text):
1230
1253
  _notice("✓ copied to clipboard")
1254
+ _progress_output_emitted()
1231
1255
 
1232
1256
 
1233
1257
  class _TextAnswer:
@@ -1257,6 +1281,7 @@ class _TextAnswer:
1257
1281
  def done(self, copy: bool) -> None:
1258
1282
  if copy and self._lines and _copy_to_clipboard(self.text):
1259
1283
  _notice("✓ copied to clipboard")
1284
+ _progress_output_emitted()
1260
1285
 
1261
1286
 
1262
1287
  # H-06: Intercept Click-level UsageError (unknown options, bad args) and emit JSON.
@@ -4825,8 +4850,15 @@ def repo_ir_cmd(
4825
4850
  size_bytes = len(raw_bytes)
4826
4851
  if gzip_output:
4827
4852
  import gzip as _gzip
4853
+ # Same durability rule as `_safe_write_file`: the bytes are on disk
4854
+ # before anything says they are (C3-54).
4828
4855
  with _gzip.open(output_path, "wb") as _gz:
4829
4856
  _gz.write(raw_bytes)
4857
+ _gz.flush()
4858
+ try:
4859
+ os.fsync(_gz.fileno())
4860
+ except (OSError, AttributeError):
4861
+ pass
4830
4862
  compressed_kb = output_path.stat().st_size // 1024
4831
4863
  size_kb = size_bytes // 1024
4832
4864
  # C3-26: when the payload went to a file, this confirmation IS the
@@ -4837,7 +4869,9 @@ def repo_ir_cmd(
4837
4869
  f"IR written to {output_path} ({compressed_kb}KB gzip, {size_kb}KB uncompressed)"
4838
4870
  )
4839
4871
  else:
4840
- output_path.write_bytes(raw_bytes)
4872
+ # Was a bare `write_bytes`: neither atomic nor durable, and the only
4873
+ # `-o` write in the CLI that skipped the shared seam (C3-54).
4874
+ _safe_write_file(output_path, output)
4841
4875
  size_kb = size_bytes // 1024
4842
4876
  if summary_only:
4843
4877
  typer.echo(
@@ -4850,6 +4884,7 @@ def repo_ir_cmd(
4850
4884
  f"IR written to {output_path} "
4851
4885
  f"({size_kb}KB, {n_nodes} nodes, {n_edges} edges)"
4852
4886
  )
4887
+ _progress_output_emitted()
4853
4888
  else:
4854
4889
  if gzip_output:
4855
4890
  _emit_error_json(
@@ -9461,18 +9496,21 @@ def pr_impact_cmd(
9461
9496
  # --files accepts three shapes: a list file, '-' for stdin, or an inline
9462
9497
  # comma-separated list. The list-file shape was the only documented one and
9463
9498
  # the only one the error hint mentioned.
9464
- _files_raw = str(files).strip()
9499
+ # Both byte-carrying shapes decode through `text_input`: stdin and list files
9500
+ # arrive as somebody else's shell wrote them, and the locale codepage is not
9501
+ # evidence of their encoding (C3-58).
9502
+ from sourcecode import text_input as _text_input
9503
+
9504
+ _files_raw = str(files).replace(_text_input.BOM_CHAR, "").strip()
9465
9505
  if _files_raw == "-":
9466
- changed_files = [ln.strip() for ln in sys.stdin.read().splitlines() if ln.strip()]
9506
+ changed_files = _text_input.parse_path_lines(_text_input.read_stdin_text())
9467
9507
  _files_desc = "stdin"
9468
9508
  else:
9469
9509
  _files_path = Path(_files_raw)
9470
9510
  if _files_path.is_file():
9471
- changed_files = [
9472
- line.strip()
9473
- for line in _files_path.read_text(encoding="utf-8").splitlines()
9474
- if line.strip()
9475
- ]
9511
+ changed_files = _text_input.parse_path_lines(
9512
+ _text_input.read_text_file(_files_path)
9513
+ )
9476
9514
  _files_desc = str(_files_path)
9477
9515
  elif all(
9478
9516
  p.strip().lower().endswith(".java")
@@ -10309,6 +10347,9 @@ def modernize_cmd(
10309
10347
  sys.stdout.buffer.flush()
10310
10348
  except AttributeError:
10311
10349
  sys.stdout.write(output + "\n")
10350
+ # This branch predates `_emit_command_output`, so it also has to release the
10351
+ # deferred completion line itself (C3-54).
10352
+ _progress_output_emitted()
10312
10353
 
10313
10354
  if copy:
10314
10355
  _copy_to_clipboard(output)
@@ -228,6 +228,43 @@ def _suffix_candidates(norm: str, file_class_index: dict[str, list[str]]) -> lis
228
228
  return shorter_given or longer_given
229
229
 
230
230
 
231
+ def _exists_on_disk(norm: str, root: Path) -> bool:
232
+ """True when the given path names a file that is actually there."""
233
+ try:
234
+ candidate = Path(norm)
235
+ if candidate.is_absolute():
236
+ return candidate.is_file()
237
+ return (root / norm).is_file()
238
+ except OSError:
239
+ # A path the OS refuses to even stat (illegal characters after a bad
240
+ # decode, a name past MAX_PATH) is not on disk as far as we can tell.
241
+ return False
242
+
243
+
244
+ def _unresolved_warning(path_str: str, norm: str, root: Path) -> str:
245
+ """Name the right thing to check for a changed file that mapped to nothing.
246
+
247
+ The two causes need opposite investigations and the single old message sent
248
+ every operator to the wrong one. A path that is *not there* is a path
249
+ problem — the field case was a BOM and cp1252 mojibake reaching us through
250
+ stdin (C3-58), and the message said "not in CIR", so the operator went
251
+ looking at analysis coverage. A path that *is* there carries no class
252
+ symbols, which is a fact about the file.
253
+ """
254
+ if _exists_on_disk(norm, root):
255
+ return (
256
+ f"No Spring classes found for '{path_str}' — the file exists but "
257
+ "contributes no class symbols to the CIR (interface-free package-info, "
258
+ "an empty file, or a file excluded from analysis)."
259
+ )
260
+ return (
261
+ f"No file at '{path_str}' under the analyzed repository — nothing was "
262
+ "matched, and this is a path problem, not an analysis gap. Check the path "
263
+ "form (relative to the repo root) and its encoding: a byte-order mark or a "
264
+ "non-UTF-8 pipe corrupts accented names."
265
+ )
266
+
267
+
231
268
  def _resolve_changed_files(
232
269
  file_list: list[str],
233
270
  file_class_index: dict[str, list[str]],
@@ -293,10 +330,7 @@ def _resolve_changed_files(
293
330
 
294
331
  if not candidates:
295
332
  unresolved_java.append(norm)
296
- warnings.append(
297
- f"No Spring classes found for '{path_str}' — "
298
- "file not in CIR or has no class symbols."
299
- )
333
+ warnings.append(_unresolved_warning(path_str, norm, root))
300
334
 
301
335
  for cls in candidates:
302
336
  if cls not in seen_classes:
@@ -0,0 +1,218 @@
1
+ """Lightweight progress indicator for long-running commands.
2
+
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.
5
+ Thread-safe; stop() is idempotent.
6
+
7
+ `done (Xs)` is a claim, and C3-54 is what happens when it is made early: the
8
+ field log printed `[ask] done (6.8m)` and the output directory held no
9
+ `f3-audit.json`, because the completion line was emitted when the *analysis*
10
+ finished — before the render and the write — and the process was torn down in
11
+ that window. 6.8 minutes of CPU, zero recoverable bytes, reported as success.
12
+
13
+ So the line is not printed where `finish()` is called. `finish()` claims the
14
+ work is done; the line is emitted when the **answer has actually been handed
15
+ over**, which the CLI signals through `output_emitted()` at its output seams. A
16
+ run that is killed between the two prints nothing, which is the honest outcome:
17
+ no answer, no completion line.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import sys
23
+ import threading
24
+ import time
25
+ from typing import Optional
26
+
27
+ _FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
28
+
29
+ #: Consecutive failed heartbeat writes before the loop gives up on the stream.
30
+ _MAX_WRITE_FAILURES = 5
31
+
32
+ # --- Completion registry ----------------------------------------------------
33
+ # Module state, not per-instance: the emitter of the answer (the CLI output
34
+ # seam) and the owner of the completion line (a Progress somewhere up the call
35
+ # stack) never see each other.
36
+ _registry_lock = threading.Lock()
37
+ _output_events = 0
38
+ _pending_done: "list[Progress]" = []
39
+
40
+
41
+ def output_emitted() -> None:
42
+ """The command's answer has been written. Release any deferred `done` line.
43
+
44
+ Called from every CLI output seam. Safe to call when no Progress is running
45
+ and safe to call more than once per run.
46
+ """
47
+ global _output_events
48
+ with _registry_lock:
49
+ _output_events += 1
50
+ released = list(_pending_done)
51
+ _pending_done.clear()
52
+ for prog in released:
53
+ prog._print_done()
54
+
55
+
56
+ def _reset_for_tests() -> None:
57
+ """Drop registry state between in-process test runs."""
58
+ global _output_events
59
+ with _registry_lock:
60
+ _output_events = 0
61
+ _pending_done.clear()
62
+
63
+
64
+ def _terminal_capable() -> bool:
65
+ if os.environ.get("ASK_PROGRESS", "").lower() in ("1", "true", "yes", "on"):
66
+ return True
67
+ if not sys.stderr.isatty():
68
+ return False
69
+ if os.environ.get("TERM") in ("dumb", ""):
70
+ return False
71
+ if os.environ.get("NO_COLOR"):
72
+ return False
73
+ if os.environ.get("CI"):
74
+ return False
75
+ return True
76
+
77
+
78
+ class Progress:
79
+ """In-place spinner on stderr.
80
+
81
+ Usage::
82
+
83
+ p = Progress()
84
+ p.start("scanning files")
85
+ ...
86
+ p.update("extracting contracts")
87
+ ...
88
+ p.finish() # clears line, prints "✓ done (3.2s)"
89
+
90
+ Always call stop() or finish() — both are idempotent.
91
+ """
92
+
93
+ def __init__(self) -> None:
94
+ self._enabled = _terminal_capable()
95
+ self._line_mode = self._enabled and not sys.stderr.isatty()
96
+ try:
97
+ self._interval = max(0.5, float(os.environ.get("ASK_PROGRESS_INTERVAL", "5")))
98
+ except ValueError:
99
+ self._interval = 5.0
100
+ self._phase = ""
101
+ self._t0 = time.monotonic()
102
+ self._lock = threading.Lock()
103
+ self._stop_event = threading.Event()
104
+ self._thread: Optional[threading.Thread] = None
105
+ self._stopped = False
106
+ self._done_printed = False
107
+ self._outputs_at_start = 0
108
+ self._elapsed_at_finish = 0.0
109
+ self._write_failures = 0
110
+
111
+ @property
112
+ def elapsed(self) -> float:
113
+ return time.monotonic() - self._t0
114
+
115
+ def start(self, phase: str = "initializing") -> "Progress":
116
+ self._t0 = time.monotonic()
117
+ with self._lock:
118
+ self._phase = phase
119
+ self._stopped = False
120
+ self._done_printed = False
121
+ with _registry_lock:
122
+ self._outputs_at_start = _output_events
123
+ if not self._enabled:
124
+ return self
125
+ self._stop_event.clear()
126
+ self._thread = threading.Thread(target=self._loop, daemon=True)
127
+ self._thread.start()
128
+ return self
129
+
130
+ def update(self, phase: str) -> None:
131
+ with self._lock:
132
+ self._phase = phase
133
+
134
+ def stop(self) -> float:
135
+ """Stop and clear spinner. Returns elapsed seconds. Idempotent."""
136
+ elapsed = self.elapsed
137
+ if self._stopped:
138
+ return elapsed
139
+ self._stopped = True
140
+ if not self._enabled:
141
+ return elapsed
142
+ self._stop_event.set()
143
+ if self._thread:
144
+ self._thread.join(timeout=1.0)
145
+ if not self._line_mode:
146
+ sys.stderr.write("\r\033[K")
147
+ sys.stderr.flush()
148
+ return elapsed
149
+
150
+ def finish(self) -> None:
151
+ """Stop the spinner and claim completion — as soon as that is true.
152
+
153
+ If the command has already handed over its answer (the `spring-audit`
154
+ shape: `finish()` called after the write), the line prints here. If it
155
+ has not, the line waits for `output_emitted()`. It is never printed on a
156
+ run that dies in between, and never on a run that raised: `finally:
157
+ finish()` on an exception path leaves a claim pending that nothing
158
+ releases (C3-54).
159
+ """
160
+ self._elapsed_at_finish = self.stop()
161
+ if not self._enabled:
162
+ return
163
+ with _registry_lock:
164
+ if _output_events > self._outputs_at_start:
165
+ already_emitted = True
166
+ else:
167
+ already_emitted = False
168
+ if self not in _pending_done:
169
+ _pending_done.append(self)
170
+ if already_emitted:
171
+ self._print_done()
172
+
173
+ def _print_done(self) -> None:
174
+ """Write the completion line once, for an answer that now exists."""
175
+ if self._done_printed or not self._enabled:
176
+ return
177
+ self._done_printed = True
178
+ elapsed = getattr(self, "_elapsed_at_finish", self.elapsed)
179
+ t = f"{elapsed:.1f}s" if elapsed < 60 else f"{elapsed / 60:.1f}m"
180
+ prefix = "[ask] " if self._line_mode else "✓ "
181
+ try:
182
+ sys.stderr.write(f"{prefix}done ({t})\n")
183
+ sys.stderr.flush()
184
+ except Exception:
185
+ pass
186
+
187
+ def _loop(self) -> None:
188
+ idx = 0
189
+ interval = self._interval if self._line_mode else 0.08
190
+ while not self._stop_event.wait(timeout=interval):
191
+ frame = _FRAMES[idx % len(_FRAMES)]
192
+ elapsed = time.monotonic() - self._t0
193
+ with self._lock:
194
+ phase = self._phase
195
+ if self._line_mode:
196
+ t = f"{elapsed:.1f}s" if elapsed < 60 else f"{elapsed / 60:.1f}m"
197
+ line = f"[ask] progress phase={phase} elapsed={t}\n"
198
+ else:
199
+ line = f"\r{frame} {phase} ({elapsed:.1f}s)"
200
+ try:
201
+ sys.stderr.write(line)
202
+ sys.stderr.flush()
203
+ self._write_failures = 0
204
+ except ValueError:
205
+ # The stream is closed — the run is ending. Nothing to retry.
206
+ break
207
+ except Exception:
208
+ # C3-56: a single failed write used to end the heartbeat for the
209
+ # rest of the run, and the operator's only remaining way to tell
210
+ # work from a hang was reading the worker's CPU time. One bad
211
+ # write is not evidence that the next one fails: on Windows a
212
+ # redirected stderr can reject a write transiently. Give up only
213
+ # when the stream refuses repeatedly.
214
+ self._write_failures += 1
215
+ if self._write_failures >= _MAX_WRITE_FAILURES:
216
+ break
217
+ continue
218
+ idx += 1
@@ -21,7 +21,7 @@ import subprocess
21
21
  from collections import deque
22
22
  from dataclasses import dataclass, field
23
23
  from pathlib import Path
24
- from typing import Any, Iterable, Optional
24
+ from typing import Any, Iterable, Iterator, Optional
25
25
 
26
26
  from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION, caller_classes
27
27
  from sourcecode.degradation import cap_effect as _cap_effect
@@ -5634,6 +5634,43 @@ _VENDOR_DIRS: frozenset[str] = frozenset({
5634
5634
  "generated-resources",
5635
5635
  })
5636
5636
 
5637
+ #: Metadata trees that hold no repository source at all. Kept apart from
5638
+ #: `_VENDOR_DIRS` because that set answers "is this file part of the product";
5639
+ #: this one answers "is there anything here worth opening". `.git` alone holds
5640
+ #: 5 542 loose objects in this repository, and every unpruned `rglob` visits
5641
+ #: every one of them.
5642
+ _METADATA_DIRS: frozenset[str] = frozenset({
5643
+ ".git", ".hg", ".svn", ".idea", ".vscode", "__pycache__",
5644
+ })
5645
+
5646
+ #: One ignore set for every tree walk that is looking for source (C3-64). A
5647
+ #: second, hand-kept list beside this one is how two surfaces come to disagree
5648
+ #: about what counts as source.
5649
+ PRUNED_TREE_DIRS: frozenset[str] = _VENDOR_DIRS | _METADATA_DIRS
5650
+
5651
+
5652
+ def walk_source_tree(
5653
+ root: Path, *, prune: frozenset[str] = PRUNED_TREE_DIRS,
5654
+ ) -> "Iterator[Path]":
5655
+ """Yield every file under `root`, never descending into a pruned directory.
5656
+
5657
+ `Path.rglob` has no way to skip a subtree: it walks the build output and the
5658
+ VCS metadata and then filters by name, so the cost of a scan is set by what
5659
+ the build wrote rather than by what the developer did. Measured on
5660
+ openmrs-core, 47 % of all tree entries live under directories in `prune`
5661
+ (C3-64).
5662
+
5663
+ Yields absolute paths, and never raises: an unreadable directory is skipped
5664
+ the same way the previous `rglob` calls swallowed `OSError`.
5665
+ """
5666
+ import os
5667
+
5668
+ for dirpath, dirnames, filenames in os.walk(root, onerror=lambda _e: None):
5669
+ dirnames[:] = [d for d in dirnames if d not in prune]
5670
+ base = Path(dirpath)
5671
+ for name in filenames:
5672
+ yield base / name
5673
+
5637
5674
 
5638
5675
  def apply_ir_size_limits(
5639
5676
  ir: dict,
@@ -609,13 +609,36 @@ def scan_security_configuration(
609
609
  out.extend(_scan_commented_controls(source, rel, xml=False))
610
610
  out.extend(_scan_java(blank_java_comments(source), rel))
611
611
 
612
+ # C3-64 — one pruned walk for all three populations. This used to be five
613
+ # `rglob` calls (one per config suffix, one for descriptors, one for
614
+ # mappers), none of which could skip a subtree: the cost was set by the size
615
+ # of `target/`, `node_modules/` and `.git` rather than by the source. The
616
+ # ignore set is the walker's own (`repository_ir.PRUNED_TREE_DIRS`), so this
617
+ # surface and Java discovery cannot disagree about what is source.
618
+ from sourcecode.repository_ir import walk_source_tree
619
+
620
+ _walked_config: "list[str]" = []
621
+ descriptors: "list[str]" = []
622
+ mappers: "list[str]" = []
623
+ for path in walk_source_tree(root):
624
+ name = path.name
625
+ lower = name.lower()
626
+ try:
627
+ rel_posix = path.relative_to(root).as_posix()
628
+ except ValueError:
629
+ continue
630
+ if name.startswith("application") and name.endswith(_CONFIG_SUFFIXES):
631
+ _walked_config.append(rel_posix)
632
+ elif lower.endswith(".xml"):
633
+ # A descriptor that also ends in `Mapper.xml` stays a descriptor —
634
+ # the old code took the same side by subtracting one set from the other.
635
+ if lower in _DESCRIPTOR_NAMES or lower.endswith(_DESCRIPTOR_SUFFIXES):
636
+ descriptors.append(rel_posix)
637
+ elif name.endswith("Mapper.xml"):
638
+ mappers.append(rel_posix)
639
+
612
640
  if config_files is None:
613
- config_files = []
614
- for suffix in _CONFIG_SUFFIXES:
615
- config_files += [
616
- p.relative_to(root).as_posix()
617
- for p in root.rglob(f"application*{suffix}")
618
- ]
641
+ config_files = _walked_config
619
642
  for rel in sorted(set(config_files)):
620
643
  try:
621
644
  text = (root / rel).read_text(encoding="utf-8", errors="replace")
@@ -624,18 +647,10 @@ def scan_security_configuration(
624
647
  out.extend(_scan_commented_config_controls(text, rel))
625
648
  out.extend(_scan_config(blank_hash_comments(text), rel))
626
649
 
627
- # CL-10 — the descriptors. Discovered the same way `environment_resolution`
650
+ # CL-10 — the descriptors. Classified the same way `environment_resolution`
628
651
  # classifies them, so the two surfaces cannot disagree about which files are
629
652
  # deployment descriptors, and a per-environment overlay tree is covered
630
653
  # because the walk finds the file wherever the build put it.
631
- descriptors: "list[str]" = []
632
- try:
633
- for path in root.rglob("*.xml"):
634
- name = path.name.lower()
635
- if name in _DESCRIPTOR_NAMES or name.endswith(_DESCRIPTOR_SUFFIXES):
636
- descriptors.append(path.relative_to(root).as_posix())
637
- except OSError:
638
- pass
639
654
  for rel in sorted(set(descriptors)):
640
655
  try:
641
656
  text = (root / rel).read_text(encoding="utf-8", errors="replace")
@@ -644,13 +659,6 @@ def scan_security_configuration(
644
659
  out.extend(_scan_commented_controls(text, rel, xml=True))
645
660
  out.extend(_scan_descriptor(blank_xml_comments(text), rel))
646
661
 
647
- mappers: "list[str]" = []
648
- try:
649
- for path in root.rglob("*.xml"):
650
- if path.name.endswith("Mapper.xml"):
651
- mappers.append(path.relative_to(root).as_posix())
652
- except OSError:
653
- pass
654
662
  for rel in sorted(set(mappers) - set(descriptors)):
655
663
  try:
656
664
  text = (root / rel).read_text(encoding="utf-8", errors="replace")