sourcecode 2.5.2__tar.gz → 2.5.3__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.5.2 → sourcecode-2.5.3}/CHANGELOG.md +65 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/PKG-INFO +1 -1
- {sourcecode-2.5.2 → sourcecode-2.5.3}/pyproject.toml +1 -1
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/canonical_ir.py +3 -2
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/repository_ir.py +188 -27
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/semantic_impact_engine.py +8 -5
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/semantic_services.py +5 -3
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/spring_impact.py +57 -8
- {sourcecode-2.5.2 → sourcecode-2.5.3}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/.gitignore +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/.ruff.toml +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/CONTRIBUTING.md +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/LICENSE +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/README.md +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/SECURITY.md +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/raw +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/cli.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/license.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/scanner.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/serializer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/supabase/functions/README.md +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.5.2 → sourcecode-2.5.3}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.3] — 2026-07-16
|
|
4
|
+
|
|
5
|
+
**impact-chain caller resolution — three false-confident zeros closed.** All three
|
|
6
|
+
defects produced the same dangerous shape: a method-scoped query returning
|
|
7
|
+
`0 callers, risk=low, confidence=high, "isolated"` for a method with real call sites
|
|
8
|
+
in source. Continuation of the 12-repo adversarial audit that drove 2.5.2.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Ambiguous field types dropped receiver-typed edges (P1-A)** (`repository_ir.py`):
|
|
13
|
+
`_receiver_typed_call_edges` resolved a field's declared type by **simple name only**,
|
|
14
|
+
so when two in-repo classes shared that name the lookup was ambiguous and the edge was
|
|
15
|
+
dropped — even though the owning file's `import` said exactly which type it held.
|
|
16
|
+
Broadleaf declares two `AdminSecurityService`, so
|
|
17
|
+
`impact-chain AdminSecurityService#sendResetPasswordNotification` reported an isolated
|
|
18
|
+
low-risk change for a method reached from the public `POST /sendResetPassword` handler.
|
|
19
|
+
`_extract_field_types` now records the import-resolved FQN and `_resolve_declared_type`
|
|
20
|
+
finishes resolution per Java scoping (an explicit import shadows a same-package type,
|
|
21
|
+
JLS 6.4.1). A name that is neither stays unresolved rather than guessed. `field_types`
|
|
22
|
+
values are now an FQN when the declaring file imported the type, else a simple name as
|
|
23
|
+
before; both consumers accept either shape, so a cached CIR from an older version still
|
|
24
|
+
resolves. Verified: query above → `AdminLoginController` / `POST /sendResetPassword`,
|
|
25
|
+
risk=medium; Broadleaf +40 edges, 0 lost; openmrs −2 edges, both phantom
|
|
26
|
+
(`import java.util.Iterator` bound by simple name to an in-repo nested `Iterator`).
|
|
27
|
+
|
|
28
|
+
- **Static calls never landed on the method node (P1-B2)** (`repository_ir.py`): the
|
|
29
|
+
per-file static-call scan discards the callee name and attributes the call to every
|
|
30
|
+
class in the file, so it could only emit `CallerClass --calls--> TargetClass`. No
|
|
31
|
+
method-scoped query on a static utility could ever resolve a caller, however many call
|
|
32
|
+
sites existed (`BroadleafCurrencyUtils#getNumberFormatFromCache` → 0 callers, 5+ real
|
|
33
|
+
static call sites). A repo-wide post-pass now joins facts already extracted (no new
|
|
34
|
+
parse): the invocation atom carries receiver/callee for `Type.method(...)`, the owner
|
|
35
|
+
file's imports resolve the receiver via P1-A's `_resolve_declared_type`, and the symbol
|
|
36
|
+
table supplies that type's methods. Field receivers, lower-case variables and library
|
|
37
|
+
types stay unresolved. Verified: the query above returns 4 callers (all 4 confirmed in
|
|
38
|
+
source), high confidence, risk=medium; Broadleaf +1265 edges, openmrs +1487, 0 edges
|
|
39
|
+
lost anywhere; class-level results unchanged (75 unique caller classes, risk critical);
|
|
40
|
+
build time flat.
|
|
41
|
+
|
|
42
|
+
- **Method-scoped empty blast claimed high confidence (P1-B)** (`spring_impact.py`):
|
|
43
|
+
every blind-spot guard was gated on `class_level_seed`, so a `Class#method` query never
|
|
44
|
+
reached one and an empty method blast was always narrated as
|
|
45
|
+
*"No callers or endpoints found. Low-risk isolated change."* — making the tool
|
|
46
|
+
contradict itself across granularity, silently, with the narrower and more natural
|
|
47
|
+
query being the less trustworthy one. When a method-scoped blast is empty but the
|
|
48
|
+
declaring class has inbound injects/imports, the result is now marked
|
|
49
|
+
`blind_spots: ["method_scope_unproven"]`, confidence drops to low, and the explanation
|
|
50
|
+
points at the class-level re-query. The method may well be uncalled; what is unproven is
|
|
51
|
+
the *emptiness* — so this warns rather than inventing callers, and risk stays as
|
|
52
|
+
computed (CH-005/CH-003/G-2 precedent). The "Low-risk isolated change" sentence is also
|
|
53
|
+
suppressed whenever *any* blind spot fired, which it already contradicted for the three
|
|
54
|
+
pre-existing class-level guards. Broadleaf survey (300 sampled methods, 197 empty
|
|
55
|
+
blasts): the guard fires on 42% of empty results, spot-checked true — static utility
|
|
56
|
+
callers, annotation attribute uses, ORM/serialization setters.
|
|
57
|
+
|
|
58
|
+
### Known limitations
|
|
59
|
+
|
|
60
|
+
- P1-B2's method-level static edges are **added**, not substituted for the class-level
|
|
61
|
+
ones, so a static caller is reported at both granularities (`X` and `X#m`) — redundant,
|
|
62
|
+
though each statement is true. Superseding was implemented first and measured wrong:
|
|
63
|
+
body facts cover method bodies only, so a static call in a field initializer or static
|
|
64
|
+
block yields no atom, and dropping the class edge there erased real coupling
|
|
65
|
+
(`BroadleafCurrencyUtils` lost 1325→561 transitive reach and all 20 affected endpoints).
|
|
66
|
+
Report-level de-duplication is queued in the backlog; no reach is lost today.
|
|
67
|
+
|
|
3
68
|
## [2.5.2] — 2026-07-14
|
|
4
69
|
|
|
5
70
|
**impact-chain correctness + cold-scan performance.** Two fixes surfaced by an
|
|
@@ -253,8 +253,9 @@ class CanonicalRepositoryIR:
|
|
|
253
253
|
|
|
254
254
|
@property
|
|
255
255
|
def field_types(self) -> dict[str, dict[str, str]]:
|
|
256
|
-
"""{class_fqn -> {field_name ->
|
|
257
|
-
|
|
256
|
+
"""{class_fqn -> {field_name -> declared_type}} for every field (annotated or
|
|
257
|
+
plain); the type is import-resolved to an FQN when the declaring file imported
|
|
258
|
+
it, else a simple name. Binds an invocation receiver to a typed dependency —
|
|
258
259
|
covers plain setter/XML-injected fields the injection graph cannot see. A
|
|
259
260
|
projection over field declarations, no new symbols/edges. Present only when
|
|
260
261
|
built via build_canonical_ir; not a cir_hash input."""
|
|
@@ -115,6 +115,11 @@ class EvidenceBundle:
|
|
|
115
115
|
|
|
116
116
|
_PKG_RE = re.compile(r'^package\s+([\w.]+)\s*;', re.MULTILINE)
|
|
117
117
|
_IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;', re.MULTILINE)
|
|
118
|
+
# Single-type imports only (`import a.b.Foo;`) — the declaration that binds a simple
|
|
119
|
+
# name to exactly one type. Static imports bind a member, not a type, and wildcards
|
|
120
|
+
# bind nothing resolvable; both fail to match ([\w.]+ cannot span the space before
|
|
121
|
+
# a static FQN, and `*` is outside the class).
|
|
122
|
+
_SINGLE_TYPE_IMPORT_RE = re.compile(r'^\s*import\s+([\w.]+)\s*;', re.MULTILINE)
|
|
118
123
|
_ANN_RE = re.compile(r'^(@[\w.]+)')
|
|
119
124
|
_ANN_WITH_ARGS_RE = re.compile(
|
|
120
125
|
r'^(@[\w.]+)\s*'
|
|
@@ -2000,19 +2005,39 @@ def _strip_leading_annotations(line: str) -> str:
|
|
|
2000
2005
|
return line.strip()
|
|
2001
2006
|
|
|
2002
2007
|
|
|
2003
|
-
def
|
|
2004
|
-
"""{
|
|
2005
|
-
|
|
2006
|
-
|
|
2008
|
+
def _file_import_map(source: str) -> dict[str, str]:
|
|
2009
|
+
"""{simple name -> FQN} for the file's single-type imports — the per-file fact
|
|
2010
|
+
that disambiguates a declared type whose simple name is not unique in the repo
|
|
2011
|
+
(Broadleaf declares two `AdminSecurityService`; only the import says which)."""
|
|
2012
|
+
out: dict[str, str] = {}
|
|
2013
|
+
for fqn in _SINGLE_TYPE_IMPORT_RE.findall(source):
|
|
2014
|
+
if "." in fqn:
|
|
2015
|
+
out[fqn.rsplit(".", 1)[-1]] = fqn
|
|
2016
|
+
return out
|
|
2017
|
+
|
|
2018
|
+
|
|
2019
|
+
def _extract_field_types(
|
|
2020
|
+
source: str, *, imports: Optional[dict[str, str]] = None
|
|
2021
|
+
) -> dict[str, dict[str, str]]:
|
|
2022
|
+
"""{class_fqn -> {field_name -> declared_type}} for EVERY field (annotated or
|
|
2023
|
+
plain), the fact that binds an invocation receiver to a typed dependency.
|
|
2024
|
+
Complements the injection graph, which only sees @-annotated /
|
|
2007
2025
|
constructor-injected fields — plain setter/XML-injected fields (e.g. openmrs
|
|
2008
2026
|
`private TemplateDAO templateDAO;`) are invisible to it but common.
|
|
2009
2027
|
|
|
2028
|
+
The declared type is the file-import-resolved FQN when the owning file imports
|
|
2029
|
+
it explicitly, else the bare simple name (wildcard import, same package, or
|
|
2030
|
+
JDK-implicit). Consumers must accept both shapes — see `_resolve_declared_type`,
|
|
2031
|
+
which finishes resolution against the repo symbol table.
|
|
2032
|
+
|
|
2010
2033
|
A hash-neutral side-stream (no new symbols/edges; not a cir_hash input),
|
|
2011
2034
|
emitted only under emit_body_facts. Class-scope only: a field sits directly in
|
|
2012
2035
|
a class body (depth == class-decl depth + 1), never inside a method body, so
|
|
2013
2036
|
method locals are excluded. `_FIELD_DECL_RE` (name followed by ';'/'=') is
|
|
2014
2037
|
disjoint from method decls (name followed by '(')."""
|
|
2015
2038
|
out: dict[str, dict[str, str]] = {}
|
|
2039
|
+
if imports is None:
|
|
2040
|
+
imports = _file_import_map(source)
|
|
2016
2041
|
depth = 0
|
|
2017
2042
|
class_stack: list[tuple[str, int]] = []
|
|
2018
2043
|
package = ""
|
|
@@ -2060,7 +2085,8 @@ def _extract_field_types(source: str) -> dict[str, dict[str, str]]:
|
|
|
2060
2085
|
fname = fld_m.group("name")
|
|
2061
2086
|
outer, _ = _type_tokens(fld_m.group("type").strip())
|
|
2062
2087
|
if fname and outer and fname not in _JAVA_KEYWORDS:
|
|
2063
|
-
out.setdefault(class_stack[-1][0], {}).setdefault(
|
|
2088
|
+
out.setdefault(class_stack[-1][0], {}).setdefault(
|
|
2089
|
+
fname, imports.get(outer, outer))
|
|
2064
2090
|
depth += net
|
|
2065
2091
|
_pop_closed(class_stack, depth)
|
|
2066
2092
|
return out
|
|
@@ -4393,6 +4419,121 @@ def extract_file_ir(
|
|
|
4393
4419
|
return _assemble(symbols, relations, changed_symbols, spring_summary, route_diffs)
|
|
4394
4420
|
|
|
4395
4421
|
|
|
4422
|
+
def _resolve_declared_type(
|
|
4423
|
+
declared: str,
|
|
4424
|
+
owner_cls: str,
|
|
4425
|
+
type_simple_to_fqn: dict[str, list[str]],
|
|
4426
|
+
known_types: set[str],
|
|
4427
|
+
) -> Optional[str]:
|
|
4428
|
+
"""The in-repo class FQN a field's declared type names, or None when it cannot
|
|
4429
|
+
be pinned to exactly one.
|
|
4430
|
+
|
|
4431
|
+
`declared` is what `_extract_field_types` recorded: an import-resolved FQN when
|
|
4432
|
+
the owning file imported the type explicitly, else a bare simple name. Resolution
|
|
4433
|
+
follows Java scoping order — an explicit import shadows a same-package type
|
|
4434
|
+
(JLS 6.4.1), which in turn beats a repo-wide simple-name guess. A simple name
|
|
4435
|
+
that is neither same-package nor unique stays unresolved rather than guessed
|
|
4436
|
+
(INV honesty): the wrong caller edge is worse than a missing one.
|
|
4437
|
+
"""
|
|
4438
|
+
if not declared:
|
|
4439
|
+
return None
|
|
4440
|
+
if "." in declared:
|
|
4441
|
+
return declared if declared in known_types else None
|
|
4442
|
+
if "." in owner_cls:
|
|
4443
|
+
scoped = f"{owner_cls.rsplit('.', 1)[0]}.{declared}"
|
|
4444
|
+
if scoped in known_types:
|
|
4445
|
+
return scoped
|
|
4446
|
+
fqns = type_simple_to_fqn.get(declared)
|
|
4447
|
+
return fqns[0] if fqns and len(fqns) == 1 else None
|
|
4448
|
+
|
|
4449
|
+
|
|
4450
|
+
def _type_indices(
|
|
4451
|
+
symbols: list[SymbolRecord],
|
|
4452
|
+
) -> tuple[dict[str, list[str]], set[str], dict[str, dict[str, list[str]]]]:
|
|
4453
|
+
"""(type simple name → in-repo class FQN(s), all class FQNs, class FQN →
|
|
4454
|
+
{method name → [method FQN]}) — the symbol-table views the call-edge post-passes
|
|
4455
|
+
join against."""
|
|
4456
|
+
type_simple_to_fqn: dict[str, list[str]] = {}
|
|
4457
|
+
known_types: set[str] = set()
|
|
4458
|
+
class_methods: dict[str, dict[str, list[str]]] = {}
|
|
4459
|
+
for s in symbols:
|
|
4460
|
+
if s.type in ("class", "interface", "enum", "record") and "#" not in s.symbol:
|
|
4461
|
+
type_simple_to_fqn.setdefault(s.symbol.rsplit(".", 1)[-1], []).append(s.symbol)
|
|
4462
|
+
known_types.add(s.symbol)
|
|
4463
|
+
elif s.symbol_kind == "method" and "#" in s.symbol:
|
|
4464
|
+
cls = _enclosing_class(s.symbol)
|
|
4465
|
+
name = s.symbol.rsplit("#", 1)[1]
|
|
4466
|
+
class_methods.setdefault(cls, {}).setdefault(name, []).append(s.symbol)
|
|
4467
|
+
return type_simple_to_fqn, known_types, class_methods
|
|
4468
|
+
|
|
4469
|
+
|
|
4470
|
+
def _static_typed_call_edges(
|
|
4471
|
+
body_facts: dict[str, list[dict]],
|
|
4472
|
+
class_imports: dict[str, dict[str, str]],
|
|
4473
|
+
field_types: dict[str, dict[str, str]],
|
|
4474
|
+
symbols: list[SymbolRecord],
|
|
4475
|
+
) -> list[RelationEdge]:
|
|
4476
|
+
"""Method-level `calls` edges for a static invocation through a type receiver
|
|
4477
|
+
(`Type.method(...)`).
|
|
4478
|
+
|
|
4479
|
+
The per-file static scan in `_build_relations` throws the callee name away and
|
|
4480
|
+
attributes the call to every class in the file, so it can only say
|
|
4481
|
+
`CallerClass --calls--> TargetClass`. Nothing ever lands on the method node, so
|
|
4482
|
+
`impact-chain Utility#method` resolved 0 callers however many static call sites
|
|
4483
|
+
existed: Broadleaf's `BroadleafCurrencyUtils#getNumberFormatFromCache` has 5+
|
|
4484
|
+
in-repo callers and reported none, which P1-B could only flag as unproven
|
|
4485
|
+
because the edge itself was missing.
|
|
4486
|
+
|
|
4487
|
+
Joins facts already extracted (no new parse — Prime Directive): the invocation
|
|
4488
|
+
atom already carries `receiver`/`callee` for `Type.method(...)`, the owner file's
|
|
4489
|
+
imports resolve the receiver to a type (`_resolve_declared_type`), and the symbol
|
|
4490
|
+
table supplies that type's methods. A receiver that is a field, a lower-case
|
|
4491
|
+
variable, or an unresolvable/library type yields nothing — unresolved stays
|
|
4492
|
+
unresolved (INV honesty).
|
|
4493
|
+
|
|
4494
|
+
These edges add to the coarse class-level ones; see the call site for why
|
|
4495
|
+
substituting them loses reach.
|
|
4496
|
+
"""
|
|
4497
|
+
if not body_facts or not class_imports:
|
|
4498
|
+
return []
|
|
4499
|
+
|
|
4500
|
+
type_simple_to_fqn, known_types, class_methods = _type_indices(symbols)
|
|
4501
|
+
edges: list[RelationEdge] = []
|
|
4502
|
+
for owner_fqn in sorted(body_facts):
|
|
4503
|
+
owner_cls = _enclosing_class(owner_fqn)
|
|
4504
|
+
imports = class_imports.get(owner_cls)
|
|
4505
|
+
if imports is None:
|
|
4506
|
+
continue
|
|
4507
|
+
fields = field_types.get(owner_cls) or {}
|
|
4508
|
+
for atom in body_facts[owner_fqn]:
|
|
4509
|
+
if atom.get("fact") != "invocation":
|
|
4510
|
+
continue
|
|
4511
|
+
receiver, callee = atom.get("receiver"), atom.get("callee")
|
|
4512
|
+
if not receiver or not callee or receiver == "this":
|
|
4513
|
+
continue
|
|
4514
|
+
if not receiver[0].isupper():
|
|
4515
|
+
continue # variable/field receiver — `_receiver_typed_call_edges`
|
|
4516
|
+
if receiver in fields:
|
|
4517
|
+
continue # a field shadows the type name → the field pass owns it
|
|
4518
|
+
target_cls = _resolve_declared_type(
|
|
4519
|
+
imports.get(receiver, receiver), owner_cls, type_simple_to_fqn, known_types
|
|
4520
|
+
)
|
|
4521
|
+
if not target_cls or target_cls == owner_cls:
|
|
4522
|
+
continue
|
|
4523
|
+
target_methods = class_methods.get(target_cls, {}).get(callee)
|
|
4524
|
+
if not target_methods:
|
|
4525
|
+
continue # callee not declared on the target type
|
|
4526
|
+
for mfqn in target_methods:
|
|
4527
|
+
edges.append(RelationEdge(
|
|
4528
|
+
from_symbol=owner_fqn,
|
|
4529
|
+
to_symbol=mfqn,
|
|
4530
|
+
type="calls",
|
|
4531
|
+
confidence="medium",
|
|
4532
|
+
evidence={"type": "method_call", "value": f"{receiver}.{callee}(...)"},
|
|
4533
|
+
))
|
|
4534
|
+
return edges
|
|
4535
|
+
|
|
4536
|
+
|
|
4396
4537
|
def _receiver_typed_call_edges(
|
|
4397
4538
|
body_facts: dict[str, list[dict]],
|
|
4398
4539
|
field_types: dict[str, dict[str, str]],
|
|
@@ -4412,30 +4553,22 @@ def _receiver_typed_call_edges(
|
|
|
4412
4553
|
Joins facts already extracted (no new parse — Prime Directive):
|
|
4413
4554
|
- invocation atom (owner method → {receiver, callee}) [body_facts]
|
|
4414
4555
|
- declared type of `receiver` on the owner's class [field_types]
|
|
4415
|
-
- target type resolved to a SINGLE in-repo class FQN
|
|
4556
|
+
- target type resolved to a SINGLE in-repo class FQN, by the owner
|
|
4557
|
+
file's import first and only then by simple name [symbols]
|
|
4416
4558
|
- callee resolved to that class's method(s) of that name [symbols]
|
|
4417
4559
|
|
|
4418
4560
|
Emits `owner_method --calls--> target_class#callee` (every overload of the name —
|
|
4419
4561
|
arity is not resolved by name alone, matching `_intra_class_call_edges`). Anything
|
|
4420
|
-
unresolved — a local variable (no field entry), a JDK/library type,
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
a repo-wide post-pass because
|
|
4562
|
+
unresolved — a local variable (no field entry), a JDK/library type, a simple name
|
|
4563
|
+
that is still ambiguous after `_resolve_declared_type`, or a callee the target
|
|
4564
|
+
class does not declare — yields nothing: an unresolved fact stays unresolved
|
|
4565
|
+
rather than being guessed (INV honesty). Runs as a repo-wide post-pass because
|
|
4566
|
+
the target type + its methods live in other files.
|
|
4424
4567
|
"""
|
|
4425
4568
|
if not body_facts or not field_types:
|
|
4426
4569
|
return []
|
|
4427
4570
|
|
|
4428
|
-
|
|
4429
|
-
type_simple_to_fqn: dict[str, list[str]] = {}
|
|
4430
|
-
class_methods: dict[str, dict[str, list[str]]] = {}
|
|
4431
|
-
for s in symbols:
|
|
4432
|
-
if s.type in ("class", "interface", "enum", "record") and "#" not in s.symbol:
|
|
4433
|
-
type_simple_to_fqn.setdefault(s.symbol.rsplit(".", 1)[-1], []).append(s.symbol)
|
|
4434
|
-
elif s.symbol_kind == "method" and "#" in s.symbol:
|
|
4435
|
-
cls = _enclosing_class(s.symbol)
|
|
4436
|
-
name = s.symbol.rsplit("#", 1)[1]
|
|
4437
|
-
class_methods.setdefault(cls, {}).setdefault(name, []).append(s.symbol)
|
|
4438
|
-
|
|
4571
|
+
type_simple_to_fqn, known_types, class_methods = _type_indices(symbols)
|
|
4439
4572
|
edges: list[RelationEdge] = []
|
|
4440
4573
|
for owner_fqn in sorted(body_facts):
|
|
4441
4574
|
owner_cls = _enclosing_class(owner_fqn)
|
|
@@ -4448,13 +4581,13 @@ def _receiver_typed_call_edges(
|
|
|
4448
4581
|
receiver, callee = atom.get("receiver"), atom.get("callee")
|
|
4449
4582
|
if not receiver or receiver == "this" or not callee:
|
|
4450
4583
|
continue
|
|
4451
|
-
|
|
4452
|
-
if not
|
|
4584
|
+
declared = fmap.get(receiver)
|
|
4585
|
+
if not declared:
|
|
4453
4586
|
continue # not a typed field (local / unknown)
|
|
4454
|
-
|
|
4455
|
-
|
|
4587
|
+
target_cls = _resolve_declared_type(
|
|
4588
|
+
declared, owner_cls, type_simple_to_fqn, known_types)
|
|
4589
|
+
if not target_cls:
|
|
4456
4590
|
continue # unresolved / ambiguous type → skip (honest)
|
|
4457
|
-
target_cls = tfqns[0]
|
|
4458
4591
|
if target_cls == owner_cls:
|
|
4459
4592
|
continue # intra-class handled by _intra_class_call_edges
|
|
4460
4593
|
target_methods = class_methods.get(target_cls, {}).get(callee)
|
|
@@ -4508,6 +4641,7 @@ def build_repo_ir(
|
|
|
4508
4641
|
_all_class_type_refs: dict[str, list[dict]] = {}
|
|
4509
4642
|
_all_annotation_arg_values: dict[str, dict[str, dict[str, str]]] = {}
|
|
4510
4643
|
_all_field_types: dict[str, dict[str, str]] = {}
|
|
4644
|
+
_all_class_imports: dict[str, dict[str, str]] = {}
|
|
4511
4645
|
|
|
4512
4646
|
# H-04: prefetch changed-file list once; avoids O(n) `git show` calls.
|
|
4513
4647
|
# _since_changed=None means git unavailable → fall back to per-file fetch.
|
|
@@ -4580,7 +4714,14 @@ def build_repo_ir(
|
|
|
4580
4714
|
_all_guard_facts.update(_extract_guard_facts(symbols, source))
|
|
4581
4715
|
_all_class_type_refs.update(_extract_class_type_refs(symbols))
|
|
4582
4716
|
_all_annotation_arg_values.update(_parse_annotation_arg_values(symbols))
|
|
4583
|
-
|
|
4717
|
+
_f_imports = _file_import_map(source)
|
|
4718
|
+
_all_field_types.update(_extract_field_types(source, imports=_f_imports))
|
|
4719
|
+
# Every type declared in this file resolves simple names through this
|
|
4720
|
+
# file's imports — the fact `_static_typed_call_edges` needs to bind a
|
|
4721
|
+
# `Type.method(...)` receiver to a type (P1-A's rule, applied to statics).
|
|
4722
|
+
for _s in symbols:
|
|
4723
|
+
if _s.type in ("class", "interface", "enum", "record") and "#" not in _s.symbol:
|
|
4724
|
+
_all_class_imports[_s.symbol] = _f_imports
|
|
4584
4725
|
|
|
4585
4726
|
old_source: Optional[str] = None
|
|
4586
4727
|
if since:
|
|
@@ -4626,6 +4767,26 @@ def build_repo_ir(
|
|
|
4626
4767
|
_receiver_typed_call_edges(_all_body_facts, _all_field_types, all_symbols)
|
|
4627
4768
|
)
|
|
4628
4769
|
|
|
4770
|
+
# Static calls through a type receiver (`Type.method(...)`). The per-file scan can
|
|
4771
|
+
# only reach the target CLASS (it discards the callee), so no method-level query on
|
|
4772
|
+
# a static utility ever resolved a caller.
|
|
4773
|
+
#
|
|
4774
|
+
# These edges are ADDED, never substituted for the class-level ones. Superseding the
|
|
4775
|
+
# class edge was tried and measurably wrong: body facts cover method bodies only, so
|
|
4776
|
+
# a static call in a field initializer or static block produces no invocation atom,
|
|
4777
|
+
# and dropping the class edge there erased the coupling — Broadleaf's
|
|
4778
|
+
# BroadleafCurrencyUtils lost transitive reach (1325 indirect callers → 561) and all
|
|
4779
|
+
# 20 affected endpoints. The class-level edge is a coarse over-approximation (the
|
|
4780
|
+
# scan attributes a call to every class in the file); this pass is precise but
|
|
4781
|
+
# partial. Keeping both leaves a caller reported at both granularities (`X` and
|
|
4782
|
+
# `X#m`) — redundant, but each statement is true, and no reach is lost.
|
|
4783
|
+
if emit_body_facts:
|
|
4784
|
+
all_relations.extend(
|
|
4785
|
+
_static_typed_call_edges(
|
|
4786
|
+
_all_body_facts, _all_class_imports, _all_field_types, all_symbols
|
|
4787
|
+
)
|
|
4788
|
+
)
|
|
4789
|
+
|
|
4629
4790
|
spring_summary = _build_spring_summary(all_symbols)
|
|
4630
4791
|
|
|
4631
4792
|
# Deduplicate relations
|
|
@@ -28,6 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
from dataclasses import dataclass
|
|
29
29
|
from typing import TYPE_CHECKING, Optional
|
|
30
30
|
|
|
31
|
+
from sourcecode.repository_ir import _resolve_declared_type
|
|
31
32
|
from sourcecode.semantic_services import SemanticServices
|
|
32
33
|
|
|
33
34
|
if TYPE_CHECKING: # avoid import cycle
|
|
@@ -236,13 +237,15 @@ class SemanticImpactEngine:
|
|
|
236
237
|
"""(field_name, type_fqn) for fields whose declared type resolves to a role
|
|
237
238
|
in `roles` — the plain setter/XML-injected dependencies the injection graph
|
|
238
239
|
cannot see (e.g. openmrs `private TemplateDAO templateDAO;`). Uses field-type
|
|
239
|
-
facts (#10)
|
|
240
|
+
facts (#10), which carry the owning file's import-resolved FQN when it had
|
|
241
|
+
one; a bare simple name still resolves only while unambiguous."""
|
|
240
242
|
by_simple = self.s.types_by_simple()
|
|
243
|
+
known = {f for fqns in by_simple.values() for f in fqns}
|
|
241
244
|
out: list[tuple[str, str]] = []
|
|
242
|
-
for fname,
|
|
243
|
-
|
|
244
|
-
if
|
|
245
|
-
out.append((fname,
|
|
245
|
+
for fname, declared in sorted(self.s.field_types_of(owner_fqn).items()):
|
|
246
|
+
fqn = _resolve_declared_type(declared, owner_fqn, by_simple, known)
|
|
247
|
+
if fqn and self._role(fqn) in roles:
|
|
248
|
+
out.append((fname, fqn))
|
|
246
249
|
return out
|
|
247
250
|
|
|
248
251
|
def _called_via_receiver(self, owner_fqn: str, receiver: str) -> Optional[str]:
|
|
@@ -209,9 +209,11 @@ class SemanticServices:
|
|
|
209
209
|
return self._g.annotation_arg_values_in(fqn)
|
|
210
210
|
|
|
211
211
|
def field_types_of(self, class_fqn: str) -> dict[str, str]:
|
|
212
|
-
"""{field_name ->
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
"""{field_name -> declared_type} for a class's fields (annotated or plain).
|
|
213
|
+
Resolves a body invocation's receiver to its typed dependency — the fact
|
|
214
|
+
injection edges miss for plain setter/XML-injected fields. The type is the
|
|
215
|
+
FQN when the declaring file imported it explicitly, else a simple name;
|
|
216
|
+
`repository_ir._resolve_declared_type` handles both."""
|
|
215
217
|
return self._g.field_types_in(class_fqn)
|
|
216
218
|
|
|
217
219
|
def return_type_index(self) -> dict[str, set[str]]:
|
|
@@ -744,10 +744,21 @@ def _build_chain_explanation(
|
|
|
744
744
|
endpoints_affected: int,
|
|
745
745
|
findings: int,
|
|
746
746
|
confidence: str,
|
|
747
|
+
blind_spot: bool = False,
|
|
747
748
|
) -> str:
|
|
748
749
|
"""Human-readable rationale, phrased to match the `impact` command's
|
|
749
|
-
explanation so both surfaces read consistently.
|
|
750
|
+
explanation so both surfaces read consistently.
|
|
751
|
+
|
|
752
|
+
`blind_spot` says a positively-detected gap explains the empty result, so it must
|
|
753
|
+
not be narrated as an isolated change: the warnings carry the diagnosis, and this
|
|
754
|
+
sentence is what an agent quotes."""
|
|
750
755
|
if not direct_callers and not indirect_callers and not endpoints_affected:
|
|
756
|
+
if blind_spot:
|
|
757
|
+
return (
|
|
758
|
+
"No callers or endpoints resolved, but this analysis has a known blind "
|
|
759
|
+
"spot (see analysis_warnings) — an empty result here is NOT evidence of "
|
|
760
|
+
"an isolated or low-risk change."
|
|
761
|
+
)
|
|
751
762
|
return "No callers or endpoints found in the impact chain. Low-risk isolated change."
|
|
752
763
|
parts: list[str] = []
|
|
753
764
|
if direct_callers:
|
|
@@ -1130,6 +1141,43 @@ class ImpactOrchestrator:
|
|
|
1130
1141
|
"proof this symbol is unused."
|
|
1131
1142
|
)
|
|
1132
1143
|
|
|
1144
|
+
# P1-B: every guard above needs a class-level seed, so a `Class#method` query
|
|
1145
|
+
# never reached one — an empty method blast was reported as a high-confidence
|
|
1146
|
+
# "low-risk isolated change" no matter what was known about the class. When the
|
|
1147
|
+
# declaring class has inbound DI/import edges, the class-level query resolves
|
|
1148
|
+
# those callers correctly (they land on the class node), so the tool used to
|
|
1149
|
+
# contradict itself across granularity — "isolated" for the method, "critical"
|
|
1150
|
+
# for its class — with no signal that the narrower, more natural query was the
|
|
1151
|
+
# less trustworthy one. The method may still be genuinely uncalled; what is
|
|
1152
|
+
# unproven is the *emptiness*, so this downgrades confidence and warns rather
|
|
1153
|
+
# than inventing callers.
|
|
1154
|
+
method_scope_blind_spot = False
|
|
1155
|
+
if empty_blast and not class_level_seed and resolution != "not_found":
|
|
1156
|
+
_crev = cir.reverse_graph.get(_class_of(resolved_symbol)) or {}
|
|
1157
|
+
method_scope_inbound = sorted(
|
|
1158
|
+
{c for et in ("injects", "imports") for c in (_crev.get(et) or [])}
|
|
1159
|
+
)
|
|
1160
|
+
if method_scope_inbound:
|
|
1161
|
+
method_scope_blind_spot = True
|
|
1162
|
+
warnings.append(
|
|
1163
|
+
f"Method-scoped empty blast radius is unproven (P1-B): no call edge "
|
|
1164
|
+
f"resolves to this method, but {len(method_scope_inbound)} in-repo "
|
|
1165
|
+
f"class(es) inject or import the declaring class "
|
|
1166
|
+
f"{_class_of(resolved_symbol)}. The call may travel an edge the "
|
|
1167
|
+
"graph does not model (wildcard-imported or generic receiver, "
|
|
1168
|
+
"reflection, method reference, framework callback). 0 callers is "
|
|
1169
|
+
"NOT proof this method is unused — re-query at class level (drop "
|
|
1170
|
+
"the '#method') to see the declaring class's callers."
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
blind_spots = (
|
|
1174
|
+
# framework_di stops being a blind spot once CH-007 recovers callers
|
|
1175
|
+
(["framework_di"] if framework_di_blind_spot and not di_recovered else [])
|
|
1176
|
+
+ (["value_type"] if value_type_blind_spot else [])
|
|
1177
|
+
+ (["unresolved_refs"] if unresolved_ref_blind_spot else [])
|
|
1178
|
+
+ (["method_scope_unproven"] if method_scope_blind_spot else [])
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1133
1181
|
confidence: str
|
|
1134
1182
|
if resolution == "not_found":
|
|
1135
1183
|
confidence = "low"
|
|
@@ -1137,7 +1185,12 @@ class ImpactOrchestrator:
|
|
|
1137
1185
|
# Callers recovered structurally via the external-interface DI bridge.
|
|
1138
1186
|
# Unambiguous binding (single in-repo impl) → medium; ambiguous → low.
|
|
1139
1187
|
confidence = "low" if di_binding_ambiguous else "medium"
|
|
1140
|
-
elif
|
|
1188
|
+
elif (
|
|
1189
|
+
framework_di_blind_spot
|
|
1190
|
+
or value_type_blind_spot
|
|
1191
|
+
or unresolved_ref_blind_spot
|
|
1192
|
+
or method_scope_blind_spot
|
|
1193
|
+
):
|
|
1141
1194
|
confidence = "low"
|
|
1142
1195
|
elif resolution == "partial" or confidence_reducing:
|
|
1143
1196
|
confidence = "medium"
|
|
@@ -1168,6 +1221,7 @@ class ImpactOrchestrator:
|
|
|
1168
1221
|
len(endpoints_affected),
|
|
1169
1222
|
len(impact_findings),
|
|
1170
1223
|
confidence,
|
|
1224
|
+
blind_spot=bool(blind_spots),
|
|
1171
1225
|
),
|
|
1172
1226
|
metadata={
|
|
1173
1227
|
"analysis_depth": depth,
|
|
@@ -1177,12 +1231,7 @@ class ImpactOrchestrator:
|
|
|
1177
1231
|
"risk_score": risk_score,
|
|
1178
1232
|
"model_build_time_ms": model.build_time_ms,
|
|
1179
1233
|
"query_time_ms": elapsed_ms,
|
|
1180
|
-
"blind_spots":
|
|
1181
|
-
# framework_di stops being a blind spot once CH-007 recovers callers
|
|
1182
|
-
(["framework_di"] if framework_di_blind_spot and not di_recovered else [])
|
|
1183
|
-
+ (["value_type"] if value_type_blind_spot else [])
|
|
1184
|
-
+ (["unresolved_refs"] if unresolved_ref_blind_spot else [])
|
|
1185
|
-
),
|
|
1234
|
+
"blind_spots": blind_spots,
|
|
1186
1235
|
"external_supertypes": external_supertypes,
|
|
1187
1236
|
"external_iface_callers_recovered": len(di_recovered_callers),
|
|
1188
1237
|
"external_iface_binding_ambiguous": di_binding_ambiguous,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|