sourcecode 3.5.0__tar.gz → 3.6.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 (192) hide show
  1. {sourcecode-3.5.0 → sourcecode-3.6.0}/CHANGELOG.md +166 -0
  2. {sourcecode-3.5.0 → sourcecode-3.6.0}/PKG-INFO +1 -1
  3. {sourcecode-3.5.0 → sourcecode-3.6.0}/pyproject.toml +1 -1
  4. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/cli.py +248 -34
  6. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/compare.py +70 -5
  7. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/hibernate_strat.py +60 -7
  8. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/migrate_check.py +244 -19
  9. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/output_budget.py +28 -0
  10. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/repository_ir.py +96 -15
  11. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/serializer.py +81 -8
  12. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_findings.py +38 -0
  13. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_security_audit.py +6 -0
  14. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_tx_analyzer.py +6 -0
  15. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/verify_repo.py +22 -4
  16. {sourcecode-3.5.0 → sourcecode-3.6.0}/.github/workflows/build-windows.yml +0 -0
  17. {sourcecode-3.5.0 → sourcecode-3.6.0}/.gitignore +0 -0
  18. {sourcecode-3.5.0 → sourcecode-3.6.0}/.ruff.toml +0 -0
  19. {sourcecode-3.5.0 → sourcecode-3.6.0}/CONTRIBUTING.md +0 -0
  20. {sourcecode-3.5.0 → sourcecode-3.6.0}/LICENSE +0 -0
  21. {sourcecode-3.5.0 → sourcecode-3.6.0}/README.md +0 -0
  22. {sourcecode-3.5.0 → sourcecode-3.6.0}/SECURITY.md +0 -0
  23. {sourcecode-3.5.0 → sourcecode-3.6.0}/raw +0 -0
  24. {sourcecode-3.5.0 → sourcecode-3.6.0}/scripts/compare_integration_engines.py +0 -0
  25. {sourcecode-3.5.0 → sourcecode-3.6.0}/scripts/customer_smoke_test.sh +0 -0
  26. {sourcecode-3.5.0 → sourcecode-3.6.0}/scripts/generate_jdk_exports.py +0 -0
  27. {sourcecode-3.5.0 → sourcecode-3.6.0}/scripts/perf_harness.py +0 -0
  28. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/adaptive_scanner.py +0 -0
  29. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/archetype.py +0 -0
  30. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/architectural_baseline.py +0 -0
  31. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/architectural_delta.py +0 -0
  32. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/architecture_analyzer.py +0 -0
  33. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/architecture_summary.py +0 -0
  34. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/ast_extractor.py +0 -0
  35. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/cache.py +0 -0
  36. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/cache_model.py +0 -0
  37. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/call_surface.py +0 -0
  38. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/caller_metrics.py +0 -0
  39. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/caller_reach.py +0 -0
  40. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/canonical_ir.py +0 -0
  41. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/chain_rules.py +0 -0
  42. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/change_plan.py +0 -0
  43. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/cir_graphs.py +0 -0
  44. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/classifier.py +0 -0
  45. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  46. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/confidence_analyzer.py +0 -0
  47. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/constraint_diff.py +0 -0
  48. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/context_cache.py +0 -0
  49. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/context_graph.py +0 -0
  50. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/context_scorer.py +0 -0
  51. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/context_summarizer.py +0 -0
  52. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/contract_diff.py +0 -0
  53. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/contract_model.py +0 -0
  54. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/contract_pipeline.py +0 -0
  55. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/coverage_parser.py +0 -0
  56. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/degradation.py +0 -0
  57. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/dependency_analyzer.py +0 -0
  58. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/deployment_prefix.py +0 -0
  59. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/__init__.py +0 -0
  60. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/base.py +0 -0
  61. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  62. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/dart.py +0 -0
  63. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/dotnet.py +0 -0
  64. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/elixir.py +0 -0
  65. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/go.py +0 -0
  66. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/heuristic.py +0 -0
  67. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/hybrid.py +0 -0
  68. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/java.py +0 -0
  69. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  70. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/nodejs.py +0 -0
  71. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/parsers.py +0 -0
  72. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/php.py +0 -0
  73. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/project.py +0 -0
  74. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/python.py +0 -0
  75. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/ruby.py +0 -0
  76. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/rust.py +0 -0
  77. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/systems.py +0 -0
  78. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/terraform.py +0 -0
  79. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/tooling.py +0 -0
  80. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/doc_analyzer.py +0 -0
  81. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  82. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/endpoint_literals.py +0 -0
  83. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/endpoint_metrics.py +0 -0
  84. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  85. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/env_analyzer.py +0 -0
  86. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/envelope.py +0 -0
  87. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/error_schema.py +0 -0
  88. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/evidence_provider.py +0 -0
  89. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/explain.py +0 -0
  90. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/facts/__init__.py +0 -0
  91. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/facts/registry.json +0 -0
  92. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/file_chunker.py +0 -0
  93. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/file_classifier.py +0 -0
  94. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/filter_surface.py +0 -0
  95. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/format_contract.py +0 -0
  96. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/fqn_utils.py +0 -0
  97. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/git_analyzer.py +0 -0
  98. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/graph_analyzer.py +0 -0
  99. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/graph_evidence.py +0 -0
  100. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/integration_coordinates.py +0 -0
  101. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/jdk_exports.py +0 -0
  102. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/license.py +0 -0
  103. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/__init__.py +0 -0
  104. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  105. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  106. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  107. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  108. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  109. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  110. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/registry.py +0 -0
  111. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/runner.py +0 -0
  112. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/server.py +0 -0
  113. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp_nudge.py +0 -0
  114. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/metrics_analyzer.py +0 -0
  115. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/migration_blast.py +0 -0
  116. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/openapi_surface.py +0 -0
  117. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/parse_cache.py +0 -0
  118. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/path_admission.py +0 -0
  119. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/path_filters.py +0 -0
  120. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/perf.py +0 -0
  121. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/posture.py +0 -0
  122. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  123. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/pr_impact.py +0 -0
  124. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/prepare_context.py +0 -0
  125. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/progress.py +0 -0
  126. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/ranking_engine.py +0 -0
  127. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/readiness_timeline.py +0 -0
  128. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/reconciliation.py +0 -0
  129. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/redactor.py +0 -0
  130. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/reference_facts.py +0 -0
  131. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/relevance_scorer.py +0 -0
  132. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/rename_refactor.py +0 -0
  133. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/repo_classifier.py +0 -0
  134. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/__init__.py +0 -0
  135. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/context.py +0 -0
  136. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/errors.py +0 -0
  137. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/executor.py +0 -0
  138. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/planner.py +0 -0
  139. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/query.py +0 -0
  140. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/request.py +0 -0
  141. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/resolution.py +0 -0
  142. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/result.py +0 -0
  143. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/retriever.py +0 -0
  144. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/runtime.py +0 -0
  145. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps.py +0 -0
  146. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  147. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
  148. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
  149. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
  150. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
  151. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  152. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/ris.py +0 -0
  153. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/runtime_classifier.py +0 -0
  154. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/scanner.py +0 -0
  155. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/schema.py +0 -0
  156. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  157. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/security_config.py +0 -0
  158. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/security_posture.py +0 -0
  159. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/semantic_analyzer.py +0 -0
  160. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  161. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/semantic_integration_engine.py +0 -0
  162. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/semantic_services.py +0 -0
  163. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_event_topology.py +0 -0
  164. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_impact.py +0 -0
  165. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_model.py +0 -0
  166. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_profiles.py +0 -0
  167. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_properties.py +0 -0
  168. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_semantic.py +0 -0
  169. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/summarizer.py +0 -0
  170. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/__init__.py +0 -0
  171. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/config.py +0 -0
  172. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/consent.py +0 -0
  173. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/events.py +0 -0
  174. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/filters.py +0 -0
  175. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/transport.py +0 -0
  176. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/test_sources.py +0 -0
  177. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/token_estimate.py +0 -0
  178. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/tree_utils.py +0 -0
  179. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/type_usage_surface.py +0 -0
  180. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/validation_inference.py +0 -0
  181. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/validation_surface.py +0 -0
  182. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/verify_edit.py +0 -0
  183. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/verify_rules.py +0 -0
  184. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/version_check.py +0 -0
  185. {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/workspace.py +0 -0
  186. {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/functions/README.md +0 -0
  187. {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/functions/get-license/index.ts +0 -0
  188. {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  189. {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/functions/telemetry/index.ts +0 -0
  190. {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/sql/license_event_ordering.sql +0 -0
  191. {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/sql/licensing_schema.sql +0 -0
  192. {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,171 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.6.0] — 2026-08-01
4
+
5
+ **The P0 block and the C1/C2 backlog.** Nine rows closed, three closed by measurement alone
6
+ (C1-17, C2-11, and C2-12 as stated did not reproduce). `migrate-check` stops billing work a
7
+ declared dependency already did, stops billing a Hibernate rewrite to another vendor's provider,
8
+ and stops assuming a single destination — `--target-jdk 11 --keep-boot` takes MIG-001 from 85 to 0
9
+ on openmrs-core.
10
+
11
+ Two patterns from 3.5.0 repeat, and both are worth carrying forward. **The reported symptom keeps
12
+ being narrower than the defect**: C1-10 was one rule and was also an evidence rule admitting JPA as
13
+ proof of Hibernate; C2-1 was a `why` trail and was also the ranking key underneath it. **And a
14
+ curated list beside real data keeps being the cause** — C2-3's trim schedule named sections the
15
+ payload does not have while the weight sat in two it never mentions, the third appearance of the
16
+ C4-1 shape in two releases.
17
+
18
+ **Minor, not patch**: blocking counts move (C1-10), reach-derived counts become nullable (C1-13),
19
+ `compare`'s ranking can move (C2-1), `migrate-check.findings` keeps one type across `--compact`
20
+ (C2-10), and two commands that wrote their confirmation to stderr now write it to stdout (C3-18).
21
+
22
+ ### Added
23
+
24
+ - **`migrate-check --target-jdk N` and `--keep-boot`** (C1-15, P0 gate 2). Asked about a JDK move,
25
+ the answer was `headline_blocker: boot3_migration` with fix hints sending a developer to migrate
26
+ 481 `javax.persistence` imports they never planned to touch; nothing could say "JDK 11, keep Boot
27
+ 2.7". The route is filtered over the `migration_target` vocabulary every rule already carried.
28
+ Measured on openmrs-core with `--target-jdk 11 --keep-boot`: **MIG-001 85 → 0**, blocking 114 → 0,
29
+ headline `hibernate_rewrite` → `none_applicable`. Narrowing is not silencing — `metadata.route`
30
+ publishes the target and every excluded axis, and a `limitations` entry states what was left out
31
+ and that it is unresolved, not fixed. **The default is unchanged**: no flags means every axis at
32
+ once.
33
+
34
+ - **`spring-audit` findings carry `first_line`** (C2-4), from the `SourceSpan` index the IR already
35
+ records. `null` where the symbol has no recorded span, never 0.
36
+
37
+ ### Changed
38
+
39
+ - **`migrate-check` reconciles findings with the build it already read** (C1-10, P0 gate 1,
40
+ **blocking counts move**). `MIG-021` fired as blocking while `javax.xml.bind:jaxb-api:2.3.1` sat
41
+ in the same `pom.xml`. A rule now declares the coordinates that satisfy it; a satisfied finding
42
+ gets `mitigated_by` and `code_context: "dependency_satisfied"`, leaving `blocking_count` exactly
43
+ as test scope does, and its hint stops saying "add this dependency". `satisfied_by` is empty for
44
+ every rule whose remedy is a source change.
45
+ - **The Hibernate axis now needs Hibernate evidence.** It billed 45.6 days to a repository whose
46
+ declared provider was EclipseLink because a bare `javax.persistence` import counted as proof —
47
+ that proves JPA, not Hibernate. A declared competing provider refutes the inference, and only
48
+ the inference: a repository carrying both coordinates keeps direct evidence. An inference from
49
+ a JPA import alone now reports `medium`, not `high`.
50
+ - Battery: `blocking_count` identical on all six repositories.
51
+
52
+ - **A capped blast radius answers `unknown`, not `0`** (C1-13, **reach-derived counts become
53
+ nullable**). A hub capped at `depth=1` reported `endpoints_affected_count: 0` for a walk that
54
+ never looked. Under the cap a reach-derived zero becomes `null`, named in
55
+ `stats.unknown_under_cap`. A **non-zero** count under the cap stays a number — it is a floor — and
56
+ an **uncapped** run keeps its measured zero.
57
+
58
+ - **`compare` ranks on measurements, not on rendered rows** (C1-13/C2-1, **ranking may move**).
59
+ Three of the seven cost fields were `len()` of a display list, so the ordering was computed over
60
+ however many rows the renderer had decided to show. Capped magnitudes are declared in
61
+ `cost_floor_fields` beside `cost`, never inside it.
62
+
63
+ - **`impact.endpoints_affected[]` gains `controller_class` and `handler_symbol`** (C2-12), the names
64
+ `impact-chain` already uses. Grouping by controller needed a different key per command on payloads
65
+ one run produces. Original keys kept as aliases.
66
+
67
+ - **`effort_breakdown.affected_files` is now `blocking_affected_files`** (C1-14, alias kept). It
68
+ named a different population from `summary.affected_files` — 264 against 265 on openmrs-core — and
69
+ the shared name was the whole of the contradiction. Both carry a `_basis`.
70
+
71
+ ### Fixed
72
+
73
+ - **The payload budget cuts where the bytes are** (C2-3). `_TRIM_SCHEDULE` is a hand-written list of
74
+ key names: on a 1 222-file `review-pr` diff it trimmed 1 733 KB → 1 732 KB against a 97 KB budget,
75
+ walking to `execution_paths` while every byte sat in `changed_files` and `runtime_changes`, which
76
+ it does not mention. Once the schedule is exhausted the remaining cut is chosen by measuring which
77
+ list is largest. Measured on that payload shape: **566 KB → 71 KB**.
78
+
79
+ - **`ask verify` no longer passes with zero contracts declared** (C3-15, **behaviour change for
80
+ pipelines**). It returned `pass` + exit 0 on a repository that declared nothing — a green light
81
+ for having verified nothing. `evaluate_repo` returns before it even builds the CIR when no
82
+ contracts exist, so no code was read and no contract was checked: the same standing as a
83
+ repository that could not be analysed, and now reported the same way — **`unverified`, exit 2**,
84
+ with the reason stated. Exit 2 and not 1 because the published contract reserves 1 for
85
+ violations found and 2 for a run that could not verify. Asserted under every `--fail-on` choice:
86
+ `never` suppresses violations from blocking, it does not turn a run that verified nothing into a
87
+ verified one. This is C3-22 in different clothes — there a typo in `$DIR` made the gate
88
+ permanently green, here a repository that never adopted contracts did.
89
+
90
+ - **A command that writes its answer to a file now confirms on stdout, not stderr** (C3-18,
91
+ **behaviour change for scripts reading stderr**). `repo-ir`, `modernize`, `export` and
92
+ `cold-start` wrote the "written to" confirmation to stderr while the payload went to the file
93
+ and stdout stayed empty, so `ask repo-ir -o ir.json > log.txt` captured **nothing** and
94
+ PowerShell 5.1 raised `NativeCommandError` over a run that succeeded. Measured on
95
+ spring-petclinic before the fix: stdout 0 bytes, stderr 155. C3-26 set this rule in 3.5.0 for
96
+ `cache warm`/`cache clear`; these four were missed because the stderr battery listed its
97
+ commands **by hand**. The `--output` rows are now discovered from the CLI.
98
+
99
+ - **Usage errors emit the JSON envelope for a pipeline** (C3-4). The interception was patched onto
100
+ `click.UsageError.show`, which Typer never calls when `rich_markup_mode` is set — it is, for the
101
+ help panels — so **no usage error had been emitting an envelope at all** and `ask <cmd>
102
+ --badflag` answered automation with a rich text box. Split by audience: a pipeline gets the
103
+ envelope, a person at a terminal keeps the `Usage:` line and the pointer to `--help`.
104
+
105
+ - **A flag no longer changes the type of a key** (C2-10, **breaking for `--compact` consumers**).
106
+ `migrate-check.findings` was a bare array in full output and a
107
+ `{total, shown, omitted, items}` object under `--compact` — the same key, two types. A consumer
108
+ iterating `findings` broke silently on a flag it did not pass: nothing errored, the loop simply
109
+ ran over the four keys of a summary object instead of over the findings.
110
+ - `findings` is a **list in both modes**. The counts moved to a `findings_cap` sibling that
111
+ exists **only when a cap actually bit**, and it carries the I-8 `display_list` statement of
112
+ what the cut changed and what stayed exact.
113
+ - **Five collections had it, not one**: top-level `findings`, `hibernate.rewrite_targets`,
114
+ `hibernate.findings`, `hibernate.critical_call_chains`, `hibernate.golden_sql_hotspots`, and
115
+ `blast_radius.files`.
116
+ - **Half of C2-13 falls out of the same fix.** `migrate-check.findings` is now the bare array
117
+ `spring-audit.findings` always was — one word, one type, one parser, measured across all
118
+ three payloads. The `schema_version` fragmentation is the half that stays open.
119
+ - Enforced rather than documented: `tests/test_flag_shape_stability.py` walks both payloads key
120
+ by key and fails on any type that moves. The same walk covers the root command's `--compact`
121
+ and `--agent`, which were **measured clean** while fixing this and are locked so they stay
122
+ clean.
123
+
124
+ - **A misplaced global flag is told where that flag lives** (C3-4). `ask spring-audit --full`
125
+ answered "No such option: --full" about a flag the tool does have — it is declared on the root
126
+ command. The flag is looked up on the root command, so every global flag typed after every
127
+ subcommand is covered and no flag name is written into the check; a genuinely unknown flag earns
128
+ no hint, asserted.
129
+
130
+ - **`repo-ir` states the output cost before it is paid** (C3-18). The advisory ran after the IR was
131
+ built, serialized and written — the whole bill settled. It now runs off the file count before the
132
+ build starts, as a **band** (10.8–27.9 KB per Java file) rather than a point: the per-file cost
133
+ spreads 2.6× across sampled repositories with how densely the code cross-references, and
134
+ publishing a midpoint as one figure is the C1-12 defect. The band is asserted to bracket what
135
+ those repositories actually produced.
136
+
137
+ - **A capped `file_relevance` list says what was cut, measured** (C3-20). The hint published the
138
+ *budget* as the number of files shown — on spring-petclinic, "Showing top 40/176" beside a list
139
+ of **25** — fired even when nothing had been cut, and advised `--full` while `--full` was active.
140
+ Two constraints trim the list (a visibility threshold and a per-category cap of half the budget)
141
+ and the sentence knew about neither. Now: nothing unless a cap really bit, the figure is what was
142
+ shown against the population it samples, a flag is named only when it would change the answer,
143
+ and the I-8 `display_list` block sits beside it.
144
+
145
+ - **`--no-cache` on a subcommand says what it does** (C3-5). Six commands claimed "always reads
146
+ fresh source (no snapshot cache)" while `ask cache model` records them reading `ris`, `cir` and
147
+ `parse`. One string now, stating the reason it is a no-op: since C1-9 every layer keys on the
148
+ exact tree state and the parse layer is content-addressed, so there is no staleness to bypass.
149
+ The flag stays accepted so scripts do not break, and is real on `ask --no-cache` (root).
150
+
151
+ - **`--output <file>` now returns the whole answer, which is what its own note promised**
152
+ (C2-2, first M8 increment). Measured on `org.broadleafcommerce.common.money.Money`: the file
153
+ held **30 of 134** caller classes and **50 of 1 302** indirect callers — the same sample stdout
154
+ gets — while the payload told the reader *"use `--output` for the full list"*.
155
+ - **The recorded cause was wrong, and fixing what was recorded would not have worked.** The
156
+ file carried no `_budget_note` at all: the caps were applied at *construction*, inside
157
+ `compute_blast_radius`, before any output flag was consulted. No option could have returned
158
+ the full list.
159
+ - A cap is now a **rendering** parameter (`list_limit`) and never a measurement one — every
160
+ `stats.*_count` is taken over the full population either way (ADR-0008 R5-R8). `--output`
161
+ renders unbounded; stdout keeps the sample and names the total; and the note only claims a
162
+ cap that actually bit, with the I-8 `display_list` effect block beside it.
163
+ - The output budget is skipped for a file as well. `--agent` and `--compact` already did this;
164
+ `impact` was the one that did not.
165
+ - Same rule for `impact-chain`, with one qualification: an **explicit `--limit` still wins**.
166
+ A default is not a request; a typed flag is (`_flag_was_given`, off Click's parameter source).
167
+ - The USER_GUIDE now states the rule where a reader meets it, beside the `impact` output.
168
+
3
169
  ## [3.5.0] — 2026-07-31
4
170
 
5
171
  **M7.7 "Robustness floor" — the axis field evaluation #5 scored 4/10.** Ten pieces, and the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 3.5.0
3
+ Version: 3.6.0
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "3.5.0"
7
+ version = "3.6.0"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "3.5.0"
7
+ __version__ = "3.6.0"
@@ -857,7 +857,13 @@ def _emit_command_output(
857
857
  if output_path is not None:
858
858
  _safe_write_file(output_path, content)
859
859
  if success_msg:
860
- typer.echo(success_msg, err=True)
860
+ # C3-26/C3-18: the payload went to the file, so stdout is free and
861
+ # this confirmation is the run's whole answer — `cache warm` and
862
+ # `cache clear` were corrected for exactly this in 3.5.0 and these
863
+ # three (`repo-ir`, `modernize`, `export`) were missed because the
864
+ # stderr battery listed its commands by hand. Measured before the
865
+ # fix on spring-petclinic: stdout 0 bytes, stderr 155.
866
+ typer.echo(success_msg)
861
867
  else:
862
868
  try:
863
869
  sys.stdout.buffer.write(content.encode("utf-8"))
@@ -874,30 +880,112 @@ def _emit_command_output(
874
880
  except AttributeError:
875
881
  sys.stdout.write(content + "\n")
876
882
  if copy and _copy_to_clipboard(content):
877
- typer.echo("✓ copied to clipboard", err=True)
883
+ # The payload is on stdout here, so this cannot join it; it is a
884
+ # notice and takes the terminal gate rather than dirtying a capture.
885
+ _notice("✓ copied to clipboard")
878
886
 
879
887
 
880
888
  # H-06: Intercept Click-level UsageError (unknown options, bad args) and emit JSON.
881
889
  # Click's default show() writes "Error: No such option: --foo" as plain text.
882
890
  # Automation consumers need JSON on stderr regardless of how the error originated.
891
+ def _root_level_flags() -> set[str]:
892
+ """Every option declared on the root command, discovered from the CLI.
893
+
894
+ Read at error time so a flag added to the root callback is covered without
895
+ anyone remembering to list it here — the C4-1 lesson about curated lists
896
+ living beside generated ones. Never raises: a failure here must not turn a
897
+ usage error into a traceback.
898
+ """
899
+ try:
900
+ import typer.main as _tm
901
+
902
+ group = _tm.get_command(app)
903
+ return {opt for param in group.params for opt in param.opts}
904
+ except Exception:
905
+ return set()
906
+
907
+
883
908
  try:
884
909
  import click.exceptions as _click_exc
885
910
 
911
+ def _misplaced_global_flag_hint(exc: Any) -> "tuple[Optional[str], Optional[str]]":
912
+ """(flag, hint) when a rejected option is really a root-level one.
913
+
914
+ C3-4: `ask spring-audit --full` answered "No such option: --full" about
915
+ a flag the tool does have — it is declared on the root command, not on
916
+ the subcommand, and nothing said so. Refusing is right; refusing
917
+ without naming where the flag lives is what cost the user the run.
918
+
919
+ The flag is looked up on the root command, so every global flag typed
920
+ after every subcommand is covered and no flag name is written here.
921
+ """
922
+ flag = str(
923
+ (getattr(exc, "option_name", None) or getattr(exc, "param_hint", None)) or ""
924
+ ).strip("'\"")
925
+ if not flag or flag not in _root_level_flags():
926
+ return (flag or None), None
927
+ cmd_path = getattr(getattr(exc, "ctx", None), "command_path", "") or ""
928
+ parts = cmd_path.split()
929
+ sub = parts[-1] if len(parts) > 1 else ""
930
+ hint = (
931
+ f"`{flag}` is a global flag: it goes before the command, "
932
+ f"as `ask {flag} <path>`" + (f", not after `{sub}`." if sub else ".")
933
+ )
934
+ return flag, hint
935
+
886
936
  def _json_click_usage_error_show(self: Any, file: Any = None) -> None: # type: ignore[override]
887
937
  import json as _je
888
- _flag = str((getattr(self, "option_name", None) or getattr(self, "param_hint", None)) or "").strip("'\"")
938
+ _flag, _hint = _misplaced_global_flag_hint(self)
889
939
  _context: dict[str, object] = {}
890
940
  if _flag:
891
941
  _context["flag"] = _flag
942
+ if _hint:
943
+ _context["scope"] = "root"
892
944
  payload = build_error_envelope(
893
945
  INVALID_INPUT_CODE,
894
946
  self.format_message(),
947
+ hint=_hint,
895
948
  **_context,
896
949
  )
897
950
  sys.stderr.write(_je.dumps(payload, ensure_ascii=False) + "\n")
898
951
  sys.stderr.flush()
899
952
 
900
953
  _click_exc.UsageError.show = _json_click_usage_error_show # type: ignore[method-assign]
954
+
955
+ # ...and again where Typer actually goes. With `rich_markup_mode` set — it
956
+ # is, for the help panels — typer.core routes every ClickException to
957
+ # `rich_utils.rich_format_error` and *never* calls `.show()`, so the patch
958
+ # above was dead for this app and no usage error has been emitting the JSON
959
+ # envelope: `ask <cmd> --badflag` answered with a rich text box on stderr.
960
+ # That is the same broken contract C3-23 closed for `archetype`, reached by
961
+ # a different route. Patched here rather than by dropping rich mode, which
962
+ # would cost the C4-1/C4-2 help panels.
963
+ try:
964
+ import typer.rich_utils as _typer_rich
965
+
966
+ _rich_format_error_original = _typer_rich.rich_format_error
967
+
968
+ def _json_rich_format_error(exc: Any) -> None:
969
+ """JSON for a pipeline, the usage box for a person.
970
+
971
+ Audience, not preference — the same reasoning as `_notice`. A
972
+ consumer parses stderr and needs the envelope; a human at a
973
+ terminal needs the `Usage:` line and the pointer to `--help`, and
974
+ would get neither from JSON. Nothing is lost either way.
975
+ """
976
+ if sys.stderr.isatty():
977
+ _rich_format_error_original(exc)
978
+ # The person who mistyped the flag is exactly who needs C3-4's
979
+ # hint, so it follows the box rather than living only in JSON.
980
+ _, _hint = _misplaced_global_flag_hint(exc)
981
+ if _hint:
982
+ typer.echo(_hint, err=True)
983
+ else:
984
+ _json_click_usage_error_show(exc)
985
+
986
+ _typer_rich.rich_format_error = _json_rich_format_error # type: ignore[assignment]
987
+ except Exception:
988
+ pass # rich unavailable — .show() above already covers the plain path
901
989
  except Exception:
902
990
  pass # click unavailable — plain-text fallback
903
991
 
@@ -1081,6 +1169,64 @@ _FREE_TIER_NODE_CAP: int = 50 # graph/semantic node cap — applies only to lar
1081
1169
  _JAVA_MIN_SCAN_DEPTH: int = 12 # Maven src/main/java/<pkg>/<module>/File depth floor
1082
1170
  _LARGE_REPO_ADVISORY_FILES: int = 3000 # cold-scan advisory threshold (files); honest expectation-setting, not a speed claim
1083
1171
 
1172
+ #: What one Java file costs in serialized `repo-ir` output, measured across four
1173
+ #: repositories (C3-18): spring-petclinic 30 files/318 KB (10.8 KB/file),
1174
+ #: jobrunr 581/9.5 MB (16.8), openmrs-core 866/21 MB (25.2), eureka 299/8.0 MB
1175
+ #: (27.3). Kept as a band, never collapsed to a midpoint: the 2.6x spread is
1176
+ #: real — it tracks how densely the code cross-references — and a point estimate
1177
+ #: would assert a precision the sample does not support (the C1-12 rule).
1178
+ _IR_BYTES_PER_FILE_LOW: int = 10_800
1179
+ _IR_BYTES_PER_FILE_HIGH: int = 27_900
1180
+ _IR_SIZE_WARN_MB: int = 10 # advise above this; below it the write is not worth a sentence
1181
+
1182
+ #: What `--no-cache` means on a subcommand (C3-5). The old text — "this command
1183
+ #: always reads fresh source (no snapshot cache)" — was false: `cache model`, the
1184
+ #: authority, records these commands reading the `ris`, `cir` and `parse` layers.
1185
+ #: They do skip the *snapshot* layer, which is the only thing the sentence got
1186
+ #: right, and it read as a promise about all caching.
1187
+ #:
1188
+ #: The flag is a genuine no-op here, and for a reason worth stating rather than
1189
+ #: asserting: since C1-9 every layer keys on the exact tree state and the
1190
+ #: analyzer fingerprint, and the parse layer is content-addressed, so none of
1191
+ #: them can serve an answer for code that has changed. There is no staleness for
1192
+ #: the flag to bypass. It is accepted so a script can pass it uniformly, and it
1193
+ #: is real on the root command, where the snapshot layer does store a rendered
1194
+ #: answer.
1195
+ _NO_CACHE_SUBCOMMAND_HELP = (
1196
+ "Accepted for compatibility; a no-op here. This command's layers key on the "
1197
+ "tree state, so none can serve a stale answer. Real on `ask --no-cache` "
1198
+ "(root), which caches a rendered answer. See `ask cache model`."
1199
+ )
1200
+
1201
+
1202
+ def _ir_size_advisory_message(file_count: int) -> "Optional[str]":
1203
+ """Return the `repo-ir` output-size advisory, or ``None`` below the threshold.
1204
+
1205
+ C3-18: the cost is stated *before* it is paid. The old advisory ran after
1206
+ the IR was built and serialized — the whole bill already settled and the
1207
+ file already on disk — so it could tell a user what they had just spent,
1208
+ never what they were about to.
1209
+
1210
+ The figure is a band, not a point. Sampled across four repositories the IR
1211
+ costs 10.8-27.9 KB per Java file, and that 2.6x spread is a real property of
1212
+ the input (how densely the code cross-references), not sampling noise.
1213
+ Publishing its midpoint as one number would assert a precision the sample
1214
+ does not support — the same defect C1-12 records for migration effort — so
1215
+ the band is published with the word "estimate" on it.
1216
+
1217
+ Pure and TTY-agnostic: the caller owns the terminal gate, so this never
1218
+ reaches stdout or a pipeline (C3-26).
1219
+ """
1220
+ est_low_mb = file_count * _IR_BYTES_PER_FILE_LOW / (1024 * 1024)
1221
+ est_high_mb = file_count * _IR_BYTES_PER_FILE_HIGH / (1024 * 1024)
1222
+ if est_high_mb <= _IR_SIZE_WARN_MB:
1223
+ return None
1224
+ return (
1225
+ f"[repo-ir] {file_count} files — estimated output "
1226
+ f"{est_low_mb:.0f}-{est_high_mb:.0f}MB (estimate, not a measurement). "
1227
+ "To spend less: --summary-only, --max-nodes N --max-edges N, or --gzip."
1228
+ )
1229
+
1084
1230
 
1085
1231
  def _cold_scan_advisory_message(
1086
1232
  file_tree: dict,
@@ -4091,6 +4237,19 @@ def repo_ir_cmd(
4091
4237
  )
4092
4238
  raise typer.Exit(code=1)
4093
4239
 
4240
+ # C3-18: state the cost before paying it, not after. The old advisory fired
4241
+ # once the IR was built AND serialized — the whole bill already paid, and the
4242
+ # file already on disk. The estimate is a RANGE because a point would be a
4243
+ # measurement we do not have: sampled across four repositories the IR costs
4244
+ # 10.8-27.9 KB per Java file (petclinic 30 files/318 KB, jobrunr 581/9.5 MB,
4245
+ # openmrs-core 866/21 MB, eureka 299/8.0 MB), a 2.6x spread that depends on
4246
+ # how densely the code cross-references. Reporting the midpoint of that as a
4247
+ # single figure is the C1-12 defect.
4248
+ if output_path and not gzip_output:
4249
+ _notice_msg = _ir_size_advisory_message(len(file_list))
4250
+ if _notice_msg:
4251
+ _notice(_notice_msg)
4252
+
4094
4253
  _ir_phase = f"extracting IR ({len(file_list)} files)"
4095
4254
  if since:
4096
4255
  _ir_phase += f" since {since}"
@@ -4113,38 +4272,32 @@ def repo_ir_cmd(
4113
4272
  output_path = output_path.with_suffix(output_path.suffix + ".gz")
4114
4273
  raw_bytes = output.encode("utf-8")
4115
4274
  size_bytes = len(raw_bytes)
4116
- _SIZE_WARN_BYTES = 10 * 1024 * 1024 # 10MB
4117
- if size_bytes > _SIZE_WARN_BYTES and not gzip_output:
4118
- typer.echo(
4119
- f"[repo-ir] Output is {size_bytes // (1024 * 1024)}MB — "
4120
- "consider --summary-only, --max-nodes N --max-edges N, or --gzip to compress.",
4121
- err=True,
4122
- )
4123
4275
  if gzip_output:
4124
4276
  import gzip as _gzip
4125
4277
  with _gzip.open(output_path, "wb") as _gz:
4126
4278
  _gz.write(raw_bytes)
4127
4279
  compressed_kb = output_path.stat().st_size // 1024
4128
4280
  size_kb = size_bytes // 1024
4281
+ # C3-26: when the payload went to a file, this confirmation IS the
4282
+ # answer of the run — the same case as `cache warm`. On stderr,
4283
+ # `ask repo-ir -o ir.json > log.txt` captured nothing and PowerShell
4284
+ # 5.1 raised NativeCommandError over a run that succeeded.
4129
4285
  typer.echo(
4130
- f"IR written to {output_path} ({compressed_kb}KB gzip, {size_kb}KB uncompressed)",
4131
- err=True,
4286
+ f"IR written to {output_path} ({compressed_kb}KB gzip, {size_kb}KB uncompressed)"
4132
4287
  )
4133
4288
  else:
4134
4289
  output_path.write_bytes(raw_bytes)
4135
4290
  size_kb = size_bytes // 1024
4136
4291
  if summary_only:
4137
4292
  typer.echo(
4138
- f"IR written to {output_path} ({size_kb}KB, graph omitted by --summary-only)",
4139
- err=True,
4293
+ f"IR written to {output_path} ({size_kb}KB, graph omitted by --summary-only)"
4140
4294
  )
4141
4295
  else:
4142
4296
  n_nodes = len((ir.get("graph") or {}).get("nodes") or [])
4143
4297
  n_edges = len((ir.get("graph") or {}).get("edges") or [])
4144
4298
  typer.echo(
4145
4299
  f"IR written to {output_path} "
4146
- f"({size_kb}KB, {n_nodes} nodes, {n_edges} edges)",
4147
- err=True,
4300
+ f"({size_kb}KB, {n_nodes} nodes, {n_edges} edges)"
4148
4301
  )
4149
4302
  else:
4150
4303
  if gzip_output:
@@ -4239,7 +4392,7 @@ def impact_cmd(
4239
4392
  ),
4240
4393
  no_cache: bool = typer.Option(
4241
4394
  False, "--no-cache",
4242
- help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
4395
+ help=_NO_CACHE_SUBCOMMAND_HELP,
4243
4396
  ),
4244
4397
  ) -> None:
4245
4398
  """Blast-radius analysis: who calls this class and what breaks if it changes?
@@ -4333,8 +4486,16 @@ def impact_cmd(
4333
4486
  finally:
4334
4487
  _prog.finish()
4335
4488
 
4336
- result = compute_blast_radius(ir, target, max_depth=depth)
4337
- result = _trim(result, BUDGET_IMPACT, label="impact")
4489
+ # `--output <file>` asks for the whole answer, and its own note promised it.
4490
+ # Both halves of that promise were broken: the caller lists were capped at
4491
+ # construction, so the file held the same 30 of 134, and the output budget
4492
+ # trimmed the file too — while `--agent` and `--compact` beside it already
4493
+ # skipped the budget when writing to a file (C2-2).
4494
+ _to_file = output_path is not None
4495
+ result = compute_blast_radius(
4496
+ ir, target, max_depth=depth, list_limit=0 if _to_file else None
4497
+ )
4498
+ result = _trim(result, BUDGET_IMPACT, label="impact", skip=_to_file)
4338
4499
  if _misread and result.get("resolution") == "not_found":
4339
4500
  result["analysis_warnings"] = [*(result.get("analysis_warnings") or []), _misread]
4340
4501
 
@@ -4422,7 +4583,7 @@ def endpoints_cmd(
4422
4583
  ),
4423
4584
  no_cache: bool = typer.Option(
4424
4585
  False, "--no-cache",
4425
- help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
4586
+ help=_NO_CACHE_SUBCOMMAND_HELP,
4426
4587
  ),
4427
4588
  by_controller: bool = typer.Option(
4428
4589
  False, "--by-controller",
@@ -5047,7 +5208,7 @@ def validation_cmd(
5047
5208
  ),
5048
5209
  no_cache: bool = typer.Option(
5049
5210
  False, "--no-cache",
5050
- help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
5211
+ help=_NO_CACHE_SUBCOMMAND_HELP,
5051
5212
  ),
5052
5213
  ) -> None:
5053
5214
  """Map request-body validation per endpoint (constraints + custom validators).
@@ -5910,7 +6071,7 @@ def spring_audit_cmd(
5910
6071
  ),
5911
6072
  no_cache: bool = typer.Option(
5912
6073
  False, "--no-cache",
5913
- help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
6074
+ help=_NO_CACHE_SUBCOMMAND_HELP,
5914
6075
  ),
5915
6076
  ) -> None:
5916
6077
  """Spring semantic audit: TX anomalies (TX-001..006) + security surface (SEC-001..003).
@@ -6182,12 +6343,13 @@ def verify_cmd(
6182
6343
 
6183
6344
  \b
6184
6345
  Contracts live in `.ask/contracts.yml` (see `ask verify-edit --help` for the
6185
- rule kinds). No contracts declared → nothing to verify, exit 0.
6346
+ rule kinds). No contracts declared → nothing was verified, exit 2.
6186
6347
 
6187
6348
  \b
6188
6349
  Exit codes (with --ci, the default): 0 = pass, 1 = violations blocked,
6189
- 2 = unverified (contracts unreadable or the repository could not be
6190
- analysed — never a silent pass). Same codes as `verify-edit`.
6350
+ 2 = unverified (no contracts declared, contracts unreadable, or the
6351
+ repository could not be analysed — never a silent pass). Same codes as
6352
+ `verify-edit`.
6191
6353
 
6192
6354
  \b
6193
6355
  Examples:
@@ -6451,6 +6613,24 @@ def migrate_check_cmd(
6451
6613
  help="Minimum severity to include: critical, high, medium, or low (default).",
6452
6614
  show_default=True,
6453
6615
  ),
6616
+ target_jdk: Optional[int] = typer.Option(
6617
+ None,
6618
+ "--target-jdk",
6619
+ help=(
6620
+ "The JDK you are actually moving to (e.g. 11, 17, 21). Rules for a "
6621
+ "later JDK are not your blockers and are not reported. Without it, "
6622
+ "every JDK axis is reported at once."
6623
+ ),
6624
+ ),
6625
+ keep_boot: bool = typer.Option(
6626
+ False,
6627
+ "--keep-boot",
6628
+ help=(
6629
+ "You are staying on Spring Boot 2.x. Drops the Boot 3 axes — the "
6630
+ "jakarta namespace, Spring Security 6 and the Hibernate 5→6 rewrite "
6631
+ "— which are not blockers for a move you are not making."
6632
+ ),
6633
+ ),
6454
6634
  compact: bool = typer.Option(
6455
6635
  False,
6456
6636
  "--compact",
@@ -6463,7 +6643,7 @@ def migrate_check_cmd(
6463
6643
  ),
6464
6644
  no_cache: bool = typer.Option(
6465
6645
  False, "--no-cache",
6466
- help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
6646
+ help=_NO_CACHE_SUBCOMMAND_HELP,
6467
6647
  ),
6468
6648
  snapshot: bool = typer.Option(
6469
6649
  False, "--snapshot",
@@ -6594,7 +6774,13 @@ def migrate_check_cmd(
6594
6774
  _prog = Progress()
6595
6775
  _prog.start(f"checking migration ({len(file_list)} files)")
6596
6776
  try:
6597
- report = run_migrate_check(file_list, target, min_severity=min_severity)
6777
+ report = run_migrate_check(
6778
+ file_list,
6779
+ target,
6780
+ min_severity=min_severity,
6781
+ target_jdk=target_jdk,
6782
+ keep_boot=keep_boot,
6783
+ )
6598
6784
  finally:
6599
6785
  _prog.finish()
6600
6786
  if _file_limitations:
@@ -6669,6 +6855,26 @@ _IMPACT_CAPPED_LISTS = (
6669
6855
  )
6670
6856
 
6671
6857
 
6858
+ def _flag_was_given(name: str) -> bool:
6859
+ """True when the user typed this option, rather than inheriting its default.
6860
+
6861
+ A default is not a request: `--output` means "give me the whole answer", and
6862
+ that reading must not override a cap the user explicitly asked for on the same
6863
+ line. Click records where each value came from, which is the only way to tell
6864
+ `--limit 100` apart from the 100 nobody typed.
6865
+ """
6866
+ try:
6867
+ import click
6868
+
6869
+ ctx = click.get_current_context(silent=True)
6870
+ if ctx is None:
6871
+ return False
6872
+ source = ctx.get_parameter_source(name)
6873
+ except Exception:
6874
+ return False
6875
+ return source is not None and getattr(source, "name", "") == "COMMANDLINE"
6876
+
6877
+
6672
6878
  def _cap_impact_lists(data: dict, limit: int) -> dict:
6673
6879
  """Cap impact-chain leaf arrays, keeping every total exact.
6674
6880
 
@@ -6745,7 +6951,7 @@ def impact_chain_cmd(
6745
6951
  ),
6746
6952
  no_cache: bool = typer.Option(
6747
6953
  False, "--no-cache",
6748
- help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
6954
+ help=_NO_CACHE_SUBCOMMAND_HELP,
6749
6955
  ),
6750
6956
  limit: int = typer.Option(
6751
6957
  100, "--limit",
@@ -6874,6 +7080,11 @@ def impact_chain_cmd(
6874
7080
  result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
6875
7081
 
6876
7082
  data = result.to_dict()
7083
+ # Same rule as `impact`: a file is a request for the whole answer (C2-2). An
7084
+ # explicit `--limit` still wins — the user asking for a cap on a file has said
7085
+ # so, and a default is not a request.
7086
+ if output_path is not None and not _flag_was_given("limit"):
7087
+ limit = 0
6877
7088
  data = _cap_impact_lists(data, limit)
6878
7089
  if _misread and result.resolution == "not_found":
6879
7090
  data["analysis_warnings"] = [*(data.get("analysis_warnings") or []), _misread]
@@ -7821,7 +8032,9 @@ def modernize_cmd(
7821
8032
 
7822
8033
  if output_path:
7823
8034
  _safe_write_file(output_path, output)
7824
- typer.echo(f"Modernization analysis written to {output_path}", err=True)
8035
+ # C3-26: the payload is in the file; this line is the answer (see
8036
+ # _emit_command_output, which this branch predates).
8037
+ typer.echo(f"Modernization analysis written to {output_path}")
7825
8038
  else:
7826
8039
  try:
7827
8040
  sys.stdout.buffer.write(output.encode("utf-8"))
@@ -8775,16 +8988,17 @@ def cold_start_cmd(
8775
8988
  _meta_cs["token_economy"] = _tok_econ(_out, _out_with_meta, target)
8776
8989
  _out = _json.dumps(_out_with_meta, indent=2, ensure_ascii=False)
8777
8990
  if not compact and _size > 400_000:
8778
- sys.stderr.write(
8991
+ # Informational: the answer is still on stdout, so this is a notice and
8992
+ # takes the terminal gate (C3-26).
8993
+ _notice(
8779
8994
  f"WARNING: Output is ~{_tokens // 1000}K tokens. This exceeds the context window of "
8780
8995
  "most LLMs (GPT-4o: 128K, Claude Sonnet: 200K). "
8781
- "Use --compact for a ~10K token subset, or --output FILE to save.\n"
8996
+ "Use --compact for a ~10K token subset, or --output FILE to save."
8782
8997
  )
8783
- sys.stderr.flush()
8784
8998
  if output_path:
8785
8999
  _safe_write_file(output_path, _out)
8786
- sys.stderr.write(f"Saved {len(_out.encode('utf-8'))} bytes to {output_path}\n")
8787
- sys.stderr.flush()
9000
+ # C3-26: payload in the file, so this line is the answer — stdout.
9001
+ typer.echo(f"Saved {len(_out.encode('utf-8'))} bytes to {output_path}")
8788
9002
  else:
8789
9003
  typer.echo(_out)
8790
9004