sourcecode 3.1.0__tar.gz → 3.1.1__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.1.0 → sourcecode-3.1.1}/CHANGELOG.md +39 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/PKG-INFO +3 -3
- {sourcecode-3.1.0 → sourcecode-3.1.1}/README.md +2 -2
- {sourcecode-3.1.0 → sourcecode-3.1.1}/pyproject.toml +1 -1
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/__init__.py +1 -1
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/cli.py +74 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/migrate_check.py +22 -1
- sourcecode-3.1.1/src/sourcecode/migration_blast.py +232 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/.gitignore +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/.ruff.toml +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/CONTRIBUTING.md +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/LICENSE +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/SECURITY.md +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/raw +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/scripts/perf_harness.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/archetype.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/cache.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/chain_rules.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/classifier.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/compare.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/deployment_prefix.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/envelope.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/explain.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/filter_surface.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/license.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/parse_cache.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/perf.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/posture.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/progress.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/readiness_timeline.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/redactor.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/ris.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/scanner.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/schema.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/security_config.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/serializer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_profiles.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_properties.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/token_estimate.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/verify_edit.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/verify_repo.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/verify_rules.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/version_check.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/src/sourcecode/workspace.py +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/supabase/functions/README.md +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-3.1.0 → sourcecode-3.1.1}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`migrate-check --blast-radius` states the regression scope behind each blocker.** The
|
|
8
|
+
finding list says what must change; a migration plan is built around what has to be
|
|
9
|
+
re-tested when that change lands. Each affected product file now resolves to the types
|
|
10
|
+
it declares and runs through the same caller traversal `impact-chain` uses, so the
|
|
11
|
+
block reports the HTTP endpoints whose call path passes through it, ranked. Measured on
|
|
12
|
+
BroadleafCommerce: 62.3% of the request surface (137 of 220 endpoints) runs through a
|
|
13
|
+
file with a finding, and 52 of the 166 affected files carry all of it. Implemented as a
|
|
14
|
+
rule over the existing IR — no new detector, ~3 ms per affected file on top of an IR
|
|
15
|
+
build shared with `impact-chain` / `explain` through the context cache.
|
|
16
|
+
- Reach is regression scope, never build breakage: a `javax→jakarta` finding fails
|
|
17
|
+
compilation whether or not any endpoint reaches it, and the payload says so.
|
|
18
|
+
- Unresolved is not zero — a file whose declared types the IR never resolved is listed
|
|
19
|
+
in `files_unresolved` with reach *unknown*.
|
|
20
|
+
- A repository the IR resolves no endpoints in (alfresco's webscript framework,
|
|
21
|
+
openmrs-core) gets `request_surface_share: null` and a note that ranking fell back to
|
|
22
|
+
caller counts, never a manufactured `0.0`.
|
|
23
|
+
- Opt-in and additive: without the flag the report is byte-identical to before, and the
|
|
24
|
+
key is absent rather than `null`.
|
|
25
|
+
|
|
26
|
+
## [3.1.1] — 2026-07-28
|
|
27
|
+
|
|
28
|
+
Documentation only; no behaviour changed.
|
|
29
|
+
|
|
30
|
+
- **`contracts.md` gains an enforcement-maturity section**: which contract kinds have been
|
|
31
|
+
run against real code and with what result, the one false block found and fixed (a
|
|
32
|
+
fully-qualified security annotation reading as no guard, 3.1.0), and what is still
|
|
33
|
+
unproven — the GitHub Actions workflow has never run in a hosted runner. Plus a
|
|
34
|
+
three-week rollout that starts advisory (`--fail-on never`), because a contract whose
|
|
35
|
+
first act is blocking a merge gets deleted.
|
|
36
|
+
- **`migrate-check.md` documents the output-size guard** added in 3.1.0: the measured
|
|
37
|
+
sizes that motivated it, the emitted `OUTPUT_TOO_LARGE` payload, and the four ways past
|
|
38
|
+
it in the order worth trying.
|
|
39
|
+
- Doc-accuracy tests cover the new claims: the rollout flags exist, every documented
|
|
40
|
+
contract kind is implemented, and the quoted guard hint is the one the code emits.
|
|
41
|
+
|
|
3
42
|
## [3.1.0] — 2026-07-28
|
|
4
43
|
|
|
5
44
|
**Posture answers about requests, not only about beans.** Milestone M4 of the strategic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.1
|
|
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
|
|
@@ -139,7 +139,7 @@ The system map: module graph, dependency views, REST surface, per-class summarie
|
|
|
139
139
|
→ [reference](docs/USER_GUIDE.md#core-commands)
|
|
140
140
|
|
|
141
141
|
### 4 · Migration & Modernization
|
|
142
|
-
Is this codebase ready to upgrade? Per-dimension readiness (Jakarta / Spring Boot / JDK / Hibernate), located blockers,
|
|
142
|
+
Is this codebase ready to upgrade? Per-dimension readiness (Jakarta / Spring Boot / JDK / Hibernate), located blockers, an effort estimate, and — with `--blast-radius` — the endpoints whose call path runs through each blocker, so the re-test plan is ordered by regression scope.
|
|
143
143
|
`ask migrate-check` · `ask modernize`
|
|
144
144
|
→ [migrate-check reference](docs/migrate-check.md) · [MODERNIZATION.md](docs/MODERNIZATION.md)
|
|
145
145
|
|
|
@@ -233,7 +233,7 @@ Matching endpoints report `policy: "custom"` and drop out of the `no_security_si
|
|
|
233
233
|
|-----|----------------|
|
|
234
234
|
| [USER_GUIDE.md](docs/USER_GUIDE.md) | Full command reference, flags, output schema, workflows |
|
|
235
235
|
| [contracts.md](docs/contracts.md) | Declare invariants in `.ask/contracts.yml`, enforce them in the edit loop and in CI |
|
|
236
|
-
| [posture.md](docs/posture.md) | What a profile set actually wires
|
|
236
|
+
| [posture.md](docs/posture.md) | What a profile set actually wires — down to which endpoints its filter chain permits — and what could not be decided (experimental) |
|
|
237
237
|
| [migrate-check.md](docs/migrate-check.md) | Migration rule catalogue (MIG-001..043) + Hibernate stratification |
|
|
238
238
|
| [MODERNIZATION.md](docs/MODERNIZATION.md) | The modernization product: assess → understand → plan → execute |
|
|
239
239
|
| [PRODUCT_TIERS.md](docs/PRODUCT_TIERS.md) | Free vs Pro, pricing model |
|
|
@@ -101,7 +101,7 @@ The system map: module graph, dependency views, REST surface, per-class summarie
|
|
|
101
101
|
→ [reference](docs/USER_GUIDE.md#core-commands)
|
|
102
102
|
|
|
103
103
|
### 4 · Migration & Modernization
|
|
104
|
-
Is this codebase ready to upgrade? Per-dimension readiness (Jakarta / Spring Boot / JDK / Hibernate), located blockers,
|
|
104
|
+
Is this codebase ready to upgrade? Per-dimension readiness (Jakarta / Spring Boot / JDK / Hibernate), located blockers, an effort estimate, and — with `--blast-radius` — the endpoints whose call path runs through each blocker, so the re-test plan is ordered by regression scope.
|
|
105
105
|
`ask migrate-check` · `ask modernize`
|
|
106
106
|
→ [migrate-check reference](docs/migrate-check.md) · [MODERNIZATION.md](docs/MODERNIZATION.md)
|
|
107
107
|
|
|
@@ -195,7 +195,7 @@ Matching endpoints report `policy: "custom"` and drop out of the `no_security_si
|
|
|
195
195
|
|-----|----------------|
|
|
196
196
|
| [USER_GUIDE.md](docs/USER_GUIDE.md) | Full command reference, flags, output schema, workflows |
|
|
197
197
|
| [contracts.md](docs/contracts.md) | Declare invariants in `.ask/contracts.yml`, enforce them in the edit loop and in CI |
|
|
198
|
-
| [posture.md](docs/posture.md) | What a profile set actually wires
|
|
198
|
+
| [posture.md](docs/posture.md) | What a profile set actually wires — down to which endpoints its filter chain permits — and what could not be decided (experimental) |
|
|
199
199
|
| [migrate-check.md](docs/migrate-check.md) | Migration rule catalogue (MIG-001..043) + Hibernate stratification |
|
|
200
200
|
| [MODERNIZATION.md](docs/MODERNIZATION.md) | The modernization product: assess → understand → plan → execute |
|
|
201
201
|
| [PRODUCT_TIERS.md](docs/PRODUCT_TIERS.md) | Free vs Pro, pricing model |
|
|
@@ -6201,6 +6201,65 @@ def posture_cmd(
|
|
|
6201
6201
|
# ── Spring Boot Migration Check ───────────────────────────────────────────────
|
|
6202
6202
|
|
|
6203
6203
|
|
|
6204
|
+
def _migration_blast_radius(
|
|
6205
|
+
target: Path,
|
|
6206
|
+
file_list: list[str],
|
|
6207
|
+
report: "object",
|
|
6208
|
+
depth: int,
|
|
6209
|
+
) -> dict:
|
|
6210
|
+
"""Resolve the regression scope behind the migration findings (--blast-radius).
|
|
6211
|
+
|
|
6212
|
+
Reuses the shared context-cache CIR — the same one `impact-chain`, `explain` and
|
|
6213
|
+
`cache warm` use — so a warmed repository skips the Java parse. Every failure mode
|
|
6214
|
+
degrades to a block that says what could not be computed; it never fabricates reach
|
|
6215
|
+
and never breaks the scan that already succeeded.
|
|
6216
|
+
"""
|
|
6217
|
+
from sourcecode.migration_blast import compute_migration_blast, SCHEMA_VERSION
|
|
6218
|
+
|
|
6219
|
+
_depth = max(1, min(int(depth), 8))
|
|
6220
|
+
findings = list(getattr(report, "findings", []) or [])
|
|
6221
|
+
|
|
6222
|
+
def _degraded(reason: str) -> dict:
|
|
6223
|
+
return {
|
|
6224
|
+
"schema_version": SCHEMA_VERSION,
|
|
6225
|
+
"depth": _depth,
|
|
6226
|
+
"status": "unavailable",
|
|
6227
|
+
"endpoints_in_repo": 0,
|
|
6228
|
+
"endpoints_reached": 0,
|
|
6229
|
+
"request_surface_share": None,
|
|
6230
|
+
"files_analyzed": 0,
|
|
6231
|
+
"files_on_request_path": 0,
|
|
6232
|
+
"files_off_request_path": 0,
|
|
6233
|
+
"files_unresolved": [],
|
|
6234
|
+
"files": [],
|
|
6235
|
+
"files_omitted": 0,
|
|
6236
|
+
"notes": [reason],
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
if not file_list:
|
|
6240
|
+
return _degraded(
|
|
6241
|
+
"No Java sources in this repository — there is no call graph to traverse, "
|
|
6242
|
+
"so no regression scope can be attributed."
|
|
6243
|
+
)
|
|
6244
|
+
|
|
6245
|
+
try:
|
|
6246
|
+
from sourcecode import context_cache as _ctxcache
|
|
6247
|
+
from sourcecode.context_graph import ContextGraph
|
|
6248
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
6249
|
+
|
|
6250
|
+
try:
|
|
6251
|
+
cir, _ = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, file_list)
|
|
6252
|
+
except Exception:
|
|
6253
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
6254
|
+
model = SpringSemanticModel.build(cir)
|
|
6255
|
+
return compute_migration_blast(cir, model, findings, depth=_depth)
|
|
6256
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
6257
|
+
return _degraded(
|
|
6258
|
+
f"Blast radius unavailable: {type(exc).__name__} while building the "
|
|
6259
|
+
"repository IR. The migration findings above are unaffected."
|
|
6260
|
+
)
|
|
6261
|
+
|
|
6262
|
+
|
|
6204
6263
|
@app.command("migrate-check")
|
|
6205
6264
|
def migrate_check_cmd(
|
|
6206
6265
|
path: Path = typer.Argument(
|
|
@@ -6265,6 +6324,18 @@ def migrate_check_cmd(
|
|
|
6265
6324
|
False, "--force",
|
|
6266
6325
|
help="Emit to stdout even when the report exceeds the output-size guard.",
|
|
6267
6326
|
),
|
|
6327
|
+
blast_radius: bool = typer.Option(
|
|
6328
|
+
False, "--blast-radius",
|
|
6329
|
+
help=(
|
|
6330
|
+
"Rank affected product files by the HTTP endpoints whose call path runs "
|
|
6331
|
+
"through them (regression scope), using the same caller traversal as "
|
|
6332
|
+
"impact-chain. Builds the repository IR — slower than a plain scan."
|
|
6333
|
+
),
|
|
6334
|
+
),
|
|
6335
|
+
depth: int = typer.Option(
|
|
6336
|
+
4, "--depth",
|
|
6337
|
+
help="Caller BFS depth for --blast-radius (1-8, default: 4).",
|
|
6338
|
+
),
|
|
6268
6339
|
) -> None:
|
|
6269
6340
|
"""Spring Boot 2→3 migration readiness: detect javax→jakarta namespace blockers.
|
|
6270
6341
|
|
|
@@ -6366,6 +6437,9 @@ def migrate_check_cmd(
|
|
|
6366
6437
|
if _file_limitations:
|
|
6367
6438
|
report.limitations.extend(_file_limitations)
|
|
6368
6439
|
|
|
6440
|
+
if blast_radius:
|
|
6441
|
+
report.blast_radius = _migration_blast_radius(target, file_list, report, depth)
|
|
6442
|
+
|
|
6369
6443
|
if format == "text":
|
|
6370
6444
|
output = report.to_text(min_severity=min_severity)
|
|
6371
6445
|
else:
|
|
@@ -1402,6 +1402,11 @@ class MigrationReport:
|
|
|
1402
1402
|
# exposure map, call-chain detection, upgrade-vs-rewrite verdict). Attached
|
|
1403
1403
|
# by run_migrate_check; None when not computed.
|
|
1404
1404
|
hibernate: Optional["HibernateStratification"] = None
|
|
1405
|
+
# Regression scope behind the findings: the endpoints whose call path runs
|
|
1406
|
+
# through each affected file (migration_blast.compute_migration_blast).
|
|
1407
|
+
# Opt-in — attached by the CLI under --blast-radius; None when not computed,
|
|
1408
|
+
# and then absent from the payload so the default report stays byte-identical.
|
|
1409
|
+
blast_radius: Optional[dict] = None
|
|
1405
1410
|
|
|
1406
1411
|
def finalize(self) -> "MigrationReport":
|
|
1407
1412
|
if not self.generated_at:
|
|
@@ -1682,7 +1687,7 @@ class MigrationReport:
|
|
|
1682
1687
|
return self
|
|
1683
1688
|
|
|
1684
1689
|
def to_dict(self) -> dict:
|
|
1685
|
-
|
|
1690
|
+
d: dict = {
|
|
1686
1691
|
"schema_version": self.schema_version,
|
|
1687
1692
|
"generated_at": self.generated_at,
|
|
1688
1693
|
"repo_id": self.repo_id,
|
|
@@ -1718,6 +1723,11 @@ class MigrationReport:
|
|
|
1718
1723
|
"limitations": self.limitations,
|
|
1719
1724
|
"metadata": self.metadata,
|
|
1720
1725
|
}
|
|
1726
|
+
# Additive and opt-in: the key appears only when --blast-radius computed it,
|
|
1727
|
+
# so a default run emits exactly the payload it emitted before.
|
|
1728
|
+
if self.blast_radius is not None:
|
|
1729
|
+
d["blast_radius"] = self.blast_radius
|
|
1730
|
+
return d
|
|
1721
1731
|
|
|
1722
1732
|
def to_compact_dict(self, top_n: int = _COMPACT_TOP_N) -> dict:
|
|
1723
1733
|
"""Bounded, decision-grade projection of `to_dict()`.
|
|
@@ -1754,6 +1764,11 @@ class MigrationReport:
|
|
|
1754
1764
|
if isinstance(h.get(_col), list):
|
|
1755
1765
|
h[_col] = _cap_collection(h[_col], top_n)
|
|
1756
1766
|
compact["hibernate"] = h
|
|
1767
|
+
blast = full.get("blast_radius")
|
|
1768
|
+
if isinstance(blast, dict) and isinstance(blast.get("files"), list):
|
|
1769
|
+
b = dict(blast)
|
|
1770
|
+
b["files"] = _cap_collection(b["files"], top_n)
|
|
1771
|
+
compact["blast_radius"] = b
|
|
1757
1772
|
return compact
|
|
1758
1773
|
|
|
1759
1774
|
def to_text(self, min_severity: str = "low") -> str:
|
|
@@ -1802,6 +1817,12 @@ class MigrationReport:
|
|
|
1802
1817
|
lines.append(self.hibernate.to_text())
|
|
1803
1818
|
lines.append("")
|
|
1804
1819
|
|
|
1820
|
+
if self.blast_radius is not None:
|
|
1821
|
+
from sourcecode.migration_blast import render_blast_text
|
|
1822
|
+
|
|
1823
|
+
lines.append(render_blast_text(self.blast_radius))
|
|
1824
|
+
lines.append("")
|
|
1825
|
+
|
|
1805
1826
|
if not visible:
|
|
1806
1827
|
lines.append("No findings at or above selected severity.")
|
|
1807
1828
|
return "\n".join(lines)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Migration blast radius — which request surface a migration finding sits under.
|
|
2
|
+
|
|
3
|
+
`migrate-check` answers *what must change*: the files whose `javax.*` imports, XML
|
|
4
|
+
descriptors and build coordinates block a Spring Boot 2→3 upgrade. It does not answer
|
|
5
|
+
*what has to be re-tested when that change lands*, which is the question a migration
|
|
6
|
+
plan is actually built around.
|
|
7
|
+
|
|
8
|
+
This module answers it as a rule over the existing IR — no new detector, no second
|
|
9
|
+
traversal engine. Each affected file resolves to the types it declares, those types are
|
|
10
|
+
run through the same caller BFS `impact-chain` uses (`spring_impact._bfs_callers` +
|
|
11
|
+
`_collect_endpoints`), and the reachable HTTP endpoints become that file's re-test set.
|
|
12
|
+
|
|
13
|
+
Honesty contract:
|
|
14
|
+
* A `javax→jakarta` finding breaks compilation whether or not an endpoint reaches it.
|
|
15
|
+
`endpoints_reached` ranks *regression scope*, never "what fails to build".
|
|
16
|
+
* A file whose declared types the IR never resolved is reported in `files_unresolved`
|
|
17
|
+
with reach `unknown` — never folded into the zero-reach bucket.
|
|
18
|
+
* A repository the IR sees no endpoints in gets `request_surface_share: null` and a
|
|
19
|
+
note saying ranking fell back to caller counts, never a manufactured 0.0.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
25
|
+
|
|
26
|
+
from sourcecode.spring_impact import _bfs_callers, _collect_endpoints
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
29
|
+
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
30
|
+
from sourcecode.migrate_check import MigrationFinding
|
|
31
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
32
|
+
|
|
33
|
+
SCHEMA_VERSION = "1.0"
|
|
34
|
+
|
|
35
|
+
DEFAULT_DEPTH = 4
|
|
36
|
+
DEFAULT_TOP_N = 25
|
|
37
|
+
_ENDPOINTS_PER_FILE = 10
|
|
38
|
+
|
|
39
|
+
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
40
|
+
_TYPE_KINDS = ("class", "interface", "enum", "annotation")
|
|
41
|
+
|
|
42
|
+
_REACH_NOTE = (
|
|
43
|
+
"endpoints_reached ranks REGRESSION SCOPE — the requests whose call path runs "
|
|
44
|
+
"through the file — not what fails to build. A javax→jakarta finding breaks "
|
|
45
|
+
"compilation whether or not any endpoint reaches it."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _types_by_file(cir: "CanonicalRepositoryIR") -> dict[str, list[str]]:
|
|
50
|
+
"""Map source file → the type FQNs declared in it, from the IR's graph nodes."""
|
|
51
|
+
raw = getattr(cir, "_raw_ir", None) or {}
|
|
52
|
+
nodes = (raw.get("graph") or {}).get("nodes") or []
|
|
53
|
+
out: dict[str, list[str]] = {}
|
|
54
|
+
for node in nodes:
|
|
55
|
+
if not isinstance(node, dict):
|
|
56
|
+
continue
|
|
57
|
+
src = node.get("source_file") or ""
|
|
58
|
+
fqn = node.get("fqn") or ""
|
|
59
|
+
if not src or not fqn or "#" in fqn:
|
|
60
|
+
continue
|
|
61
|
+
if (node.get("symbol_kind") or node.get("type")) not in _TYPE_KINDS:
|
|
62
|
+
continue
|
|
63
|
+
out.setdefault(src, []).append(fqn)
|
|
64
|
+
for src in out:
|
|
65
|
+
out[src] = sorted(set(out[src]))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _max_severity(findings: list["MigrationFinding"]) -> str:
|
|
70
|
+
return min(
|
|
71
|
+
(f.severity for f in findings),
|
|
72
|
+
key=lambda s: _SEVERITY_ORDER.get(s, 3),
|
|
73
|
+
default="low",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compute_migration_blast(
|
|
78
|
+
cir: "CanonicalRepositoryIR",
|
|
79
|
+
model: "SpringSemanticModel",
|
|
80
|
+
findings: list["MigrationFinding"],
|
|
81
|
+
*,
|
|
82
|
+
depth: int = DEFAULT_DEPTH,
|
|
83
|
+
top_n: int = DEFAULT_TOP_N,
|
|
84
|
+
) -> dict:
|
|
85
|
+
"""Resolve the request surface behind each migration finding.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
cir: CanonicalRepositoryIR for the same repository that was scanned.
|
|
89
|
+
model: SpringSemanticModel built from that CIR (endpoint index).
|
|
90
|
+
findings: MigrationFinding list from `run_migrate_check`.
|
|
91
|
+
depth: Caller BFS depth, the same knob `impact-chain --depth` exposes.
|
|
92
|
+
top_n: Files retained in `files`; the rest are counted in `files_omitted`.
|
|
93
|
+
|
|
94
|
+
Returns a JSON-serializable dict. Never raises on a partial IR: anything it
|
|
95
|
+
could not resolve is reported as unresolved rather than dropped.
|
|
96
|
+
"""
|
|
97
|
+
by_file: dict[str, list["MigrationFinding"]] = {}
|
|
98
|
+
non_source = 0
|
|
99
|
+
non_product = 0
|
|
100
|
+
for f in findings:
|
|
101
|
+
src = getattr(f, "source_file", "") or ""
|
|
102
|
+
if not src.endswith(".java"):
|
|
103
|
+
# XML descriptors and build coordinates carry no type the call graph
|
|
104
|
+
# can reach; they are counted, never ranked.
|
|
105
|
+
non_source += 1
|
|
106
|
+
continue
|
|
107
|
+
if getattr(f, "code_context", "main") != "main":
|
|
108
|
+
# Test and generated sources are outside the product blast radius, the
|
|
109
|
+
# same boundary blocking_count draws.
|
|
110
|
+
non_product += 1
|
|
111
|
+
continue
|
|
112
|
+
by_file.setdefault(src, []).append(f)
|
|
113
|
+
|
|
114
|
+
declared = _types_by_file(cir)
|
|
115
|
+
impl_graph = getattr(cir, "implementation_graph", None)
|
|
116
|
+
reverse_graph = getattr(cir, "reverse_graph", None) or {}
|
|
117
|
+
endpoints_in_repo = len(getattr(cir, "endpoints", None) or [])
|
|
118
|
+
|
|
119
|
+
rows: list[dict[str, Any]] = []
|
|
120
|
+
unresolved: list[str] = []
|
|
121
|
+
reached_ids: set[str] = set()
|
|
122
|
+
|
|
123
|
+
for src in sorted(by_file):
|
|
124
|
+
seeds = declared.get(src) or []
|
|
125
|
+
if not seeds:
|
|
126
|
+
unresolved.append(src)
|
|
127
|
+
continue
|
|
128
|
+
direct, indirect, truncated, *_rest = _bfs_callers(
|
|
129
|
+
seeds, reverse_graph, depth, impl_graph=impl_graph,
|
|
130
|
+
)
|
|
131
|
+
endpoints = _collect_endpoints(list(direct) + list(indirect), seeds, model)
|
|
132
|
+
for ep in endpoints:
|
|
133
|
+
reached_ids.add(ep.endpoint_id)
|
|
134
|
+
file_findings = by_file[src]
|
|
135
|
+
shown = sorted(
|
|
136
|
+
(e.to_dict() for e in endpoints),
|
|
137
|
+
key=lambda e: (e["path"], e["method"], e["endpoint_id"]),
|
|
138
|
+
)
|
|
139
|
+
rows.append({
|
|
140
|
+
"file": src,
|
|
141
|
+
"max_severity": _max_severity(file_findings),
|
|
142
|
+
"finding_count": len(file_findings),
|
|
143
|
+
"rule_ids": sorted({f.rule_id for f in file_findings}),
|
|
144
|
+
"types": seeds,
|
|
145
|
+
"direct_callers": len(direct),
|
|
146
|
+
"indirect_callers": len(indirect),
|
|
147
|
+
"traversal_truncated": bool(truncated),
|
|
148
|
+
"endpoints_reached": len(endpoints),
|
|
149
|
+
"endpoints": shown[:_ENDPOINTS_PER_FILE],
|
|
150
|
+
"endpoints_omitted": max(0, len(shown) - _ENDPOINTS_PER_FILE),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
rows.sort(key=lambda r: (
|
|
154
|
+
-r["endpoints_reached"],
|
|
155
|
+
-(r["direct_callers"] + r["indirect_callers"]),
|
|
156
|
+
_SEVERITY_ORDER.get(r["max_severity"], 3),
|
|
157
|
+
r["file"],
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
on_path = sum(1 for r in rows if r["endpoints_reached"] > 0)
|
|
161
|
+
notes: list[str] = [_REACH_NOTE]
|
|
162
|
+
if endpoints_in_repo:
|
|
163
|
+
share = round(len(reached_ids) / endpoints_in_repo, 4)
|
|
164
|
+
else:
|
|
165
|
+
share = None
|
|
166
|
+
notes.append(
|
|
167
|
+
"The IR resolved no HTTP endpoints in this repository, so no request "
|
|
168
|
+
"surface can be attributed — files are ranked by caller count alone."
|
|
169
|
+
)
|
|
170
|
+
if unresolved:
|
|
171
|
+
notes.append(
|
|
172
|
+
f"{len(unresolved)} file(s) carry a finding but declare no type the IR "
|
|
173
|
+
"resolved (parse gap or non-type source); their reach is unknown, not zero."
|
|
174
|
+
)
|
|
175
|
+
if any(r["traversal_truncated"] for r in rows):
|
|
176
|
+
notes.append(
|
|
177
|
+
"At least one traversal hit the hub-class guard and was capped — those "
|
|
178
|
+
"files' reach is a lower bound."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"schema_version": SCHEMA_VERSION,
|
|
183
|
+
"depth": depth,
|
|
184
|
+
"endpoints_in_repo": endpoints_in_repo,
|
|
185
|
+
"endpoints_reached": len(reached_ids),
|
|
186
|
+
"request_surface_share": share,
|
|
187
|
+
"files_analyzed": len(rows),
|
|
188
|
+
"files_on_request_path": on_path,
|
|
189
|
+
"files_off_request_path": len(rows) - on_path,
|
|
190
|
+
"files_unresolved": sorted(unresolved),
|
|
191
|
+
"findings_not_source": non_source,
|
|
192
|
+
"findings_not_product": non_product,
|
|
193
|
+
"files": rows[:top_n],
|
|
194
|
+
"files_omitted": max(0, len(rows) - top_n),
|
|
195
|
+
"notes": notes,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def render_blast_text(blast: Optional[dict], *, top_n: int = 10) -> str:
|
|
200
|
+
"""Render the blast-radius block as the text-format section."""
|
|
201
|
+
if not blast:
|
|
202
|
+
return ""
|
|
203
|
+
lines: list[str] = ["Migration blast radius (regression scope)"]
|
|
204
|
+
share = blast.get("request_surface_share")
|
|
205
|
+
if share is None:
|
|
206
|
+
lines.append(
|
|
207
|
+
f" {blast.get('endpoints_in_repo', 0)} endpoints in IR — "
|
|
208
|
+
"no request surface attributable; ranked by callers."
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
lines.append(
|
|
212
|
+
f" {blast.get('endpoints_reached', 0)}/{blast.get('endpoints_in_repo', 0)} "
|
|
213
|
+
f"endpoints reached ({share * 100:.1f}% of the request surface)"
|
|
214
|
+
)
|
|
215
|
+
lines.append(
|
|
216
|
+
f" Files on a request path: {blast.get('files_on_request_path', 0)}"
|
|
217
|
+
f" of {blast.get('files_analyzed', 0)} analyzed"
|
|
218
|
+
+ (f" [{len(blast.get('files_unresolved') or [])} unresolved]"
|
|
219
|
+
if blast.get("files_unresolved") else "")
|
|
220
|
+
)
|
|
221
|
+
for row in (blast.get("files") or [])[:top_n]:
|
|
222
|
+
lines.append(
|
|
223
|
+
f" {row['endpoints_reached']:>4} endpoints "
|
|
224
|
+
f"[{row['max_severity'].upper()}] {row['file']} "
|
|
225
|
+
f"({row['direct_callers']}+{row['indirect_callers']} callers)"
|
|
226
|
+
)
|
|
227
|
+
omitted = blast.get("files_omitted", 0) + max(0, len(blast.get("files") or []) - top_n)
|
|
228
|
+
if omitted:
|
|
229
|
+
lines.append(f" +{omitted} more file(s)")
|
|
230
|
+
for note in blast.get("notes") or []:
|
|
231
|
+
lines.append(f" note: {note}")
|
|
232
|
+
return "\n".join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|