sourcecode 3.2.2__tar.gz → 3.4.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 (189) hide show
  1. {sourcecode-3.2.2 → sourcecode-3.4.0}/CHANGELOG.md +103 -0
  2. {sourcecode-3.2.2 → sourcecode-3.4.0}/PKG-INFO +8 -5
  3. {sourcecode-3.2.2 → sourcecode-3.4.0}/README.md +7 -4
  4. {sourcecode-3.2.2 → sourcecode-3.4.0}/pyproject.toml +1 -1
  5. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/cli.py +41 -39
  7. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/license.py +68 -6
  8. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/migrate_check.py +89 -7
  9. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/verify_edit.py +190 -7
  10. {sourcecode-3.2.2 → sourcecode-3.4.0}/.github/workflows/build-windows.yml +0 -0
  11. {sourcecode-3.2.2 → sourcecode-3.4.0}/.gitignore +0 -0
  12. {sourcecode-3.2.2 → sourcecode-3.4.0}/.ruff.toml +0 -0
  13. {sourcecode-3.2.2 → sourcecode-3.4.0}/CONTRIBUTING.md +0 -0
  14. {sourcecode-3.2.2 → sourcecode-3.4.0}/LICENSE +0 -0
  15. {sourcecode-3.2.2 → sourcecode-3.4.0}/SECURITY.md +0 -0
  16. {sourcecode-3.2.2 → sourcecode-3.4.0}/raw +0 -0
  17. {sourcecode-3.2.2 → sourcecode-3.4.0}/scripts/compare_integration_engines.py +0 -0
  18. {sourcecode-3.2.2 → sourcecode-3.4.0}/scripts/customer_smoke_test.sh +0 -0
  19. {sourcecode-3.2.2 → sourcecode-3.4.0}/scripts/generate_jdk_exports.py +0 -0
  20. {sourcecode-3.2.2 → sourcecode-3.4.0}/scripts/perf_harness.py +0 -0
  21. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/adaptive_scanner.py +0 -0
  22. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/archetype.py +0 -0
  23. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/architectural_baseline.py +0 -0
  24. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/architectural_delta.py +0 -0
  25. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/architecture_analyzer.py +0 -0
  26. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/architecture_summary.py +0 -0
  27. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/ast_extractor.py +0 -0
  28. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/cache.py +0 -0
  29. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/cache_model.py +0 -0
  30. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/call_surface.py +0 -0
  31. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/caller_metrics.py +0 -0
  32. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/canonical_ir.py +0 -0
  33. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/chain_rules.py +0 -0
  34. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/change_plan.py +0 -0
  35. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/cir_graphs.py +0 -0
  36. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/classifier.py +0 -0
  37. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  38. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/compare.py +0 -0
  39. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/confidence_analyzer.py +0 -0
  40. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/constraint_diff.py +0 -0
  41. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/context_cache.py +0 -0
  42. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/context_graph.py +0 -0
  43. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/context_scorer.py +0 -0
  44. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/context_summarizer.py +0 -0
  45. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/contract_diff.py +0 -0
  46. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/contract_model.py +0 -0
  47. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/contract_pipeline.py +0 -0
  48. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/coverage_parser.py +0 -0
  49. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/dependency_analyzer.py +0 -0
  50. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/deployment_prefix.py +0 -0
  51. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/__init__.py +0 -0
  52. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/base.py +0 -0
  53. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  54. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/dart.py +0 -0
  55. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/dotnet.py +0 -0
  56. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/elixir.py +0 -0
  57. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/go.py +0 -0
  58. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/heuristic.py +0 -0
  59. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/hybrid.py +0 -0
  60. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/java.py +0 -0
  61. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  62. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/nodejs.py +0 -0
  63. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/parsers.py +0 -0
  64. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/php.py +0 -0
  65. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/project.py +0 -0
  66. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/python.py +0 -0
  67. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/ruby.py +0 -0
  68. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/rust.py +0 -0
  69. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/systems.py +0 -0
  70. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/terraform.py +0 -0
  71. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/detectors/tooling.py +0 -0
  72. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/doc_analyzer.py +0 -0
  73. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
  74. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/endpoint_literals.py +0 -0
  75. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/endpoint_metrics.py +0 -0
  76. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  77. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/env_analyzer.py +0 -0
  78. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/envelope.py +0 -0
  79. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/error_schema.py +0 -0
  80. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/evidence_provider.py +0 -0
  81. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/explain.py +0 -0
  82. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/facts/__init__.py +0 -0
  83. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/facts/registry.json +0 -0
  84. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/file_chunker.py +0 -0
  85. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/file_classifier.py +0 -0
  86. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/filter_surface.py +0 -0
  87. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/format_contract.py +0 -0
  88. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/fqn_utils.py +0 -0
  89. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/git_analyzer.py +0 -0
  90. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/graph_analyzer.py +0 -0
  91. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/graph_evidence.py +0 -0
  92. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/hibernate_strat.py +0 -0
  93. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/integration_coordinates.py +0 -0
  94. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/jdk_exports.py +0 -0
  95. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/__init__.py +0 -0
  96. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  97. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  98. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  99. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  100. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  101. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  102. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/registry.py +0 -0
  103. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/runner.py +0 -0
  104. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp/server.py +0 -0
  105. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/mcp_nudge.py +0 -0
  106. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/metrics_analyzer.py +0 -0
  107. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/migration_blast.py +0 -0
  108. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/openapi_surface.py +0 -0
  109. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/output_budget.py +0 -0
  110. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/parse_cache.py +0 -0
  111. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/path_filters.py +0 -0
  112. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/perf.py +0 -0
  113. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/posture.py +0 -0
  114. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  115. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/pr_impact.py +0 -0
  116. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/prepare_context.py +0 -0
  117. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/progress.py +0 -0
  118. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/ranking_engine.py +0 -0
  119. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/readiness_timeline.py +0 -0
  120. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/reconciliation.py +0 -0
  121. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/redactor.py +0 -0
  122. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/reference_facts.py +0 -0
  123. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/relevance_scorer.py +0 -0
  124. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/rename_refactor.py +0 -0
  125. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/repo_classifier.py +0 -0
  126. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/repository_ir.py +0 -0
  127. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/__init__.py +0 -0
  128. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/context.py +0 -0
  129. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/errors.py +0 -0
  130. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/executor.py +0 -0
  131. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/planner.py +0 -0
  132. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/query.py +0 -0
  133. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/request.py +0 -0
  134. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/resolution.py +0 -0
  135. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/result.py +0 -0
  136. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/retriever.py +0 -0
  137. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/runtime.py +0 -0
  138. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps.py +0 -0
  139. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  140. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
  141. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
  142. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
  143. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
  144. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  145. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/ris.py +0 -0
  146. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/runtime_classifier.py +0 -0
  147. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/scanner.py +0 -0
  148. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/schema.py +0 -0
  149. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
  150. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/security_config.py +0 -0
  151. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/security_posture.py +0 -0
  152. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/semantic_analyzer.py +0 -0
  153. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/semantic_impact_engine.py +0 -0
  154. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/semantic_integration_engine.py +0 -0
  155. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/semantic_services.py +0 -0
  156. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/serializer.py +0 -0
  157. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_event_topology.py +0 -0
  158. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_findings.py +0 -0
  159. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_impact.py +0 -0
  160. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_model.py +0 -0
  161. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_profiles.py +0 -0
  162. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_properties.py +0 -0
  163. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_security_audit.py +0 -0
  164. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_semantic.py +0 -0
  165. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  166. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/summarizer.py +0 -0
  167. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/telemetry/__init__.py +0 -0
  168. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/telemetry/config.py +0 -0
  169. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/telemetry/consent.py +0 -0
  170. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/telemetry/events.py +0 -0
  171. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/telemetry/filters.py +0 -0
  172. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/telemetry/transport.py +0 -0
  173. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/test_sources.py +0 -0
  174. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/token_estimate.py +0 -0
  175. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/tree_utils.py +0 -0
  176. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/type_usage_surface.py +0 -0
  177. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/validation_inference.py +0 -0
  178. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/validation_surface.py +0 -0
  179. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/verify_repo.py +0 -0
  180. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/verify_rules.py +0 -0
  181. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/version_check.py +0 -0
  182. {sourcecode-3.2.2 → sourcecode-3.4.0}/src/sourcecode/workspace.py +0 -0
  183. {sourcecode-3.2.2 → sourcecode-3.4.0}/supabase/functions/README.md +0 -0
  184. {sourcecode-3.2.2 → sourcecode-3.4.0}/supabase/functions/get-license/index.ts +0 -0
  185. {sourcecode-3.2.2 → sourcecode-3.4.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  186. {sourcecode-3.2.2 → sourcecode-3.4.0}/supabase/functions/telemetry/index.ts +0 -0
  187. {sourcecode-3.2.2 → sourcecode-3.4.0}/supabase/sql/license_event_ordering.sql +0 -0
  188. {sourcecode-3.2.2 → sourcecode-3.4.0}/supabase/sql/licensing_schema.sql +0 -0
  189. {sourcecode-3.2.2 → sourcecode-3.4.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -2,6 +2,109 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.4.0] — 2026-07-29
6
+
7
+ **Two of the five reconciliation gates, and one commercial surface that answered five ways.**
8
+ The theme of field evaluation #4 was not missing analysis: it was analysis the product had
9
+ already done and did not consult — a migration estimate that ignored its own
10
+ `auto_fix_available`, and a gate that reported `pass` for a file it never looked at.
11
+
12
+ **A minor, not a patch, and for one reason: `verify-edit` can now exit 2 where it exited 0.**
13
+ No API changed, but a build-configuration-only edit stops being a green light, so an installed
14
+ hook or a pipeline that gates on the exit code will start blocking on it. A patch release must
15
+ never be able to turn a green pipeline red.
16
+
17
+ **Upgrade notes.**
18
+
19
+ - `verify-edit`: a change to `pom.xml`, `build.gradle`, `application*.yml`, `web.xml` or a
20
+ container descriptor now yields `unverified` (exit 2). `git commit --no-verify` and
21
+ `ask verify-edit --no-ci` are the escape hatches.
22
+ - `migrate-check`: published effort numbers **move down** — the same work, split by what it
23
+ actually is. Measured: openmrs-core 122.1 → 76.4 d, BroadleafCommerce 163.3 → 157.7,
24
+ alfresco 22.8 → 5.2. `blocking_count` and `readiness_score` are unchanged.
25
+ - `auth status`: new authoritative fields; `pro`, `pro_effective`, `pro_gating` and `pro_reason`
26
+ still ship, now derived from the authority and listed in `deprecated_fields`.
27
+
28
+ ### Fixed
29
+
30
+ - **`verify-edit` sees the files that decide what the code compiles to** (C3-14, the second
31
+ reconciliation gate). Editing a `pom.xml` produced `changed_files: []`, `verdict: pass`,
32
+ `exit_code: 0` — and `blast_radius: {"confidence": "high", "note": "no changed files"}`.
33
+ git reported the file; the gate did not look at it, then said everything was fine about a
34
+ change that moves every source file in the repository at once.
35
+ - New blocking axis **`build_config_changed`**, and a new `changed_build_files` list beside
36
+ `changed_files`. It names what moved: `effective_compiler_release` (from → to) and
37
+ `dependency_delta` per coordinate.
38
+ - **The verdict is `unverified`, not `break`.** A build edit is neither a proven regression
39
+ nor computed-clean, which is exactly what `changed: null` has always meant here — so it
40
+ flows through the existing "could not compute" path and exits 2. No special case.
41
+ - Admission is by file name and deliberately narrow — `pom.xml`, `build.gradle`,
42
+ `settings.gradle`, `gradle.properties`, `application*.yml|properties`, `bootstrap*`,
43
+ `web.xml`, `weblogic*.xml`, `jboss-*.xml`, `*-context.xml`, `persistence.xml`, `beans.xml`.
44
+ `messages.properties` and `logback.xml` are out: every admitted file degrades a verdict, and
45
+ a loose rule turns the gate into noise people learn to ignore.
46
+ - `blast_radius` stops reporting `confidence: "high"` over a change whose reach it has not
47
+ measured.
48
+ - **Upgrade note (behaviour change):** a build-configuration-only edit now exits **2**
49
+ (unverified) where it exited 0 (pass). An installed pre-commit/pre-push hook will block on
50
+ such an edit; `git commit --no-verify` remains the escape hatch, and `--no-ci` still reports
51
+ without gating.
52
+
53
+ ### Changed
54
+
55
+ - **The migration estimate reads the report's own `auto_fix_available`** (C1-11, the first of
56
+ the five reconciliation gates from field evaluation #4). `migrate-check` shipped an
57
+ OpenRewrite recipe for most findings and then billed every one of them at the hand-edit rate.
58
+ Reproduced on the battery before fixing: **openmrs-core carries a recipe for 296 of 310
59
+ findings and costed all 310 as hand work**, for a headline of 122.1 person-days.
60
+ - `effort_breakdown` splits the same total into **`manual_days`** + **`mechanical_days`** +
61
+ `hibernate_rewrite_effort_days`. The partition is **by file**, because the severity weights
62
+ are per file: a file is mechanical only when *every* finding in it has a recipe, and one
63
+ unautomatable finding costs the whole file manually — a human is in that file either way.
64
+ - **The review factor is published, not buried.** `mechanical_review_factor` (0.2) carries
65
+ `mechanical_review_factor_basis: "assumption, not a measurement"`, so a team can substitute
66
+ their own number instead of inheriting ours silently. `recipes[]` lists what would run and
67
+ `mechanical_basis` states both denominators.
68
+ - **`validation_days` is `null` with a note, never `0`** — the re-test scope is a different
69
+ measurement (`migrate-check --blast-radius`), and `0` would read as "no re-testing needed"
70
+ (invariant I-3).
71
+ - **Published numbers move.** Measured: openmrs-core 122.1 → **76.4 d**, BroadleafCommerce
72
+ 163.3 → **157.7 d**, alfresco 22.8 → **5.2 d**. `blocking_count` and `readiness_score` are
73
+ unchanged in all three — the split changes what the work is called, never what blocks.
74
+
75
+ - **`ask auth status` answers one question instead of five** (P-2). It used to publish
76
+ `{"status": "unauthenticated", "pro": true, "pro_reason": "early-adoption unlock"}` — every
77
+ field true on its own axis, and together unreadable: a buyer could not tell what they have,
78
+ why they have it, or what changes when it ends.
79
+ - One authority, `license.entitlement()`, now answers it: **`entitlement`** (`pro`/`free` —
80
+ what runs today), **`source`** (why: `license_key`, `early_adoption_unlock`, `free_tier`),
81
+ **`authenticated`** (whether a credential exists here — a *separate* fact, since today one
82
+ can be entitled without one), **`paywall_active`**, **`when_it_changes`** (what you lose
83
+ when that source stops applying) and **`free_repo_java_file_limit`** (the number the gate
84
+ actually compares against).
85
+ - `when_it_changes` states the gate in the terms the gate uses and **never quotes a price** —
86
+ pricing lives in `docs/PRODUCT_TIERS.md`, and a number printed here would be stale the day
87
+ it moves. Asserted.
88
+ - **`is_pro` is derived from the same authority.** It is what every gate reads
89
+ (`can_use`, `require_feature`), and it used to be computed beside the status block rather
90
+ than from it — which is how a status page came to contradict the commands next to it.
91
+ - `pro`, `pro_effective`, `pro_gating` and `pro_reason` are still emitted for existing
92
+ consumers, are derived from the authority (so they cannot drift), and are listed in the new
93
+ `deprecated_fields`. They are removed in a future major.
94
+ - `plan_status` no longer reports `unknown` for a licence file with no explicit status while
95
+ the authority reads that same file as active.
96
+
97
+ ## [3.3.0] — 2026-07-29
98
+
99
+ **A default you must remember to disable is the wrong default for someone else's code.**
100
+ Telemetry moves from opt-out to opt-in. A minor and not a patch: no API changed, but a default
101
+ about data leaving the machine did, and that is a change a user must be told about rather than
102
+ discover.
103
+
104
+ **Upgrade note.** If you ever ran `ask telemetry enable` or `ask telemetry disable`, nothing
105
+ changes for you — your choice is on record and is honoured. If you never chose, telemetry is
106
+ now off and `ask telemetry enable` is how you turn it on.
107
+
5
108
  ### Changed
6
109
 
7
110
  - **Telemetry is opt-in** (P-1). It was opt-out; with no explicit choice on record, nothing is
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 3.2.2
3
+ Version: 3.4.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
@@ -283,10 +283,13 @@ Honest limits worth knowing before you rely on it:
283
283
  > full Pro entitlements — no size gate, no key. The tiers below describe the model the
284
284
  > paywall will return to later.
285
285
  >
286
- > **What that means concretely.** `ask auth status` reports `"status": "unauthenticated"`
287
- > together with `"pro": true`, `"pro_reason": "early-adoption unlock"` that combination is
288
- > expected, not a bug: you are unauthenticated *and* unlocked. When the unlock ends, gating
289
- > returns **by repo size and automation, never by command**: `posture`, `endpoints`,
286
+ > **What that means concretely.** Ask the product: `ask auth status` answers in one block —
287
+ > `entitlement` (what runs today), `source` (*why* — a licence, the unlock, or the free tier),
288
+ > `authenticated` (whether a credential exists, which is a separate fact) and
289
+ > `when_it_changes` (what you lose when that source stops applying). A fresh install reads
290
+ > `entitlement: pro`, `source: early_adoption_unlock`, `authenticated: false` — unauthenticated
291
+ > *and* entitled, stated as two facts instead of one contradiction. When the unlock ends,
292
+ > gating returns **by repo size and automation, never by command**: `posture`, `endpoints`,
290
293
  > `spring-audit` and `migrate-check` stay in the base tier at full output. Nothing you can run
291
294
  > today becomes a paid-only command tomorrow.
292
295
 
@@ -245,10 +245,13 @@ Honest limits worth knowing before you rely on it:
245
245
  > full Pro entitlements — no size gate, no key. The tiers below describe the model the
246
246
  > paywall will return to later.
247
247
  >
248
- > **What that means concretely.** `ask auth status` reports `"status": "unauthenticated"`
249
- > together with `"pro": true`, `"pro_reason": "early-adoption unlock"` that combination is
250
- > expected, not a bug: you are unauthenticated *and* unlocked. When the unlock ends, gating
251
- > returns **by repo size and automation, never by command**: `posture`, `endpoints`,
248
+ > **What that means concretely.** Ask the product: `ask auth status` answers in one block —
249
+ > `entitlement` (what runs today), `source` (*why* — a licence, the unlock, or the free tier),
250
+ > `authenticated` (whether a credential exists, which is a separate fact) and
251
+ > `when_it_changes` (what you lose when that source stops applying). A fresh install reads
252
+ > `entitlement: pro`, `source: early_adoption_unlock`, `authenticated: false` — unauthenticated
253
+ > *and* entitled, stated as two facts instead of one contradiction. When the unlock ends,
254
+ > gating returns **by repo size and automation, never by command**: `posture`, `endpoints`,
252
255
  > `spring-audit` and `migrate-check` stay in the base tier at full output. Nothing you can run
253
256
  > today becomes a paid-only command tomorrow.
254
257
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "3.2.2"
7
+ version = "3.4.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.2.2"
7
+ __version__ = "3.4.0"
@@ -6106,9 +6106,14 @@ def verify_edit_cmd(
6106
6106
 
6107
6107
  Deterministic, in-loop verdict over the diff of the working tree against HEAD:
6108
6108
  public contract, @Transactional proxy boundary, security surface, bean wiring,
6109
- custom domain rules, and blast radius. Built for the agent write-loop (MCP) and
6110
- git hooks. Only regressions the edit INTRODUCED (vs HEAD) block — pre-existing
6111
- debt never fails an unrelated commit.
6109
+ custom domain rules, build/configuration changes, and blast radius. Built for the
6110
+ agent write-loop (MCP) and git hooks. Only regressions the edit INTRODUCED (vs
6111
+ HEAD) block — pre-existing debt never fails an unrelated commit.
6112
+
6113
+ Build and configuration files (pom.xml, build.gradle, application*.yml, web.xml,
6114
+ container descriptors, Spring XML) are reported under `changed_build_files` and
6115
+ make the verdict UNVERIFIED, never `pass`: their effect is not decidable from a
6116
+ source diff, and a compiler-release bump moves every file in the repo at once.
6112
6117
 
6113
6118
  Custom rules: drop a `.ask/verify-rules.json` at the repo root to enforce your own
6114
6119
  invariants (e.g. "no @RestController injects a *DaoJpa", "every /payroll endpoint
@@ -8105,47 +8110,44 @@ def auth_status_cmd() -> None:
8105
8110
  """Show current authentication and plan status."""
8106
8111
  import json as _json
8107
8112
  try:
8108
- from sourcecode.license import (
8109
- _PRO_UNLOCK_ALL as _unlock,
8110
- _license_data as _ld,
8111
- is_pro as _ip,
8112
- )
8113
+ from sourcecode.license import _license_data as _ld, entitlement as _entitlement
8114
+ _ent = _entitlement()
8113
8115
  except Exception:
8114
8116
  _ld = None
8115
- _ip = False
8116
- _unlock = False
8117
-
8118
- # While the early-adoption unlock is on, every Pro command runs. Reporting
8119
- # `pro: false` here contradicted the --help header and the commands' own
8120
- # behaviour: the status has to describe the gating that is actually in force,
8121
- # not the credential that happens to be absent.
8122
- _gating = {
8123
- "pro_gating": "disabled" if _unlock else "enabled",
8124
- "pro_effective": bool(_ip or _unlock),
8125
- }
8126
- if _unlock:
8127
- _gating["pro_reason"] = "early-adoption unlock: Pro commands run without a license"
8128
-
8129
- if not _ld:
8130
- out: dict = {
8131
- "status": "unauthenticated",
8132
- "pro": bool(_unlock),
8133
- **_gating,
8117
+ _ent = {
8118
+ "entitlement": "free", "source": "free_tier", "authenticated": False,
8119
+ "paywall_active": True, "when_it_changes": "", "free_repo_java_file_limit": None,
8134
8120
  }
8135
- sys.stdout.write(_json.dumps(out, ensure_ascii=False) + "\n")
8136
- sys.stdout.flush()
8137
- return
8138
8121
 
8139
- out = {
8140
- "status": "authenticated",
8141
- "auth_method": _ld.get("auth_method", "license_key"),
8142
- "email": _ld.get("email", ""),
8143
- "plan": _ld.get("plan", "unknown"),
8144
- "plan_status": _ld.get("status", "unknown"),
8145
- "pro": _ip,
8146
- "validated_at": _ld.get("validated_at") or _ld.get("activated_at") or "",
8147
- **_gating,
8122
+ # P-2: one authority (`license.entitlement`) answers "what runs here, and why".
8123
+ # The legacy keys below are *derived* from it rather than computed a second
8124
+ # time — that is what stops `{"status": "unauthenticated", "pro": true}` from
8125
+ # reappearing. They are deprecated and will be removed in a future major.
8126
+ _pro = _ent["entitlement"] == "pro"
8127
+ _legacy = {
8128
+ "pro": _pro,
8129
+ "pro_effective": _pro,
8130
+ "pro_gating": "enabled" if _ent["paywall_active"] else "disabled",
8131
+ }
8132
+ if _ent["source"] == "early_adoption_unlock":
8133
+ _legacy["pro_reason"] = "early-adoption unlock: Pro commands run without a license"
8134
+
8135
+ out: dict = {
8136
+ "status": "authenticated" if _ent["authenticated"] else "unauthenticated",
8137
+ **_ent,
8138
+ **_legacy,
8139
+ "deprecated_fields": ["pro", "pro_effective", "pro_gating", "pro_reason"],
8148
8140
  }
8141
+ if _ld:
8142
+ out.update({
8143
+ "auth_method": _ld.get("auth_method", "license_key"),
8144
+ "email": _ld.get("email", ""),
8145
+ "plan": _ld.get("plan", "unknown"),
8146
+ # The authority reads a missing status as active, so reporting
8147
+ # "unknown" here would contradict the entitlement in the same object.
8148
+ "plan_status": _ld.get("status", "active"),
8149
+ "validated_at": _ld.get("validated_at") or _ld.get("activated_at") or "",
8150
+ })
8149
8151
  sys.stdout.write(_json.dumps(out, indent=2, ensure_ascii=False) + "\n")
8150
8152
  sys.stdout.flush()
8151
8153
 
@@ -376,16 +376,78 @@ def _maybe_revalidate() -> None:
376
376
  pass
377
377
 
378
378
 
379
- def _init() -> None:
380
- global _license_data, is_pro
381
- _license_data = _load_license_file()
382
- is_pro = (
379
+ def entitlement() -> dict:
380
+ """The one answer to "what runs here today, and why" (P-2).
381
+
382
+ `auth status` used to publish four fields about this — `status`, `pro`,
383
+ `pro_gating`, `pro_effective`, plus a `pro_reason` sentence — and they read as
384
+ a contradiction: `{"status": "unauthenticated", "pro": true}`. Each field was
385
+ true on its own axis; together they told a buyer nothing about what they have,
386
+ why they have it, or what changes when it ends. That is the ADR-0008 pattern
387
+ exactly, on the commercial surface instead of an analysis one.
388
+
389
+ Every published entitlement field is derived from this dict and nowhere else.
390
+
391
+ Keys:
392
+ entitlement "pro" | "free" — what actually runs right now
393
+ source why: "license_key" | "early_adoption_unlock" | "free_tier"
394
+ authenticated whether a credential is on this machine (an independent
395
+ fact: today one can be entitled to Pro without one)
396
+ paywall_active whether the paywall is enforcing anything at all
397
+ when_it_changes what a user loses when `source` stops applying, in the
398
+ terms the gate actually uses — never a price
399
+ free_repo_java_file_limit
400
+ the number the size gate compares against
401
+ """
402
+ authenticated = _license_data is not None
403
+ licensed = bool(
383
404
  _license_data is not None
384
405
  and _license_data.get("plan") == "pro"
385
406
  and _license_data.get("status", "active") != "inactive"
386
407
  )
387
- if _PRO_UNLOCK_ALL:
388
- is_pro = True # TEMPORARY: early-adoption Pro unlock (see _PRO_UNLOCK_ALL)
408
+ if licensed:
409
+ source = "license_key"
410
+ elif _PRO_UNLOCK_ALL:
411
+ source = "early_adoption_unlock"
412
+ else:
413
+ source = "free_tier"
414
+
415
+ if source == "license_key":
416
+ when = "Your licence entitles you to Pro; nothing changes while it stays active."
417
+ elif source == "early_adoption_unlock":
418
+ when = (
419
+ "Pro runs without a licence during early adoption. When that ends, every command "
420
+ f"still runs at full output on repositories up to {_FREE_REPO_JAVA_FILE_LIMIT} Java "
421
+ "source files; above that, the heavy-analysis commands ask for Pro. Capability is "
422
+ "never what is gated — size, automation and team are."
423
+ )
424
+ else:
425
+ when = (
426
+ f"Every command runs at full output on repositories up to {_FREE_REPO_JAVA_FILE_LIMIT} "
427
+ "Java source files. Above that, the heavy-analysis commands require Pro."
428
+ )
429
+
430
+ return {
431
+ "entitlement": "pro" if (licensed or _PRO_UNLOCK_ALL) else "free",
432
+ "source": source,
433
+ "authenticated": authenticated,
434
+ "paywall_active": not _PRO_UNLOCK_ALL,
435
+ "when_it_changes": when,
436
+ "free_repo_java_file_limit": _FREE_REPO_JAVA_FILE_LIMIT,
437
+ }
438
+
439
+
440
+ def _init() -> None:
441
+ """Load the credential and derive the entitlement from the one authority.
442
+
443
+ `is_pro` is what every gate reads (`can_use`, `require_feature`), and it is
444
+ computed here from `entitlement()` rather than beside it — a second copy of
445
+ this rule is how `auth status` came to publish a state the commands
446
+ contradicted (P-2).
447
+ """
448
+ global _license_data, is_pro
449
+ _license_data = _load_license_file()
450
+ is_pro = entitlement()["entitlement"] == "pro"
389
451
 
390
452
 
391
453
  _init()
@@ -765,6 +765,15 @@ _BOOT3_MIGRATION_TARGETS: frozenset[str] = frozenset(
765
765
  # reported as a separate hygiene metric, excluded from JDK-modernization scoring.
766
766
  _BEST_PRACTICE_TARGETS: frozenset[str] = frozenset({"java_8_best_practice"})
767
767
 
768
+ #: What a file costs when every finding in it has an OpenRewrite recipe, as a
769
+ #: fraction of the hand-edit rate (C1-11). The recipe does not remove the work —
770
+ #: it turns editing into running one recipe and reviewing the diff.
771
+ #:
772
+ #: This is an **assumption, not a measurement**, and it is published as a field
773
+ #: (`effort_breakdown.mechanical_review_factor`) with that label so a reader can
774
+ #: substitute their own team's number instead of inheriting ours silently.
775
+ _MECHANICAL_REVIEW_FACTOR: float = 0.2
776
+
768
777
  # BUG #3: the migration dimensions that feed the readiness_score aggregate, in
769
778
  # order. jdk_modernization is deliberately NOT here — it is orthogonal upkeep debt
770
779
  # reported on its own axis, never folded into the migration headline.
@@ -1625,12 +1634,45 @@ class MigrationReport:
1625
1634
 
1626
1635
  # BUG #5: effort over MAIN findings only — N/A axes (Hibernate-6 phantom,
1627
1636
  # test fixtures) no longer pad the estimate.
1628
- _file_effort = (
1629
- len(critical_files) * 0.5
1630
- + len(high_files) * 0.25
1631
- + len(medium_files) * 0.1
1632
- + len(low_files) * 0.05
1633
- )
1637
+ #
1638
+ # C1-11 (field eval #4): the estimate used to bill every file at the
1639
+ # hand-edit rate while the report itself shipped an OpenRewrite recipe for
1640
+ # most of the findings in it — on openmrs, 296 of 310 findings carry
1641
+ # `auto_fix_available: true` and all 310 were costed as hand work. A number
1642
+ # that contradicts a field in the same payload is the ADR-0008 defect, so
1643
+ # the estimate now reads that field.
1644
+ #
1645
+ # The unit is the file, because the weights below are per file: a file is
1646
+ # mechanical only when EVERY main finding in it carries a recipe. One
1647
+ # unautomatable finding puts a human in the file, and the rest of that
1648
+ # file's work comes with them.
1649
+ _findings_by_file: "dict[str, list[MigrationFinding]]" = {}
1650
+ for f in main_findings:
1651
+ _findings_by_file.setdefault(f.source_file, []).append(f)
1652
+ _mechanical_files = {
1653
+ path for path, items in _findings_by_file.items()
1654
+ if items and all(bool(i.openrewrite_recipe) for i in items)
1655
+ }
1656
+
1657
+ def _weighted(files: "set[str]") -> float:
1658
+ return (
1659
+ len(critical_files & files) * 0.5
1660
+ + len(high_files & files) * 0.25
1661
+ + len(medium_files & files) * 0.1
1662
+ + len(low_files & files) * 0.05
1663
+ )
1664
+
1665
+ _all_effort_files = critical_files | high_files | medium_files | low_files
1666
+ _manual_effort = _weighted(_all_effort_files - _mechanical_files)
1667
+ # A recipe does not remove the work, it changes what the work is: run the
1668
+ # recipe once, then review the diff. Reviewing is charged at a fraction of
1669
+ # editing. The fraction is an assumption, not a measurement, so it is
1670
+ # published as a field rather than buried in this expression.
1671
+ _mechanical_raw = _weighted(_all_effort_files & _mechanical_files)
1672
+ _mechanical_effort = _mechanical_raw * _MECHANICAL_REVIEW_FACTOR
1673
+ _file_effort = _manual_effort + _mechanical_effort
1674
+ _auto_fixable = sum(1 for f in main_findings if f.openrewrite_recipe)
1675
+ _recipes = sorted({f.openrewrite_recipe for f in main_findings if f.openrewrite_recipe})
1634
1676
  # BUG #1 (v1.70.0): when the Hibernate 5→6 axis APPLIES, fold its measured
1635
1677
  # rewrite effort (risk_matrix → total_effort_range_days) into the headline
1636
1678
  # estimate. Previously a Hibernate-5 project whose ${hibernateVersion} was
@@ -1646,6 +1688,33 @@ class MigrationReport:
1646
1688
  self.estimated_effort_days = round(_file_effort + _hib_effort, 1)
1647
1689
  self.effort_breakdown = {
1648
1690
  "findings_effort_days": round(_file_effort, 1),
1691
+ # C1-11: the same total, split by what the work actually is.
1692
+ "mechanical_days": round(_mechanical_effort, 1),
1693
+ "manual_days": round(_manual_effort, 1),
1694
+ # Not estimated rather than estimated at zero: the re-test scope is a
1695
+ # different measurement (which endpoints the changed files reach), and
1696
+ # `migrate-check --blast-radius` is what produces it. I-3: a fact that
1697
+ # cannot be decided says so.
1698
+ "validation_days": None,
1699
+ "validation_days_note": (
1700
+ "Not estimated here — the re-test scope is the endpoints the changed "
1701
+ "files reach. Run `migrate-check --blast-radius` for that ordering."
1702
+ ),
1703
+ "mechanical_basis": (
1704
+ f"{_auto_fixable} of {len(main_findings)} findings carry an OpenRewrite "
1705
+ f"recipe; {len(_mechanical_files & _all_effort_files)} of "
1706
+ f"{len(_all_effort_files)} affected files have a recipe for *every* "
1707
+ f"finding in them and are costed at {_MECHANICAL_REVIEW_FACTOR:g}× the "
1708
+ "hand-edit rate (run the recipe, review the diff). A file with one "
1709
+ "unautomatable finding is costed manually in full — a human is in that "
1710
+ "file either way."
1711
+ ),
1712
+ "mechanical_review_factor": _MECHANICAL_REVIEW_FACTOR,
1713
+ "mechanical_review_factor_basis": "assumption, not a measurement",
1714
+ "auto_fixable_findings": _auto_fixable,
1715
+ "auto_fixable_files": len(_mechanical_files & _all_effort_files),
1716
+ "affected_files": len(_all_effort_files),
1717
+ "recipes": _recipes,
1649
1718
  "hibernate_rewrite_effort_days": round(_hib_effort, 1),
1650
1719
  "hibernate_rewrite_range": (
1651
1720
  hib.total_effort_range_days if (_hibernate_applies and hib is not None) else None
@@ -1809,7 +1878,20 @@ class MigrationReport:
1809
1878
  f"(critical: {main_crit}, high: {main_high})"
1810
1879
  + (f" [+{nb} in test/generated, non-blocking]" if nb else ""),
1811
1880
  f"Affected files: {self.summary.get('affected_files', 0)}",
1812
- f"Estimated effort: {self.estimated_effort_days}d",
1881
+ f"Estimated effort: {self.estimated_effort_days}d"
1882
+ + (
1883
+ f" ({self.effort_breakdown.get('manual_days', 0)}d manual + "
1884
+ f"{self.effort_breakdown.get('mechanical_days', 0)}d mechanical"
1885
+ + (f" + {self.effort_breakdown.get('hibernate_rewrite_effort_days')}d hibernate"
1886
+ if self.effort_breakdown.get("hibernate_rewrite_effort_days") else "")
1887
+ + "; validation not estimated)"
1888
+ if self.effort_breakdown else ""
1889
+ ),
1890
+ (
1891
+ f" {self.effort_breakdown.get('auto_fixable_findings', 0)} findings carry an "
1892
+ f"OpenRewrite recipe — see effort_breakdown.recipes"
1893
+ if self.effort_breakdown.get("auto_fixable_findings") else ""
1894
+ ),
1813
1895
  "",
1814
1896
  ]
1815
1897