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.
- {sourcecode-3.5.0 → sourcecode-3.6.0}/CHANGELOG.md +166 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/PKG-INFO +1 -1
- {sourcecode-3.5.0 → sourcecode-3.6.0}/pyproject.toml +1 -1
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/cli.py +248 -34
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/compare.py +70 -5
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/hibernate_strat.py +60 -7
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/migrate_check.py +244 -19
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/output_budget.py +28 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/repository_ir.py +96 -15
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/serializer.py +81 -8
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_findings.py +38 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_security_audit.py +6 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_tx_analyzer.py +6 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/verify_repo.py +22 -4
- {sourcecode-3.5.0 → sourcecode-3.6.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/.gitignore +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/.ruff.toml +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/CONTRIBUTING.md +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/LICENSE +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/README.md +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/SECURITY.md +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/raw +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/scripts/perf_harness.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/archetype.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/cache_model.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/caller_reach.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/chain_rules.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/degradation.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/deployment_prefix.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/envelope.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/facts/__init__.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/facts/registry.json +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/filter_surface.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/integration_coordinates.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/license.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/migration_blast.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/parse_cache.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/path_admission.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/perf.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/posture.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/readiness_timeline.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/reference_facts.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_profiles.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_properties.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/test_sources.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/token_estimate.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/verify_edit.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/verify_rules.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/functions/README.md +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-3.5.0 → sourcecode-3.6.0}/supabase/sql/licensing_schema.sql +0 -0
- {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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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
|
-
|
|
4337
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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
|
|
6190
|
-
analysed — never a silent pass). Same codes as
|
|
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=
|
|
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(
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
8787
|
-
|
|
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
|
|