sourcecode 2.6.10__tar.gz → 2.7.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-2.6.10 → sourcecode-2.7.0}/CHANGELOG.md +92 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/PKG-INFO +1 -1
- {sourcecode-2.6.10 → sourcecode-2.7.0}/pyproject.toml +1 -1
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/archetype.py +11 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/caller_metrics.py +19 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/cli.py +190 -52
- sourcecode-2.7.0/src/sourcecode/deployment_prefix.py +229 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/explain.py +32 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/hibernate_strat.py +11 -4
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/migrate_check.py +13 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/pr_impact.py +135 -16
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/repository_ir.py +285 -13
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/steps_endpoint.py +32 -8
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/steps_graph.py +9 -1
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/steps_intf.py +32 -6
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/steps_txsec.py +114 -6
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/security_posture.py +41 -8
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/serializer.py +52 -6
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/spring_findings.py +44 -0
- sourcecode-2.7.0/src/sourcecode/spring_profiles.py +208 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/validation_inference.py +58 -6
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/validation_surface.py +91 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/.gitignore +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/.ruff.toml +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/CONTRIBUTING.md +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/LICENSE +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/README.md +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/SECURITY.md +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/raw +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/scripts/perf_harness.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/compare.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/filter_surface.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/license.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/parse_cache.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/perf.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/readiness_timeline.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/token_estimate.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/verify_edit.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/verify_rules.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/supabase/functions/README.md +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.6.10 → sourcecode-2.7.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,97 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.7.0] — 2026-07-26
|
|
4
|
+
|
|
5
|
+
**The analyzer reads facts it had been walking past, and stops spending the context budget
|
|
6
|
+
it exists to save.** Driven by an external audit of 2.6.10 and validated by a field battery
|
|
7
|
+
across eleven real repositories plus an A/B against the previous release.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Deployment prefix.** Neither `server.servlet.context-path` nor `spring.mvc.servlet.path`
|
|
12
|
+
was read anywhere, so an app deployed under `/app-server/api` was inventoried as `/v1/x` —
|
|
13
|
+
every URL wrong by the same constant, which is what makes the error invisible. Endpoints
|
|
14
|
+
now carry `effective_path` alongside the mapping-relative `path`, plus a
|
|
15
|
+
`deployment_prefix` block with per-source provenance. A value carrying a placeholder, or
|
|
16
|
+
declared only under a profile, is recorded but never imposed on every route.
|
|
17
|
+
- **Two unread profile signals.** Profile names came from one signal — the
|
|
18
|
+
`application-{profile}.yml` filename convention. Multi-document activation
|
|
19
|
+
(`spring.config.activate.on-profile`) and `@Profile` on beans are read now, and
|
|
20
|
+
`profile_conditional_beans` says WHICH beans each profile switches — routinely the
|
|
21
|
+
security filter chain.
|
|
22
|
+
- **A repo's own authorization gate on the endpoint surface.** Custom-gate detection fed
|
|
23
|
+
only the `spring-audit` posture block; every other surface reported `policy: none_detected`
|
|
24
|
+
for each endpoint of a repo whose authorization runs entirely through its own annotation.
|
|
25
|
+
Such endpoints now carry `policy: custom_gate_inferred` with the annotation as evidence,
|
|
26
|
+
and the model becomes `custom_annotation_based`. Coverage and specificity alone were
|
|
27
|
+
measured to be insufficient for a per-endpoint claim — they select caching and
|
|
28
|
+
documentation annotations — so the claim additionally requires an in-repo `@interface`
|
|
29
|
+
declaration. Structural, no name matching.
|
|
30
|
+
- **`explain --limit`** (default 50), capping the list sections with the truncation and the
|
|
31
|
+
exact total stated in `warnings`. `--limit 0` restores the full lists.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Cross-file `String` constants were not folded.** `@RequestMapping(Routes.BASE)` resolved
|
|
36
|
+
to nothing and "nothing" was emitted as the empty path, collapsing every handler of a
|
|
37
|
+
controller onto `GET /`. A repo-wide constant table is built from the pass the assembler
|
|
38
|
+
already runs; a name declared twice with different values is dropped, not guessed; what
|
|
39
|
+
still cannot be resolved is published as `path_resolution: "unresolved"` with the
|
|
40
|
+
expression, instead of a fabricated `/`. On keycloak this recovers three real endpoints
|
|
41
|
+
(`AuthZen.EVALUATION_PATH`, `AdapterConstants.K_LOGOUT`) with no route newly unresolved.
|
|
42
|
+
- **Types whose name sits on the line after the keyword** (`public class\n Name {`) were
|
|
43
|
+
not extracted.
|
|
44
|
+
- **Hibernate rewrite snippets quoted literal-blanked source**, emitting
|
|
45
|
+
`em.createNamedQuery("")` — dropping the query name, the one datum that makes the target
|
|
46
|
+
actionable.
|
|
47
|
+
- **Confidence described the lookup, not the answer.** `retrieve endpoint-security` graded
|
|
48
|
+
"high" whenever the reference resolved, so an endpoint with no policy and no finding came
|
|
49
|
+
back maximally confident about nothing. It is high only when evidence attaches, and an
|
|
50
|
+
explicit `evidence_absent` observation says which case it is.
|
|
51
|
+
- **Archetype counted signal diversity by cardinality**, which one dominant signal satisfies
|
|
52
|
+
trivially. "High" now also requires the top signal to supply at most 80% of the label's
|
|
53
|
+
score.
|
|
54
|
+
- **Cycle counts read as one command contradicting another** (`modernize` 15 tangles vs
|
|
55
|
+
`subsystem-coupling` 0 import cycles). Both are right and measure different graphs; a
|
|
56
|
+
shared reconciliation note now travels with both numbers.
|
|
57
|
+
- **`auth status` reported `pro: false`** while every Pro command ran under the
|
|
58
|
+
early-adoption unlock. It now reports the gating in force (`pro_gating`, `pro_effective`,
|
|
59
|
+
`pro_reason`).
|
|
60
|
+
- **`transaction-entrypoints` counted only `@Transactional` declared ON a controller**, so a
|
|
61
|
+
repo with 129 boundaries reported 1 entrypoint. Boundaries reachable from a controller
|
|
62
|
+
within 3 call hops are reported alongside, each labelled with its `entrypoint_basis`.
|
|
63
|
+
Rows that a signature-free FQN made indistinguishable (overloads) collapse with the count
|
|
64
|
+
kept as `occurrences`; rows that differ in any attribute stay separate.
|
|
65
|
+
- **`integration-inventory` said nothing four times**: identity keyed on
|
|
66
|
+
(kind, target-or-client) produced byte-identical rows. Rows collapse with `occurrences`,
|
|
67
|
+
and a null target carries the reason it is null. On Broadleaf: 9 rows → 4.
|
|
68
|
+
- **`cache warm <subdir>` warmed the whole monorepo**, populating a cache for a different
|
|
69
|
+
target than `ask <subdir>` reads. It warms exactly the path given and says so when that
|
|
70
|
+
differs from the git root.
|
|
71
|
+
- **`migrate-check --compact` shipped an empty `headline_blocker`**, forcing the reader back
|
|
72
|
+
into the full report. It names the weakest applicable axis, or says "none" /
|
|
73
|
+
"none_applicable". The guard against asserting a framework blocker without version
|
|
74
|
+
evidence is preserved.
|
|
75
|
+
- **`impact-chain` enumerations were unbounded.** Lists are capped; every total stays exact
|
|
76
|
+
in `metadata` and `truncated_lists`.
|
|
77
|
+
- **The profile signal was gated on the env-var scan**: a repo that declares profiles but
|
|
78
|
+
reads no environment variable published nothing about them — the exact shape the profile
|
|
79
|
+
signals were added for.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- **`export --c4` omits the code level (C4 level 4) by default.** It enumerates every symbol
|
|
84
|
+
per directory and dominated the export: keycloak 18.6 MB → 2.5 MB, Broadleaf 9.7 MB →
|
|
85
|
+
1.1 MB. The omission is explicit in the payload (`included: false` with the reason and the
|
|
86
|
+
directory count) and `--c4-code` restores it.
|
|
87
|
+
|
|
88
|
+
### Notes
|
|
89
|
+
|
|
90
|
+
- Validated before release: full suite (3,869 passed), eleven repositories × eight commands
|
|
91
|
+
with no crash and valid JSON throughout, output identical across `PYTHONHASHSEED` values
|
|
92
|
+
on cold caches, and an A/B against 2.6.10 in which every difference maps to a change listed
|
|
93
|
+
above.
|
|
94
|
+
|
|
3
95
|
## [2.6.10] — 2026-07-25
|
|
4
96
|
|
|
5
97
|
**Identical runs now produce identical output.** Found by a field battery across twelve
|
|
@@ -604,12 +604,23 @@ class ArchetypeClassifier:
|
|
|
604
604
|
# Gini alone labeling a REST API an "engine") — is a single point of view and
|
|
605
605
|
# must not read as high confidence (field-eval Fase 0).
|
|
606
606
|
distinct_signals = len({e.signal for e in top.evidence if e.contribution > 0})
|
|
607
|
+
# Diversity by COUNT is satisfiable while one signal carries the
|
|
608
|
+
# verdict: an audited repo was labelled "engine" at high confidence on
|
|
609
|
+
# 1.500 of a 1.504 score — a fan-in Gini alone, with the second signal
|
|
610
|
+
# contributing 0.3%. Counting it as two views is a fiction, so a label
|
|
611
|
+
# whose top signal supplies almost all of its score cannot be "high"
|
|
612
|
+
# however many other signals nominally touch it.
|
|
613
|
+
_contributions = sorted(
|
|
614
|
+
(e.contribution for e in top.evidence if e.contribution > 0), reverse=True
|
|
615
|
+
)
|
|
616
|
+
top_signal_share = (_contributions[0] / top.score) if _contributions else 1.0
|
|
607
617
|
if (
|
|
608
618
|
confident_enough
|
|
609
619
|
and rel_margin >= 0.4
|
|
610
620
|
and mass_backed
|
|
611
621
|
and graph_backed
|
|
612
622
|
and distinct_signals >= 2
|
|
623
|
+
and top_signal_share <= 0.8
|
|
613
624
|
):
|
|
614
625
|
confidence = "high"
|
|
615
626
|
elif confident_enough and rel_margin >= 0.25 and mass_backed:
|
|
@@ -40,3 +40,22 @@ CALLER_METRIC_RECONCILIATION: str = (
|
|
|
40
40
|
"references or DI-interface resolution). The top-level impact.direct_callers array "
|
|
41
41
|
"may be truncated for output size — use stats.direct_caller_count for the true total."
|
|
42
42
|
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Cycle / tangle metrics come from TWO different graphs. Both are correct; they
|
|
46
|
+
# answer different questions, and without saying so the pair reads as one command
|
|
47
|
+
# contradicting another (external audit 2026-07-25: modernize reported 15 cyclic
|
|
48
|
+
# tangles while retrieve subsystem-coupling reported import_cycles: 0).
|
|
49
|
+
CYCLE_METRIC_RECONCILIATION: str = (
|
|
50
|
+
"Cycle metrics differ across commands BY DESIGN. modernize.cross_module_tangles "
|
|
51
|
+
"is computed over the SYMBOL dependency graph, aggregated to subsystem packages: "
|
|
52
|
+
"coupling_type=cyclic means two subsystems reference each other (a 2-cycle "
|
|
53
|
+
"between package groups). The module-graph metrics (import_cycles, cyclic_density, "
|
|
54
|
+
"scc_mass — used by archetype and the subsystem-coupling retrieval intent) are "
|
|
55
|
+
"computed over the MODULE/DIRECTORY graph, are bounded for cost (node/edge caps), "
|
|
56
|
+
"and count strongly-connected components among modules. A repo can therefore "
|
|
57
|
+
"show mutual package-level coupling (tangles > 0) with no module-level SCC "
|
|
58
|
+
"(import_cycles = 0): the coupling exists between package groups inside the same "
|
|
59
|
+
"module. Neither number invalidates the other — read tangles for decomposition "
|
|
60
|
+
"risk and import_cycles for module-graph topology."
|
|
61
|
+
)
|
|
@@ -17,7 +17,10 @@ from sourcecode.error_schema import INVALID_INPUT_CODE, build_error_envelope
|
|
|
17
17
|
from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
|
|
18
18
|
from sourcecode.progress import Progress
|
|
19
19
|
from sourcecode import perf
|
|
20
|
-
from sourcecode.caller_metrics import
|
|
20
|
+
from sourcecode.caller_metrics import (
|
|
21
|
+
CALLER_METRIC_RECONCILIATION,
|
|
22
|
+
CYCLE_METRIC_RECONCILIATION,
|
|
23
|
+
)
|
|
21
24
|
from sourcecode.repository_ir import extract_java_endpoints as _extract_java_endpoints
|
|
22
25
|
|
|
23
26
|
|
|
@@ -4472,6 +4475,7 @@ def _build_c4_export(
|
|
|
4472
4475
|
endpoints: "list[dict]",
|
|
4473
4476
|
integrations: "dict",
|
|
4474
4477
|
endpoint_meta: "Optional[dict]" = None,
|
|
4478
|
+
include_code_level: bool = False,
|
|
4475
4479
|
) -> "dict":
|
|
4476
4480
|
"""Assemble a unified, tool-agnostic C4 architecture export + incremental manifest.
|
|
4477
4481
|
|
|
@@ -4514,7 +4518,18 @@ def _build_c4_export(
|
|
|
4514
4518
|
},
|
|
4515
4519
|
"containers": containers,
|
|
4516
4520
|
"components": module_graph,
|
|
4517
|
-
|
|
4521
|
+
# C4 level 4 (code) is a per-directory enumeration of every symbol —
|
|
4522
|
+
# 18 MB on a large repo, and by C4's own guidance a level you generate
|
|
4523
|
+
# for a specific area rather than wholesale. Opt in with --c4-code.
|
|
4524
|
+
"code": by_directory if include_code_level else {
|
|
4525
|
+
"included": False,
|
|
4526
|
+
"reason": (
|
|
4527
|
+
"C4 level 4 (code) omitted by default: it enumerates every "
|
|
4528
|
+
"symbol per directory and dominates the export size. Pass "
|
|
4529
|
+
"--c4-code to include it."
|
|
4530
|
+
),
|
|
4531
|
+
"directory_count": len(by_directory),
|
|
4532
|
+
},
|
|
4518
4533
|
},
|
|
4519
4534
|
"api_surface": api_surface,
|
|
4520
4535
|
"manifest": {
|
|
@@ -4561,9 +4576,14 @@ def export_cmd(
|
|
|
4561
4576
|
),
|
|
4562
4577
|
c4: bool = typer.Option(
|
|
4563
4578
|
False, "--c4",
|
|
4564
|
-
help="Unified C4 architecture export (context/containers/components
|
|
4579
|
+
help="Unified C4 architecture export (context/containers/components) "
|
|
4565
4580
|
"+ per-directory incremental manifest. Vendor-neutral.",
|
|
4566
4581
|
),
|
|
4582
|
+
c4_code: bool = typer.Option(
|
|
4583
|
+
False, "--c4-code",
|
|
4584
|
+
help="Include C4 level 4 (code): every symbol per directory. Omitted by "
|
|
4585
|
+
"default because it dominates the export size (18 MB on a large repo).",
|
|
4586
|
+
),
|
|
4567
4587
|
) -> None:
|
|
4568
4588
|
"""Export structured, tool-agnostic codebase views for downstream tooling.
|
|
4569
4589
|
|
|
@@ -4641,6 +4661,7 @@ def export_cmd(
|
|
|
4641
4661
|
endpoints,
|
|
4642
4662
|
_integrations,
|
|
4643
4663
|
endpoint_meta=_ep_data,
|
|
4664
|
+
include_code_level=c4_code,
|
|
4644
4665
|
)
|
|
4645
4666
|
_prog.finish()
|
|
4646
4667
|
output = _serialize_dict(data, format)
|
|
@@ -4767,7 +4788,7 @@ def validation_cmd(
|
|
|
4767
4788
|
try:
|
|
4768
4789
|
from sourcecode.validation_inference import infer_validation_pattern
|
|
4769
4790
|
|
|
4770
|
-
_vres = infer_validation_pattern(_graph.cir)
|
|
4791
|
+
_vres = infer_validation_pattern(_graph.cir, target)
|
|
4771
4792
|
data["validation_pattern"] = _vres.to_dict()
|
|
4772
4793
|
# Proposal 1 (Explainability): a compact confidence block projecting the
|
|
4773
4794
|
# verdict's own confidence tier + rollup + declared precision limits, plus
|
|
@@ -5702,24 +5723,7 @@ def spring_audit_cmd(
|
|
|
5702
5723
|
if scope in ("all", "security"):
|
|
5703
5724
|
results.append(run_security_audit(cir, root=target, min_severity=min_severity, model=_model))
|
|
5704
5725
|
|
|
5705
|
-
|
|
5706
|
-
combined = results[0]
|
|
5707
|
-
else:
|
|
5708
|
-
all_findings: list[SpringFinding] = []
|
|
5709
|
-
all_limitations: list[str] = []
|
|
5710
|
-
merged_meta: dict = {}
|
|
5711
|
-
for r in results:
|
|
5712
|
-
all_findings.extend(r.findings)
|
|
5713
|
-
all_limitations.extend(r.limitations)
|
|
5714
|
-
merged_meta.update(r.metadata)
|
|
5715
|
-
combined = SpringAuditResult(
|
|
5716
|
-
repo_id=results[0].repo_id,
|
|
5717
|
-
spring_detected=any(r.spring_detected for r in results),
|
|
5718
|
-
scope="all",
|
|
5719
|
-
findings=all_findings,
|
|
5720
|
-
limitations=all_limitations,
|
|
5721
|
-
metadata=merged_meta,
|
|
5722
|
-
).finalize()
|
|
5726
|
+
combined = SpringAuditResult.merge(results, scope=scope)
|
|
5723
5727
|
|
|
5724
5728
|
if _file_limitations:
|
|
5725
5729
|
combined.limitations.extend(_file_limitations)
|
|
@@ -6034,6 +6038,40 @@ def migrate_check_cmd(
|
|
|
6034
6038
|
# ── Spring Impact Chain ───────────────────────────────────────────────────────
|
|
6035
6039
|
|
|
6036
6040
|
|
|
6041
|
+
_IMPACT_CAPPED_LISTS = (
|
|
6042
|
+
"direct_callers", "indirect_callers", "endpoints_affected",
|
|
6043
|
+
"security_surfaces", "transactional_boundaries", "impact_findings",
|
|
6044
|
+
)
|
|
6045
|
+
|
|
6046
|
+
|
|
6047
|
+
def _cap_impact_lists(data: dict, limit: int) -> dict:
|
|
6048
|
+
"""Cap impact-chain leaf arrays, keeping every total exact.
|
|
6049
|
+
|
|
6050
|
+
One hub class produced 324 KB of JSON (511 callers, 262 endpoints, 262
|
|
6051
|
+
security surfaces) from a tool sold on context economy. The COUNTS are the
|
|
6052
|
+
decision signal and stay exact — in `metadata` (callers_total,
|
|
6053
|
+
endpoints_total) and in `truncated_lists` per capped list — while the
|
|
6054
|
+
enumerations, which are what blows the budget, are capped.
|
|
6055
|
+
"""
|
|
6056
|
+
if limit <= 0:
|
|
6057
|
+
return data
|
|
6058
|
+
truncated: dict[str, dict] = {}
|
|
6059
|
+
for key in _IMPACT_CAPPED_LISTS:
|
|
6060
|
+
values = data.get(key)
|
|
6061
|
+
if isinstance(values, list) and len(values) > limit:
|
|
6062
|
+
truncated[key] = {"total": len(values), "shown": limit,
|
|
6063
|
+
"omitted": len(values) - limit}
|
|
6064
|
+
data[key] = values[:limit]
|
|
6065
|
+
if truncated:
|
|
6066
|
+
data["truncated_lists"] = truncated
|
|
6067
|
+
data.setdefault("analysis_warnings", []).append(
|
|
6068
|
+
f"Output capped to {limit} items per list; every total stays exact "
|
|
6069
|
+
"(truncated_lists per list, metadata.callers_total / "
|
|
6070
|
+
"metadata.endpoints_total). Use --limit 0 for the full enumeration."
|
|
6071
|
+
)
|
|
6072
|
+
return data
|
|
6073
|
+
|
|
6074
|
+
|
|
6037
6075
|
@app.command("impact-chain")
|
|
6038
6076
|
def impact_chain_cmd(
|
|
6039
6077
|
symbol: str = typer.Argument(
|
|
@@ -6076,6 +6114,12 @@ def impact_chain_cmd(
|
|
|
6076
6114
|
False, "--no-cache",
|
|
6077
6115
|
help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
|
|
6078
6116
|
),
|
|
6117
|
+
limit: int = typer.Option(
|
|
6118
|
+
100, "--limit",
|
|
6119
|
+
help="Maximum items per list section (callers, endpoints, security "
|
|
6120
|
+
"surfaces). Totals stay exact in metadata/truncated_lists. "
|
|
6121
|
+
"0 disables the cap.",
|
|
6122
|
+
),
|
|
6079
6123
|
) -> None:
|
|
6080
6124
|
"""Spring impact-chain: systemic blast radius of a symbol with TX/SEC enrichment.
|
|
6081
6125
|
|
|
@@ -6198,6 +6242,7 @@ def impact_chain_cmd(
|
|
|
6198
6242
|
result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
|
|
6199
6243
|
|
|
6200
6244
|
data = result.to_dict()
|
|
6245
|
+
data = _cap_impact_lists(data, limit)
|
|
6201
6246
|
_prog.finish()
|
|
6202
6247
|
output = _serialize_dict(data, format)
|
|
6203
6248
|
_emit_command_output(
|
|
@@ -6222,10 +6267,14 @@ def pr_impact_cmd(
|
|
|
6222
6267
|
Path("."),
|
|
6223
6268
|
help="Repository root (default: current directory)",
|
|
6224
6269
|
),
|
|
6225
|
-
files:
|
|
6270
|
+
files: str = typer.Option(
|
|
6226
6271
|
...,
|
|
6227
6272
|
"--files",
|
|
6228
|
-
help=
|
|
6273
|
+
help=(
|
|
6274
|
+
"Changed files to analyze: a text file with one path per line "
|
|
6275
|
+
"(e.g. from `git diff --name-only`), a comma-separated list of paths, "
|
|
6276
|
+
"or '-' to read the list from stdin."
|
|
6277
|
+
),
|
|
6229
6278
|
),
|
|
6230
6279
|
output_path: Optional[Path] = typer.Option(
|
|
6231
6280
|
None, "--output", "-o",
|
|
@@ -6236,6 +6285,14 @@ def pr_impact_cmd(
|
|
|
6236
6285
|
help="Output format: text (default) or json.",
|
|
6237
6286
|
show_default=True,
|
|
6238
6287
|
),
|
|
6288
|
+
fail_on: Optional[str] = typer.Option(
|
|
6289
|
+
None, "--fail-on",
|
|
6290
|
+
help=(
|
|
6291
|
+
"Exit non-zero when the risk level reaches this threshold: "
|
|
6292
|
+
"critical | high | medium | low | unknown. UNKNOWN (partial evidence) "
|
|
6293
|
+
"always fails when a threshold is set."
|
|
6294
|
+
),
|
|
6295
|
+
),
|
|
6239
6296
|
copy: bool = typer.Option(
|
|
6240
6297
|
False, "--copy", "-c",
|
|
6241
6298
|
help="Copy output to clipboard after a successful run.",
|
|
@@ -6251,6 +6308,11 @@ def pr_impact_cmd(
|
|
|
6251
6308
|
- Event publishers and consumers triggered by the change
|
|
6252
6309
|
- @Transactional methods in the changed classes
|
|
6253
6310
|
- Consolidated risk level (CRITICAL / HIGH / MEDIUM / LOW)
|
|
6311
|
+
- Changed files that could NOT be analyzed (unresolved / non-Java)
|
|
6312
|
+
|
|
6313
|
+
\b
|
|
6314
|
+
A changed Java file that maps to no class caps the verdict at UNKNOWN:
|
|
6315
|
+
a partial blast radius is never reported as a low one.
|
|
6254
6316
|
|
|
6255
6317
|
\b
|
|
6256
6318
|
Reuses existing graph and impact analysis — no new parsers.
|
|
@@ -6260,7 +6322,8 @@ def pr_impact_cmd(
|
|
|
6260
6322
|
Examples:
|
|
6261
6323
|
ask pr-impact --files changed_files.txt
|
|
6262
6324
|
ask pr-impact /path/to/repo --files diff.txt --format json
|
|
6263
|
-
ask pr-impact --files
|
|
6325
|
+
ask pr-impact --files A.java,B.java --fail-on high
|
|
6326
|
+
git diff --name-only origin/main | ask pr-impact --files -
|
|
6264
6327
|
"""
|
|
6265
6328
|
import json as _json
|
|
6266
6329
|
|
|
@@ -6280,33 +6343,53 @@ def pr_impact_cmd(
|
|
|
6280
6343
|
)
|
|
6281
6344
|
raise typer.Exit(code=1)
|
|
6282
6345
|
|
|
6283
|
-
if not files.exists() or files.is_dir():
|
|
6284
|
-
_emit_error_json(
|
|
6285
|
-
INVALID_INPUT_CODE,
|
|
6286
|
-
f"--files '{files}' does not exist or is a directory. Expected a text file listing changed file paths (one per line).",
|
|
6287
|
-
path=str(files),
|
|
6288
|
-
hint=(
|
|
6289
|
-
"Create a file with one changed Java file path per line, then pass it with --files. "
|
|
6290
|
-
"Example: git diff --name-only HEAD~1 > changed.txt && ask pr-impact . --files changed.txt"
|
|
6291
|
-
),
|
|
6292
|
-
expected="A text file containing one Java file path per line.",
|
|
6293
|
-
)
|
|
6294
|
-
raise typer.Exit(code=1)
|
|
6295
|
-
|
|
6296
6346
|
_enforce_format("pr-impact", format)
|
|
6297
6347
|
|
|
6298
|
-
#
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6348
|
+
# --files accepts three shapes: a list file, '-' for stdin, or an inline
|
|
6349
|
+
# comma-separated list. The list-file shape was the only documented one and
|
|
6350
|
+
# the only one the error hint mentioned.
|
|
6351
|
+
_files_raw = str(files).strip()
|
|
6352
|
+
if _files_raw == "-":
|
|
6353
|
+
changed_files = [ln.strip() for ln in sys.stdin.read().splitlines() if ln.strip()]
|
|
6354
|
+
_files_desc = "stdin"
|
|
6355
|
+
else:
|
|
6356
|
+
_files_path = Path(_files_raw)
|
|
6357
|
+
if _files_path.is_file():
|
|
6358
|
+
changed_files = [
|
|
6359
|
+
line.strip()
|
|
6360
|
+
for line in _files_path.read_text(encoding="utf-8").splitlines()
|
|
6361
|
+
if line.strip()
|
|
6362
|
+
]
|
|
6363
|
+
_files_desc = str(_files_path)
|
|
6364
|
+
elif all(
|
|
6365
|
+
p.strip().lower().endswith(".java")
|
|
6366
|
+
for p in _files_raw.split(",") if p.strip()
|
|
6367
|
+
):
|
|
6368
|
+
# Inline path list. Restricted to .java entries so that a mistyped
|
|
6369
|
+
# list-file path ("changd.txt") still errors loudly instead of being
|
|
6370
|
+
# read as a one-element list of changed files.
|
|
6371
|
+
changed_files = [p.strip() for p in _files_raw.split(",") if p.strip()]
|
|
6372
|
+
_files_desc = "inline list"
|
|
6373
|
+
else:
|
|
6374
|
+
_emit_error_json(
|
|
6375
|
+
INVALID_INPUT_CODE,
|
|
6376
|
+
f"--files '{_files_raw}' is not a readable file, a comma-separated list, or '-'.",
|
|
6377
|
+
path=_files_raw,
|
|
6378
|
+
hint=(
|
|
6379
|
+
"Pass a file with one changed path per line "
|
|
6380
|
+
"(git diff --name-only HEAD~1 > changed.txt && ask pr-impact . --files changed.txt), "
|
|
6381
|
+
"a comma-separated list (--files A.java,B.java), or '-' to read from stdin."
|
|
6382
|
+
),
|
|
6383
|
+
expected="A list file, a comma-separated list of paths, or '-'.",
|
|
6384
|
+
)
|
|
6385
|
+
raise typer.Exit(code=1)
|
|
6386
|
+
|
|
6304
6387
|
if not changed_files:
|
|
6305
6388
|
_emit_error_json(
|
|
6306
6389
|
INVALID_INPUT_CODE,
|
|
6307
|
-
f"--files '{
|
|
6308
|
-
hint="
|
|
6309
|
-
expected="One
|
|
6390
|
+
f"--files '{_files_desc}' is empty.",
|
|
6391
|
+
hint="Provide at least one changed file path.",
|
|
6392
|
+
expected="One file path per line, or a comma-separated list.",
|
|
6310
6393
|
)
|
|
6311
6394
|
raise typer.Exit(code=1)
|
|
6312
6395
|
|
|
@@ -6347,6 +6430,23 @@ def pr_impact_cmd(
|
|
|
6347
6430
|
),
|
|
6348
6431
|
)
|
|
6349
6432
|
|
|
6433
|
+
if fail_on:
|
|
6434
|
+
_levels = {"critical": 4, "high": 3, "medium": 2, "low": 1, "unknown": 0}
|
|
6435
|
+
_threshold = _levels.get(fail_on.strip().lower())
|
|
6436
|
+
if _threshold is None:
|
|
6437
|
+
_emit_error_json(
|
|
6438
|
+
INVALID_INPUT_CODE,
|
|
6439
|
+
f"--fail-on '{fail_on}' is not a risk level.",
|
|
6440
|
+
hint="Use one of: critical, high, medium, low, unknown.",
|
|
6441
|
+
expected="critical | high | medium | low | unknown",
|
|
6442
|
+
)
|
|
6443
|
+
raise typer.Exit(code=1)
|
|
6444
|
+
_actual = _levels.get(report.risk_level.lower(), 0)
|
|
6445
|
+
# UNKNOWN means the analysis could not see everything it was asked about.
|
|
6446
|
+
# A gate must not read that as "below threshold".
|
|
6447
|
+
if report.risk_level == "UNKNOWN" or _actual >= _threshold:
|
|
6448
|
+
raise typer.Exit(code=1)
|
|
6449
|
+
|
|
6350
6450
|
|
|
6351
6451
|
# ── Explain Command ───────────────────────────────────────────────────────────
|
|
6352
6452
|
|
|
@@ -6371,6 +6471,11 @@ def explain_cmd(
|
|
|
6371
6471
|
"human-readable text/Markdown, NOT JSON — pass --format json (or use a "
|
|
6372
6472
|
".json output path) for machine-readable output.",
|
|
6373
6473
|
),
|
|
6474
|
+
limit: int = typer.Option(
|
|
6475
|
+
50, "--limit",
|
|
6476
|
+
help="Maximum items per list section (callers, methods, deps). "
|
|
6477
|
+
"0 disables the cap. Truncation is always reported in warnings.",
|
|
6478
|
+
),
|
|
6374
6479
|
copy: bool = typer.Option(
|
|
6375
6480
|
False, "--copy", "-c",
|
|
6376
6481
|
help="Copy output to clipboard after a successful run.",
|
|
@@ -6468,7 +6573,7 @@ def explain_cmd(
|
|
|
6468
6573
|
except Exception:
|
|
6469
6574
|
cir = ContextGraph.build(file_list, target).cir # fallback: never break explain
|
|
6470
6575
|
model = SpringSemanticModel.build(cir)
|
|
6471
|
-
explanation = explain_class(class_name, cir, model)
|
|
6576
|
+
explanation = explain_class(class_name, cir, model).capped(limit)
|
|
6472
6577
|
finally:
|
|
6473
6578
|
_prog.finish()
|
|
6474
6579
|
if _cc_look is not None:
|
|
@@ -7166,7 +7271,8 @@ def modernize_cmd(
|
|
|
7166
7271
|
"that blocks independent module extraction; coupling_type="
|
|
7167
7272
|
"directional (reverse_edge_count=0) is a normal one-way layered "
|
|
7168
7273
|
"dependency, listed for coupling strength, NOT a tangle. Empty "
|
|
7169
|
-
"means no cross-subsystem structural coupling was detected."
|
|
7274
|
+
"means no cross-subsystem structural coupling was detected. "
|
|
7275
|
+
+ CYCLE_METRIC_RECONCILIATION
|
|
7170
7276
|
),
|
|
7171
7277
|
# BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
|
|
7172
7278
|
"recommendation": (
|
|
@@ -7441,13 +7547,33 @@ def auth_status_cmd() -> None:
|
|
|
7441
7547
|
"""Show current authentication and plan status."""
|
|
7442
7548
|
import json as _json
|
|
7443
7549
|
try:
|
|
7444
|
-
from sourcecode.license import
|
|
7550
|
+
from sourcecode.license import (
|
|
7551
|
+
_PRO_UNLOCK_ALL as _unlock,
|
|
7552
|
+
_license_data as _ld,
|
|
7553
|
+
is_pro as _ip,
|
|
7554
|
+
)
|
|
7445
7555
|
except Exception:
|
|
7446
7556
|
_ld = None
|
|
7447
7557
|
_ip = False
|
|
7558
|
+
_unlock = False
|
|
7559
|
+
|
|
7560
|
+
# While the early-adoption unlock is on, every Pro command runs. Reporting
|
|
7561
|
+
# `pro: false` here contradicted the --help header and the commands' own
|
|
7562
|
+
# behaviour: the status has to describe the gating that is actually in force,
|
|
7563
|
+
# not the credential that happens to be absent.
|
|
7564
|
+
_gating = {
|
|
7565
|
+
"pro_gating": "disabled" if _unlock else "enabled",
|
|
7566
|
+
"pro_effective": bool(_ip or _unlock),
|
|
7567
|
+
}
|
|
7568
|
+
if _unlock:
|
|
7569
|
+
_gating["pro_reason"] = "early-adoption unlock: Pro commands run without a license"
|
|
7448
7570
|
|
|
7449
7571
|
if not _ld:
|
|
7450
|
-
out: dict = {
|
|
7572
|
+
out: dict = {
|
|
7573
|
+
"status": "unauthenticated",
|
|
7574
|
+
"pro": bool(_unlock),
|
|
7575
|
+
**_gating,
|
|
7576
|
+
}
|
|
7451
7577
|
sys.stdout.write(_json.dumps(out, ensure_ascii=False) + "\n")
|
|
7452
7578
|
sys.stdout.flush()
|
|
7453
7579
|
return
|
|
@@ -7460,6 +7586,7 @@ def auth_status_cmd() -> None:
|
|
|
7460
7586
|
"plan_status": _ld.get("status", "unknown"),
|
|
7461
7587
|
"pro": _ip,
|
|
7462
7588
|
"validated_at": _ld.get("validated_at") or _ld.get("activated_at") or "",
|
|
7589
|
+
**_gating,
|
|
7463
7590
|
}
|
|
7464
7591
|
sys.stdout.write(_json.dumps(out, indent=2, ensure_ascii=False) + "\n")
|
|
7465
7592
|
sys.stdout.flush()
|
|
@@ -8625,8 +8752,19 @@ def cache_warm_cmd(
|
|
|
8625
8752
|
"""
|
|
8626
8753
|
import shutil as _shutil
|
|
8627
8754
|
import subprocess as _sub
|
|
8628
|
-
|
|
8755
|
+
# Warm exactly the path given. Resolving up to the enclosing git root warmed
|
|
8756
|
+
# (and reported on) the whole monorepo when asked for one module — every other
|
|
8757
|
+
# command scopes to the argument, so `cache warm ./service-a` populated a cache
|
|
8758
|
+
# for a different target than `ask ./service-a` reads.
|
|
8759
|
+
target = Path(path).resolve()
|
|
8760
|
+
_git_root = _resolve_repo_root(Path(path))
|
|
8629
8761
|
typer.echo(f"Warming cache for {target} …", err=True)
|
|
8762
|
+
if _git_root != target:
|
|
8763
|
+
typer.echo(
|
|
8764
|
+
f"(scoped to {target}; the enclosing git repository is {_git_root} — "
|
|
8765
|
+
"git-derived signals are computed within the scope above)",
|
|
8766
|
+
err=True,
|
|
8767
|
+
)
|
|
8630
8768
|
_sc_bin = _shutil.which("sourcecode") or sys.argv[0]
|
|
8631
8769
|
cmd = [_sc_bin, str(target)]
|
|
8632
8770
|
if compact:
|