sourcecode 3.3.0__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sourcecode-3.3.0 → sourcecode-3.4.0}/CHANGELOG.md +92 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/PKG-INFO +8 -5
- {sourcecode-3.3.0 → sourcecode-3.4.0}/README.md +7 -4
- {sourcecode-3.3.0 → sourcecode-3.4.0}/pyproject.toml +1 -1
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/cli.py +8 -3
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/migrate_check.py +89 -7
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/verify_edit.py +190 -7
- {sourcecode-3.3.0 → sourcecode-3.4.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/.gitignore +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/.ruff.toml +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/CONTRIBUTING.md +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/LICENSE +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/SECURITY.md +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/raw +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/scripts/perf_harness.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/archetype.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/cache_model.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/chain_rules.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/compare.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/deployment_prefix.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/envelope.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/facts/__init__.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/facts/registry.json +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/filter_surface.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/integration_coordinates.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/license.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/migration_blast.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/parse_cache.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/perf.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/posture.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/readiness_timeline.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/reference_facts.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_profiles.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_properties.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/test_sources.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/token_estimate.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/verify_repo.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/verify_rules.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/supabase/functions/README.md +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-3.3.0 → sourcecode-3.4.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -2,6 +2,98 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.4.0] — 2026-07-29
|
|
6
|
+
|
|
7
|
+
**Two of the five reconciliation gates, and one commercial surface that answered five ways.**
|
|
8
|
+
The theme of field evaluation #4 was not missing analysis: it was analysis the product had
|
|
9
|
+
already done and did not consult — a migration estimate that ignored its own
|
|
10
|
+
`auto_fix_available`, and a gate that reported `pass` for a file it never looked at.
|
|
11
|
+
|
|
12
|
+
**A minor, not a patch, and for one reason: `verify-edit` can now exit 2 where it exited 0.**
|
|
13
|
+
No API changed, but a build-configuration-only edit stops being a green light, so an installed
|
|
14
|
+
hook or a pipeline that gates on the exit code will start blocking on it. A patch release must
|
|
15
|
+
never be able to turn a green pipeline red.
|
|
16
|
+
|
|
17
|
+
**Upgrade notes.**
|
|
18
|
+
|
|
19
|
+
- `verify-edit`: a change to `pom.xml`, `build.gradle`, `application*.yml`, `web.xml` or a
|
|
20
|
+
container descriptor now yields `unverified` (exit 2). `git commit --no-verify` and
|
|
21
|
+
`ask verify-edit --no-ci` are the escape hatches.
|
|
22
|
+
- `migrate-check`: published effort numbers **move down** — the same work, split by what it
|
|
23
|
+
actually is. Measured: openmrs-core 122.1 → 76.4 d, BroadleafCommerce 163.3 → 157.7,
|
|
24
|
+
alfresco 22.8 → 5.2. `blocking_count` and `readiness_score` are unchanged.
|
|
25
|
+
- `auth status`: new authoritative fields; `pro`, `pro_effective`, `pro_gating` and `pro_reason`
|
|
26
|
+
still ship, now derived from the authority and listed in `deprecated_fields`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **`verify-edit` sees the files that decide what the code compiles to** (C3-14, the second
|
|
31
|
+
reconciliation gate). Editing a `pom.xml` produced `changed_files: []`, `verdict: pass`,
|
|
32
|
+
`exit_code: 0` — and `blast_radius: {"confidence": "high", "note": "no changed files"}`.
|
|
33
|
+
git reported the file; the gate did not look at it, then said everything was fine about a
|
|
34
|
+
change that moves every source file in the repository at once.
|
|
35
|
+
- New blocking axis **`build_config_changed`**, and a new `changed_build_files` list beside
|
|
36
|
+
`changed_files`. It names what moved: `effective_compiler_release` (from → to) and
|
|
37
|
+
`dependency_delta` per coordinate.
|
|
38
|
+
- **The verdict is `unverified`, not `break`.** A build edit is neither a proven regression
|
|
39
|
+
nor computed-clean, which is exactly what `changed: null` has always meant here — so it
|
|
40
|
+
flows through the existing "could not compute" path and exits 2. No special case.
|
|
41
|
+
- Admission is by file name and deliberately narrow — `pom.xml`, `build.gradle`,
|
|
42
|
+
`settings.gradle`, `gradle.properties`, `application*.yml|properties`, `bootstrap*`,
|
|
43
|
+
`web.xml`, `weblogic*.xml`, `jboss-*.xml`, `*-context.xml`, `persistence.xml`, `beans.xml`.
|
|
44
|
+
`messages.properties` and `logback.xml` are out: every admitted file degrades a verdict, and
|
|
45
|
+
a loose rule turns the gate into noise people learn to ignore.
|
|
46
|
+
- `blast_radius` stops reporting `confidence: "high"` over a change whose reach it has not
|
|
47
|
+
measured.
|
|
48
|
+
- **Upgrade note (behaviour change):** a build-configuration-only edit now exits **2**
|
|
49
|
+
(unverified) where it exited 0 (pass). An installed pre-commit/pre-push hook will block on
|
|
50
|
+
such an edit; `git commit --no-verify` remains the escape hatch, and `--no-ci` still reports
|
|
51
|
+
without gating.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- **The migration estimate reads the report's own `auto_fix_available`** (C1-11, the first of
|
|
56
|
+
the five reconciliation gates from field evaluation #4). `migrate-check` shipped an
|
|
57
|
+
OpenRewrite recipe for most findings and then billed every one of them at the hand-edit rate.
|
|
58
|
+
Reproduced on the battery before fixing: **openmrs-core carries a recipe for 296 of 310
|
|
59
|
+
findings and costed all 310 as hand work**, for a headline of 122.1 person-days.
|
|
60
|
+
- `effort_breakdown` splits the same total into **`manual_days`** + **`mechanical_days`** +
|
|
61
|
+
`hibernate_rewrite_effort_days`. The partition is **by file**, because the severity weights
|
|
62
|
+
are per file: a file is mechanical only when *every* finding in it has a recipe, and one
|
|
63
|
+
unautomatable finding costs the whole file manually — a human is in that file either way.
|
|
64
|
+
- **The review factor is published, not buried.** `mechanical_review_factor` (0.2) carries
|
|
65
|
+
`mechanical_review_factor_basis: "assumption, not a measurement"`, so a team can substitute
|
|
66
|
+
their own number instead of inheriting ours silently. `recipes[]` lists what would run and
|
|
67
|
+
`mechanical_basis` states both denominators.
|
|
68
|
+
- **`validation_days` is `null` with a note, never `0`** — the re-test scope is a different
|
|
69
|
+
measurement (`migrate-check --blast-radius`), and `0` would read as "no re-testing needed"
|
|
70
|
+
(invariant I-3).
|
|
71
|
+
- **Published numbers move.** Measured: openmrs-core 122.1 → **76.4 d**, BroadleafCommerce
|
|
72
|
+
163.3 → **157.7 d**, alfresco 22.8 → **5.2 d**. `blocking_count` and `readiness_score` are
|
|
73
|
+
unchanged in all three — the split changes what the work is called, never what blocks.
|
|
74
|
+
|
|
75
|
+
- **`ask auth status` answers one question instead of five** (P-2). It used to publish
|
|
76
|
+
`{"status": "unauthenticated", "pro": true, "pro_reason": "early-adoption unlock"}` — every
|
|
77
|
+
field true on its own axis, and together unreadable: a buyer could not tell what they have,
|
|
78
|
+
why they have it, or what changes when it ends.
|
|
79
|
+
- One authority, `license.entitlement()`, now answers it: **`entitlement`** (`pro`/`free` —
|
|
80
|
+
what runs today), **`source`** (why: `license_key`, `early_adoption_unlock`, `free_tier`),
|
|
81
|
+
**`authenticated`** (whether a credential exists here — a *separate* fact, since today one
|
|
82
|
+
can be entitled without one), **`paywall_active`**, **`when_it_changes`** (what you lose
|
|
83
|
+
when that source stops applying) and **`free_repo_java_file_limit`** (the number the gate
|
|
84
|
+
actually compares against).
|
|
85
|
+
- `when_it_changes` states the gate in the terms the gate uses and **never quotes a price** —
|
|
86
|
+
pricing lives in `docs/PRODUCT_TIERS.md`, and a number printed here would be stale the day
|
|
87
|
+
it moves. Asserted.
|
|
88
|
+
- **`is_pro` is derived from the same authority.** It is what every gate reads
|
|
89
|
+
(`can_use`, `require_feature`), and it used to be computed beside the status block rather
|
|
90
|
+
than from it — which is how a status page came to contradict the commands next to it.
|
|
91
|
+
- `pro`, `pro_effective`, `pro_gating` and `pro_reason` are still emitted for existing
|
|
92
|
+
consumers, are derived from the authority (so they cannot drift), and are listed in the new
|
|
93
|
+
`deprecated_fields`. They are removed in a future major.
|
|
94
|
+
- `plan_status` no longer reports `unknown` for a licence file with no explicit status while
|
|
95
|
+
the authority reads that same file as active.
|
|
96
|
+
|
|
5
97
|
## [3.3.0] — 2026-07-29
|
|
6
98
|
|
|
7
99
|
**A default you must remember to disable is the wrong default for someone else's code.**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -283,10 +283,13 @@ Honest limits worth knowing before you rely on it:
|
|
|
283
283
|
> full Pro entitlements — no size gate, no key. The tiers below describe the model the
|
|
284
284
|
> paywall will return to later.
|
|
285
285
|
>
|
|
286
|
-
> **What that means concretely.** `ask auth status`
|
|
287
|
-
>
|
|
288
|
-
>
|
|
289
|
-
>
|
|
286
|
+
> **What that means concretely.** Ask the product: `ask auth status` answers in one block —
|
|
287
|
+
> `entitlement` (what runs today), `source` (*why* — a licence, the unlock, or the free tier),
|
|
288
|
+
> `authenticated` (whether a credential exists, which is a separate fact) and
|
|
289
|
+
> `when_it_changes` (what you lose when that source stops applying). A fresh install reads
|
|
290
|
+
> `entitlement: pro`, `source: early_adoption_unlock`, `authenticated: false` — unauthenticated
|
|
291
|
+
> *and* entitled, stated as two facts instead of one contradiction. When the unlock ends,
|
|
292
|
+
> gating returns **by repo size and automation, never by command**: `posture`, `endpoints`,
|
|
290
293
|
> `spring-audit` and `migrate-check` stay in the base tier at full output. Nothing you can run
|
|
291
294
|
> today becomes a paid-only command tomorrow.
|
|
292
295
|
|
|
@@ -245,10 +245,13 @@ Honest limits worth knowing before you rely on it:
|
|
|
245
245
|
> full Pro entitlements — no size gate, no key. The tiers below describe the model the
|
|
246
246
|
> paywall will return to later.
|
|
247
247
|
>
|
|
248
|
-
> **What that means concretely.** `ask auth status`
|
|
249
|
-
>
|
|
250
|
-
>
|
|
251
|
-
>
|
|
248
|
+
> **What that means concretely.** Ask the product: `ask auth status` answers in one block —
|
|
249
|
+
> `entitlement` (what runs today), `source` (*why* — a licence, the unlock, or the free tier),
|
|
250
|
+
> `authenticated` (whether a credential exists, which is a separate fact) and
|
|
251
|
+
> `when_it_changes` (what you lose when that source stops applying). A fresh install reads
|
|
252
|
+
> `entitlement: pro`, `source: early_adoption_unlock`, `authenticated: false` — unauthenticated
|
|
253
|
+
> *and* entitled, stated as two facts instead of one contradiction. When the unlock ends,
|
|
254
|
+
> gating returns **by repo size and automation, never by command**: `posture`, `endpoints`,
|
|
252
255
|
> `spring-audit` and `migrate-check` stay in the base tier at full output. Nothing you can run
|
|
253
256
|
> today becomes a paid-only command tomorrow.
|
|
254
257
|
|
|
@@ -6106,9 +6106,14 @@ def verify_edit_cmd(
|
|
|
6106
6106
|
|
|
6107
6107
|
Deterministic, in-loop verdict over the diff of the working tree against HEAD:
|
|
6108
6108
|
public contract, @Transactional proxy boundary, security surface, bean wiring,
|
|
6109
|
-
custom domain rules, and blast radius. Built for the
|
|
6110
|
-
git hooks. Only regressions the edit INTRODUCED (vs
|
|
6111
|
-
debt never fails an unrelated commit.
|
|
6109
|
+
custom domain rules, build/configuration changes, and blast radius. Built for the
|
|
6110
|
+
agent write-loop (MCP) and git hooks. Only regressions the edit INTRODUCED (vs
|
|
6111
|
+
HEAD) block — pre-existing debt never fails an unrelated commit.
|
|
6112
|
+
|
|
6113
|
+
Build and configuration files (pom.xml, build.gradle, application*.yml, web.xml,
|
|
6114
|
+
container descriptors, Spring XML) are reported under `changed_build_files` and
|
|
6115
|
+
make the verdict UNVERIFIED, never `pass`: their effect is not decidable from a
|
|
6116
|
+
source diff, and a compiler-release bump moves every file in the repo at once.
|
|
6112
6117
|
|
|
6113
6118
|
Custom rules: drop a `.ask/verify-rules.json` at the repo root to enforce your own
|
|
6114
6119
|
invariants (e.g. "no @RestController injects a *DaoJpa", "every /payroll endpoint
|
|
@@ -765,6 +765,15 @@ _BOOT3_MIGRATION_TARGETS: frozenset[str] = frozenset(
|
|
|
765
765
|
# reported as a separate hygiene metric, excluded from JDK-modernization scoring.
|
|
766
766
|
_BEST_PRACTICE_TARGETS: frozenset[str] = frozenset({"java_8_best_practice"})
|
|
767
767
|
|
|
768
|
+
#: What a file costs when every finding in it has an OpenRewrite recipe, as a
|
|
769
|
+
#: fraction of the hand-edit rate (C1-11). The recipe does not remove the work —
|
|
770
|
+
#: it turns editing into running one recipe and reviewing the diff.
|
|
771
|
+
#:
|
|
772
|
+
#: This is an **assumption, not a measurement**, and it is published as a field
|
|
773
|
+
#: (`effort_breakdown.mechanical_review_factor`) with that label so a reader can
|
|
774
|
+
#: substitute their own team's number instead of inheriting ours silently.
|
|
775
|
+
_MECHANICAL_REVIEW_FACTOR: float = 0.2
|
|
776
|
+
|
|
768
777
|
# BUG #3: the migration dimensions that feed the readiness_score aggregate, in
|
|
769
778
|
# order. jdk_modernization is deliberately NOT here — it is orthogonal upkeep debt
|
|
770
779
|
# reported on its own axis, never folded into the migration headline.
|
|
@@ -1625,12 +1634,45 @@ class MigrationReport:
|
|
|
1625
1634
|
|
|
1626
1635
|
# BUG #5: effort over MAIN findings only — N/A axes (Hibernate-6 phantom,
|
|
1627
1636
|
# test fixtures) no longer pad the estimate.
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1637
|
+
#
|
|
1638
|
+
# C1-11 (field eval #4): the estimate used to bill every file at the
|
|
1639
|
+
# hand-edit rate while the report itself shipped an OpenRewrite recipe for
|
|
1640
|
+
# most of the findings in it — on openmrs, 296 of 310 findings carry
|
|
1641
|
+
# `auto_fix_available: true` and all 310 were costed as hand work. A number
|
|
1642
|
+
# that contradicts a field in the same payload is the ADR-0008 defect, so
|
|
1643
|
+
# the estimate now reads that field.
|
|
1644
|
+
#
|
|
1645
|
+
# The unit is the file, because the weights below are per file: a file is
|
|
1646
|
+
# mechanical only when EVERY main finding in it carries a recipe. One
|
|
1647
|
+
# unautomatable finding puts a human in the file, and the rest of that
|
|
1648
|
+
# file's work comes with them.
|
|
1649
|
+
_findings_by_file: "dict[str, list[MigrationFinding]]" = {}
|
|
1650
|
+
for f in main_findings:
|
|
1651
|
+
_findings_by_file.setdefault(f.source_file, []).append(f)
|
|
1652
|
+
_mechanical_files = {
|
|
1653
|
+
path for path, items in _findings_by_file.items()
|
|
1654
|
+
if items and all(bool(i.openrewrite_recipe) for i in items)
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
def _weighted(files: "set[str]") -> float:
|
|
1658
|
+
return (
|
|
1659
|
+
len(critical_files & files) * 0.5
|
|
1660
|
+
+ len(high_files & files) * 0.25
|
|
1661
|
+
+ len(medium_files & files) * 0.1
|
|
1662
|
+
+ len(low_files & files) * 0.05
|
|
1663
|
+
)
|
|
1664
|
+
|
|
1665
|
+
_all_effort_files = critical_files | high_files | medium_files | low_files
|
|
1666
|
+
_manual_effort = _weighted(_all_effort_files - _mechanical_files)
|
|
1667
|
+
# A recipe does not remove the work, it changes what the work is: run the
|
|
1668
|
+
# recipe once, then review the diff. Reviewing is charged at a fraction of
|
|
1669
|
+
# editing. The fraction is an assumption, not a measurement, so it is
|
|
1670
|
+
# published as a field rather than buried in this expression.
|
|
1671
|
+
_mechanical_raw = _weighted(_all_effort_files & _mechanical_files)
|
|
1672
|
+
_mechanical_effort = _mechanical_raw * _MECHANICAL_REVIEW_FACTOR
|
|
1673
|
+
_file_effort = _manual_effort + _mechanical_effort
|
|
1674
|
+
_auto_fixable = sum(1 for f in main_findings if f.openrewrite_recipe)
|
|
1675
|
+
_recipes = sorted({f.openrewrite_recipe for f in main_findings if f.openrewrite_recipe})
|
|
1634
1676
|
# BUG #1 (v1.70.0): when the Hibernate 5→6 axis APPLIES, fold its measured
|
|
1635
1677
|
# rewrite effort (risk_matrix → total_effort_range_days) into the headline
|
|
1636
1678
|
# estimate. Previously a Hibernate-5 project whose ${hibernateVersion} was
|
|
@@ -1646,6 +1688,33 @@ class MigrationReport:
|
|
|
1646
1688
|
self.estimated_effort_days = round(_file_effort + _hib_effort, 1)
|
|
1647
1689
|
self.effort_breakdown = {
|
|
1648
1690
|
"findings_effort_days": round(_file_effort, 1),
|
|
1691
|
+
# C1-11: the same total, split by what the work actually is.
|
|
1692
|
+
"mechanical_days": round(_mechanical_effort, 1),
|
|
1693
|
+
"manual_days": round(_manual_effort, 1),
|
|
1694
|
+
# Not estimated rather than estimated at zero: the re-test scope is a
|
|
1695
|
+
# different measurement (which endpoints the changed files reach), and
|
|
1696
|
+
# `migrate-check --blast-radius` is what produces it. I-3: a fact that
|
|
1697
|
+
# cannot be decided says so.
|
|
1698
|
+
"validation_days": None,
|
|
1699
|
+
"validation_days_note": (
|
|
1700
|
+
"Not estimated here — the re-test scope is the endpoints the changed "
|
|
1701
|
+
"files reach. Run `migrate-check --blast-radius` for that ordering."
|
|
1702
|
+
),
|
|
1703
|
+
"mechanical_basis": (
|
|
1704
|
+
f"{_auto_fixable} of {len(main_findings)} findings carry an OpenRewrite "
|
|
1705
|
+
f"recipe; {len(_mechanical_files & _all_effort_files)} of "
|
|
1706
|
+
f"{len(_all_effort_files)} affected files have a recipe for *every* "
|
|
1707
|
+
f"finding in them and are costed at {_MECHANICAL_REVIEW_FACTOR:g}× the "
|
|
1708
|
+
"hand-edit rate (run the recipe, review the diff). A file with one "
|
|
1709
|
+
"unautomatable finding is costed manually in full — a human is in that "
|
|
1710
|
+
"file either way."
|
|
1711
|
+
),
|
|
1712
|
+
"mechanical_review_factor": _MECHANICAL_REVIEW_FACTOR,
|
|
1713
|
+
"mechanical_review_factor_basis": "assumption, not a measurement",
|
|
1714
|
+
"auto_fixable_findings": _auto_fixable,
|
|
1715
|
+
"auto_fixable_files": len(_mechanical_files & _all_effort_files),
|
|
1716
|
+
"affected_files": len(_all_effort_files),
|
|
1717
|
+
"recipes": _recipes,
|
|
1649
1718
|
"hibernate_rewrite_effort_days": round(_hib_effort, 1),
|
|
1650
1719
|
"hibernate_rewrite_range": (
|
|
1651
1720
|
hib.total_effort_range_days if (_hibernate_applies and hib is not None) else None
|
|
@@ -1809,7 +1878,20 @@ class MigrationReport:
|
|
|
1809
1878
|
f"(critical: {main_crit}, high: {main_high})"
|
|
1810
1879
|
+ (f" [+{nb} in test/generated, non-blocking]" if nb else ""),
|
|
1811
1880
|
f"Affected files: {self.summary.get('affected_files', 0)}",
|
|
1812
|
-
f"Estimated effort: {self.estimated_effort_days}d"
|
|
1881
|
+
f"Estimated effort: {self.estimated_effort_days}d"
|
|
1882
|
+
+ (
|
|
1883
|
+
f" ({self.effort_breakdown.get('manual_days', 0)}d manual + "
|
|
1884
|
+
f"{self.effort_breakdown.get('mechanical_days', 0)}d mechanical"
|
|
1885
|
+
+ (f" + {self.effort_breakdown.get('hibernate_rewrite_effort_days')}d hibernate"
|
|
1886
|
+
if self.effort_breakdown.get("hibernate_rewrite_effort_days") else "")
|
|
1887
|
+
+ "; validation not estimated)"
|
|
1888
|
+
if self.effort_breakdown else ""
|
|
1889
|
+
),
|
|
1890
|
+
(
|
|
1891
|
+
f" {self.effort_breakdown.get('auto_fixable_findings', 0)} findings carry an "
|
|
1892
|
+
f"OpenRewrite recipe — see effort_breakdown.recipes"
|
|
1893
|
+
if self.effort_breakdown.get("auto_fixable_findings") else ""
|
|
1894
|
+
),
|
|
1813
1895
|
"",
|
|
1814
1896
|
]
|
|
1815
1897
|
|
|
@@ -19,6 +19,7 @@ No public command and no verdict layer yet (those are Fase V1). What it must pro
|
|
|
19
19
|
"""
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
+
import re
|
|
22
23
|
import shutil
|
|
23
24
|
import subprocess
|
|
24
25
|
import tempfile
|
|
@@ -48,6 +49,10 @@ class HeadVsWorking:
|
|
|
48
49
|
head_sha: str
|
|
49
50
|
head_cache_hit: bool
|
|
50
51
|
timings_ms: dict[str, float] = field(default_factory=dict)
|
|
52
|
+
#: Changed files that are not code but decide what the code compiles to and how
|
|
53
|
+
#: it is wired (C3-14). Kept apart from ``changed_files`` because the CIR diff
|
|
54
|
+
#: cannot reason about them — they make the verdict *unverified*, not *broken*.
|
|
55
|
+
changed_build_files: tuple[str, ...] = ()
|
|
51
56
|
|
|
52
57
|
|
|
53
58
|
# Git env vars that REDIRECT git at where it operates. When verify-edit runs INSIDE a
|
|
@@ -98,8 +103,8 @@ def _head_sha(root: Path) -> str:
|
|
|
98
103
|
return ""
|
|
99
104
|
|
|
100
105
|
|
|
101
|
-
def
|
|
102
|
-
"""
|
|
106
|
+
def _changed_paths(root: Path) -> tuple[str, ...]:
|
|
107
|
+
"""Every path that differs from HEAD: tracked diff ∪ untracked adds.
|
|
103
108
|
|
|
104
109
|
Deletions are kept (the path is gone but the contract change is real). Pure
|
|
105
110
|
git; never reads file contents.
|
|
@@ -114,7 +119,53 @@ def _changed_java_files(root: Path) -> tuple[str, ...]:
|
|
|
114
119
|
files.update(line.strip() for line in ru.stdout.splitlines() if line.strip())
|
|
115
120
|
except Exception:
|
|
116
121
|
pass
|
|
117
|
-
return tuple(sorted(
|
|
122
|
+
return tuple(sorted(files))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _changed_java_files(root: Path) -> tuple[str, ...]:
|
|
126
|
+
"""The `.java` subset of `_changed_paths` — what the CIR diff can reason about."""
|
|
127
|
+
return tuple(f for f in _changed_paths(root) if f.endswith(".java"))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
#: Files that change what the code *means* without being code: the build, the
|
|
131
|
+
#: container contract, the Spring wiring, the configuration a profile resolves.
|
|
132
|
+
#:
|
|
133
|
+
#: C3-14 (field eval #4): an edit to any of these was invisible to `verify-edit`,
|
|
134
|
+
#: which answered `changed_files: []` and `verdict: pass` — a confident green over
|
|
135
|
+
#: a change that can move every source file in the repository at once. Matched on
|
|
136
|
+
#: the file name, never on content, so the check costs nothing.
|
|
137
|
+
_BUILD_FILE_NAMES: frozenset[str] = frozenset({
|
|
138
|
+
"pom.xml", "build.gradle", "build.gradle.kts",
|
|
139
|
+
"settings.gradle", "settings.gradle.kts", "gradle.properties",
|
|
140
|
+
"web.xml", "context.xml", "jboss-web.xml", "beans.xml", "persistence.xml",
|
|
141
|
+
})
|
|
142
|
+
#: Descriptor families matched by prefix + `.xml`: the container contract and the
|
|
143
|
+
#: classic Spring XML wiring.
|
|
144
|
+
_XML_DESCRIPTOR_PREFIXES: tuple[str, ...] = ("weblogic", "jboss-", "applicationcontext")
|
|
145
|
+
#: Config file families that a profile resolves against. Deliberately narrow —
|
|
146
|
+
#: `messages.properties` is text for humans, not wiring.
|
|
147
|
+
_CONFIG_PREFIXES: tuple[str, ...] = ("application", "bootstrap")
|
|
148
|
+
_CONFIG_SUFFIXES: tuple[str, ...] = (".properties", ".yml", ".yaml")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_build_config_path(rel_path: str) -> bool:
|
|
152
|
+
"""Admission rule, kept narrow on purpose: every admitted file degrades a verdict
|
|
153
|
+
to unverified, and a loose rule turns the gate into noise people learn to ignore."""
|
|
154
|
+
name = rel_path.rsplit("/", 1)[-1].lower()
|
|
155
|
+
if name in _BUILD_FILE_NAMES:
|
|
156
|
+
return True
|
|
157
|
+
if name.endswith(".xml") and (
|
|
158
|
+
name.endswith("-context.xml") or name.startswith(_XML_DESCRIPTOR_PREFIXES)
|
|
159
|
+
):
|
|
160
|
+
return True
|
|
161
|
+
if name.endswith(_CONFIG_SUFFIXES) and name.startswith(_CONFIG_PREFIXES):
|
|
162
|
+
return True
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _changed_build_files(root: Path) -> tuple[str, ...]:
|
|
167
|
+
"""Changed non-Java files that can alter compilation or runtime wiring."""
|
|
168
|
+
return tuple(f for f in _changed_paths(root) if _is_build_config_path(f))
|
|
118
169
|
|
|
119
170
|
|
|
120
171
|
# ── CIR construction ───────────────────────────────────────────────────────────
|
|
@@ -213,7 +264,9 @@ def head_vs_working(path: Path) -> HeadVsWorking:
|
|
|
213
264
|
timings: dict[str, float] = {}
|
|
214
265
|
|
|
215
266
|
t0 = time.perf_counter()
|
|
216
|
-
|
|
267
|
+
all_changed = _changed_paths(root)
|
|
268
|
+
changed = tuple(f for f in all_changed if f.endswith(".java"))
|
|
269
|
+
changed_build = tuple(f for f in all_changed if _is_build_config_path(f))
|
|
217
270
|
timings["changed_files_ms"] = (time.perf_counter() - t0) * 1000.0
|
|
218
271
|
|
|
219
272
|
t1 = time.perf_counter()
|
|
@@ -231,6 +284,7 @@ def head_vs_working(path: Path) -> HeadVsWorking:
|
|
|
231
284
|
head_sha=head_sha,
|
|
232
285
|
head_cache_hit=hit,
|
|
233
286
|
timings_ms=timings,
|
|
287
|
+
changed_build_files=changed_build,
|
|
234
288
|
)
|
|
235
289
|
|
|
236
290
|
|
|
@@ -252,7 +306,7 @@ VERDICT_SCHEMA = "verify-edit-v1"
|
|
|
252
306
|
# no-op (clean) when no rules file exists, so adding it never blocks a repo that has none.
|
|
253
307
|
_BLOCKING_AXES = (
|
|
254
308
|
"contract_broken", "tx_boundary_changed", "security_delta", "new_orphan_bean",
|
|
255
|
-
"custom_rule_violated",
|
|
309
|
+
"custom_rule_violated", "build_config_changed",
|
|
256
310
|
)
|
|
257
311
|
|
|
258
312
|
|
|
@@ -264,6 +318,7 @@ class AxisResult:
|
|
|
264
318
|
changed: Optional[bool]
|
|
265
319
|
detail: str = ""
|
|
266
320
|
items: tuple[dict, ...] = ()
|
|
321
|
+
data: dict = field(default_factory=dict)
|
|
267
322
|
|
|
268
323
|
def to_dict(self) -> dict:
|
|
269
324
|
d: dict = {"changed": self.changed}
|
|
@@ -271,6 +326,7 @@ class AxisResult:
|
|
|
271
326
|
d["detail"] = self.detail
|
|
272
327
|
if self.items:
|
|
273
328
|
d["items"] = list(self.items)
|
|
329
|
+
d.update(self.data)
|
|
274
330
|
return d
|
|
275
331
|
|
|
276
332
|
|
|
@@ -279,6 +335,7 @@ class VerifyVerdict:
|
|
|
279
335
|
verdict: str # "pass" | "break" | "unverified"
|
|
280
336
|
axes: dict[str, AxisResult]
|
|
281
337
|
changed_files: tuple[str, ...]
|
|
338
|
+
changed_build_files: tuple[str, ...]
|
|
282
339
|
blast_radius: dict
|
|
283
340
|
reasons: tuple[str, ...]
|
|
284
341
|
exit_code: int
|
|
@@ -290,6 +347,7 @@ class VerifyVerdict:
|
|
|
290
347
|
"verdict": self.verdict,
|
|
291
348
|
"head_sha": self.head_sha,
|
|
292
349
|
"changed_files": list(self.changed_files),
|
|
350
|
+
"changed_build_files": list(self.changed_build_files),
|
|
293
351
|
"axes": {name: ax.to_dict() for name, ax in self.axes.items()},
|
|
294
352
|
"blast_radius": self.blast_radius,
|
|
295
353
|
"reasons": list(self.reasons),
|
|
@@ -451,9 +509,132 @@ def _custom_rules_axis(cir_head: Any, cir_working: Any, root: Path) -> AxisResul
|
|
|
451
509
|
return AxisResult(False)
|
|
452
510
|
|
|
453
511
|
|
|
512
|
+
# ── build configuration (C3-14) ────────────────────────────────────────────────
|
|
513
|
+
_COMPILER_PROPS: tuple[str, ...] = (
|
|
514
|
+
"java.version", "maven.compiler.release", "maven.compiler.source",
|
|
515
|
+
"maven.compiler.target",
|
|
516
|
+
)
|
|
517
|
+
_MAVEN_DEP_RE: "re.Pattern[str]" = re.compile(
|
|
518
|
+
r"<dependency>(.*?)</dependency>", re.DOTALL | re.IGNORECASE
|
|
519
|
+
)
|
|
520
|
+
_MAVEN_FIELD_RE: "re.Pattern[str]" = re.compile(
|
|
521
|
+
r"<(groupId|artifactId|version)>\s*([^<]+?)\s*</\1>", re.IGNORECASE
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _file_at_head(root: Path, rel_path: str) -> Optional[str]:
|
|
526
|
+
"""The HEAD content of a path, or None when it did not exist there."""
|
|
527
|
+
try:
|
|
528
|
+
r = _run_git(root, "show", f"HEAD:{rel_path}")
|
|
529
|
+
return r.stdout if r.returncode == 0 else None
|
|
530
|
+
except Exception:
|
|
531
|
+
return None
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _maven_property(text: str, name: str) -> Optional[str]:
|
|
535
|
+
m = re.search(rf"<{re.escape(name)}>\s*([^<]+?)\s*</{re.escape(name)}>", text, re.IGNORECASE)
|
|
536
|
+
return m.group(1) if m else None
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _maven_dependencies(text: str) -> "dict[str, str]":
|
|
540
|
+
"""`groupId:artifactId` → version (or "" when managed elsewhere). A diff input,
|
|
541
|
+
not an inventory: this answers *what changed in this file*, which is why it does
|
|
542
|
+
not go through the declared-coordinates authority (that one answers a different
|
|
543
|
+
question — which client libraries the build declares, allowlisted by kind)."""
|
|
544
|
+
out: dict[str, str] = {}
|
|
545
|
+
for block in _MAVEN_DEP_RE.findall(text or ""):
|
|
546
|
+
fields = {k.lower(): v for k, v in _MAVEN_FIELD_RE.findall(block)}
|
|
547
|
+
gid, aid = fields.get("groupid"), fields.get("artifactid")
|
|
548
|
+
if gid and aid:
|
|
549
|
+
out[f"{gid}:{aid}"] = fields.get("version", "")
|
|
550
|
+
return out
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _build_config_axis(root: Path, changed_build_files: tuple[str, ...]) -> AxisResult:
|
|
554
|
+
"""C3-14: a changed build/config file is a change `verify-edit` cannot decide.
|
|
555
|
+
|
|
556
|
+
`changed: None` on purpose. The CIR diff proves nothing about a `pom.xml` edit —
|
|
557
|
+
it is neither a proven regression (`True`) nor computed-clean (`False`) — so the
|
|
558
|
+
axis degrades to *unverified* and the verdict follows the machinery that already
|
|
559
|
+
exists for "could not compute". Answering `pass` was the defect: a compiler-release
|
|
560
|
+
bump moves every source file in the repository at once, and the gate said nothing
|
|
561
|
+
changed.
|
|
562
|
+
"""
|
|
563
|
+
if not changed_build_files:
|
|
564
|
+
return AxisResult(False, "no build or configuration file changed")
|
|
565
|
+
|
|
566
|
+
compiler: dict = {}
|
|
567
|
+
dependency_delta: list[dict] = []
|
|
568
|
+
for rel in changed_build_files:
|
|
569
|
+
if not rel.endswith("pom.xml"):
|
|
570
|
+
continue
|
|
571
|
+
before = _file_at_head(root, rel)
|
|
572
|
+
try:
|
|
573
|
+
after = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
574
|
+
except OSError:
|
|
575
|
+
after = None
|
|
576
|
+
if before is None or after is None:
|
|
577
|
+
continue
|
|
578
|
+
for prop in _COMPILER_PROPS:
|
|
579
|
+
was, now = _maven_property(before, prop), _maven_property(after, prop)
|
|
580
|
+
if was != now:
|
|
581
|
+
compiler[prop] = {"from": was, "to": now, "file": rel}
|
|
582
|
+
deps_before, deps_after = _maven_dependencies(before), _maven_dependencies(after)
|
|
583
|
+
for key in sorted(set(deps_before) | set(deps_after)):
|
|
584
|
+
if deps_before.get(key) == deps_after.get(key):
|
|
585
|
+
continue
|
|
586
|
+
dependency_delta.append({
|
|
587
|
+
"coordinate": key,
|
|
588
|
+
"from": deps_before.get(key),
|
|
589
|
+
"to": deps_after.get(key),
|
|
590
|
+
"file": rel,
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
detail = f"{len(changed_build_files)} build/configuration file(s) changed; "
|
|
594
|
+
if compiler:
|
|
595
|
+
first = next(iter(compiler.values()))
|
|
596
|
+
detail += f"compiler release {first['from']} → {first['to']}; "
|
|
597
|
+
if dependency_delta:
|
|
598
|
+
detail += f"{len(dependency_delta)} dependency change(s); "
|
|
599
|
+
detail += "effect on runtime behaviour is not decidable from the source diff"
|
|
600
|
+
|
|
601
|
+
return AxisResult(
|
|
602
|
+
None,
|
|
603
|
+
detail,
|
|
604
|
+
items=tuple({"file": f} for f in changed_build_files[:20]),
|
|
605
|
+
data={
|
|
606
|
+
"files": list(changed_build_files),
|
|
607
|
+
"effective_compiler_release": compiler or None,
|
|
608
|
+
"dependency_delta": dependency_delta or None,
|
|
609
|
+
"blast_note": (
|
|
610
|
+
"A build or configuration change applies to the whole compilation "
|
|
611
|
+
"unit, not to a file: its reach is every source file in scope, and "
|
|
612
|
+
"`verify-edit` does not model it. Verdict degrades to unverified "
|
|
613
|
+
"(exit 2) rather than passing."
|
|
614
|
+
),
|
|
615
|
+
},
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
|
|
454
619
|
# ── blast radius (reach over the changed files; informational, not a diff) ──────
|
|
455
|
-
def _blast_radius(
|
|
620
|
+
def _blast_radius(
|
|
621
|
+
cir_working: Any,
|
|
622
|
+
changed_files: tuple[str, ...],
|
|
623
|
+
changed_build_files: tuple[str, ...] = (),
|
|
624
|
+
) -> dict:
|
|
456
625
|
if not changed_files:
|
|
626
|
+
# C3-14: "no changed files" with high confidence was a lie when the build
|
|
627
|
+
# changed — the reach of that edit is the whole tree, and it is unmeasured.
|
|
628
|
+
if changed_build_files:
|
|
629
|
+
return {
|
|
630
|
+
"affected_entry_points": None,
|
|
631
|
+
"confidence": "unknown",
|
|
632
|
+
"note": (
|
|
633
|
+
f"no changed .java files, but {len(changed_build_files)} build/"
|
|
634
|
+
"configuration file(s) changed — reach is the whole compilation "
|
|
635
|
+
"unit and is not modelled here"
|
|
636
|
+
),
|
|
637
|
+
}
|
|
457
638
|
return {"affected_entry_points": 0, "confidence": "high", "note": "no changed files"}
|
|
458
639
|
try:
|
|
459
640
|
from sourcecode.context_graph import ContextGraph
|
|
@@ -487,8 +668,9 @@ def verdict(hvw: HeadVsWorking, root: Path) -> VerifyVerdict:
|
|
|
487
668
|
"security_delta": _security_axis(hvw.cir_head, hvw.cir_working, root),
|
|
488
669
|
"new_orphan_bean": _orphan_bean_axis(hvw.cir_head, hvw.cir_working),
|
|
489
670
|
"custom_rule_violated": _custom_rules_axis(hvw.cir_head, hvw.cir_working, root),
|
|
671
|
+
"build_config_changed": _build_config_axis(root, hvw.changed_build_files),
|
|
490
672
|
}
|
|
491
|
-
blast = _blast_radius(hvw.cir_working, hvw.changed_files)
|
|
673
|
+
blast = _blast_radius(hvw.cir_working, hvw.changed_files, hvw.changed_build_files)
|
|
492
674
|
|
|
493
675
|
reasons: list[str] = []
|
|
494
676
|
broke = False
|
|
@@ -513,6 +695,7 @@ def verdict(hvw: HeadVsWorking, root: Path) -> VerifyVerdict:
|
|
|
513
695
|
verdict=verdict_str,
|
|
514
696
|
axes=axes,
|
|
515
697
|
changed_files=hvw.changed_files,
|
|
698
|
+
changed_build_files=hvw.changed_build_files,
|
|
516
699
|
blast_radius=blast,
|
|
517
700
|
reasons=tuple(reasons),
|
|
518
701
|
exit_code=code,
|
|
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
|