sourcecode 1.55.0__tar.gz → 1.56.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.
Files changed (121) hide show
  1. {sourcecode-1.55.0 → sourcecode-1.56.0}/CHANGELOG.md +32 -0
  2. {sourcecode-1.55.0 → sourcecode-1.56.0}/PKG-INFO +7 -4
  3. {sourcecode-1.55.0 → sourcecode-1.56.0}/README.md +6 -3
  4. {sourcecode-1.55.0 → sourcecode-1.56.0}/pyproject.toml +1 -1
  5. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/spring_impact.py +124 -1
  7. {sourcecode-1.55.0 → sourcecode-1.56.0}/.github/workflows/build-windows.yml +0 -0
  8. {sourcecode-1.55.0 → sourcecode-1.56.0}/.gitignore +0 -0
  9. {sourcecode-1.55.0 → sourcecode-1.56.0}/.ruff.toml +0 -0
  10. {sourcecode-1.55.0 → sourcecode-1.56.0}/CONTRIBUTING.md +0 -0
  11. {sourcecode-1.55.0 → sourcecode-1.56.0}/LICENSE +0 -0
  12. {sourcecode-1.55.0 → sourcecode-1.56.0}/SECURITY.md +0 -0
  13. {sourcecode-1.55.0 → sourcecode-1.56.0}/raw +0 -0
  14. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/adaptive_scanner.py +0 -0
  15. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/architecture_analyzer.py +0 -0
  16. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/architecture_summary.py +0 -0
  17. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/ast_extractor.py +0 -0
  18. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/cache.py +0 -0
  19. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/canonical_ir.py +0 -0
  20. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/cir_graphs.py +0 -0
  21. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/classifier.py +0 -0
  22. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/cli.py +0 -0
  23. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  24. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/confidence_analyzer.py +0 -0
  25. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/context_scorer.py +0 -0
  26. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/context_summarizer.py +0 -0
  27. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/contract_model.py +0 -0
  28. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/contract_pipeline.py +0 -0
  29. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/coverage_parser.py +0 -0
  30. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/dependency_analyzer.py +0 -0
  31. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/__init__.py +0 -0
  32. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/base.py +0 -0
  33. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  34. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/dart.py +0 -0
  35. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/dotnet.py +0 -0
  36. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/elixir.py +0 -0
  37. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/go.py +0 -0
  38. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/heuristic.py +0 -0
  39. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/hybrid.py +0 -0
  40. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/java.py +0 -0
  41. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  42. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/nodejs.py +0 -0
  43. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/parsers.py +0 -0
  44. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/php.py +0 -0
  45. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/project.py +0 -0
  46. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/python.py +0 -0
  47. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/ruby.py +0 -0
  48. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/rust.py +0 -0
  49. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/systems.py +0 -0
  50. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/terraform.py +0 -0
  51. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/detectors/tooling.py +0 -0
  52. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/doc_analyzer.py +0 -0
  53. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  54. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/env_analyzer.py +0 -0
  55. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/error_schema.py +0 -0
  56. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/explain.py +0 -0
  57. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/file_chunker.py +0 -0
  58. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/file_classifier.py +0 -0
  59. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/flow_analyzer.py +0 -0
  60. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/format_contract.py +0 -0
  61. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/fqn_utils.py +0 -0
  62. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/git_analyzer.py +0 -0
  63. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/graph_analyzer.py +0 -0
  64. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/integration_detector.py +0 -0
  65. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/license.py +0 -0
  66. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/__init__.py +0 -0
  67. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  68. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  69. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  70. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  71. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  72. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  73. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/registry.py +0 -0
  74. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/runner.py +0 -0
  75. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp/server.py +0 -0
  76. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/mcp_nudge.py +0 -0
  77. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/metrics_analyzer.py +0 -0
  78. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/migrate_check.py +0 -0
  79. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/openapi_surface.py +0 -0
  80. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/output_budget.py +0 -0
  81. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/path_filters.py +0 -0
  82. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  83. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/pr_impact.py +0 -0
  84. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/prepare_context.py +0 -0
  85. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/progress.py +0 -0
  86. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/ranking_engine.py +0 -0
  87. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/redactor.py +0 -0
  88. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/relevance_scorer.py +0 -0
  89. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/rename_refactor.py +0 -0
  90. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/repo_classifier.py +0 -0
  91. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/repository_ir.py +0 -0
  92. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/ris.py +0 -0
  93. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/runtime_classifier.py +0 -0
  94. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/scanner.py +0 -0
  95. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/schema.py +0 -0
  96. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/security_config.py +0 -0
  97. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/semantic_analyzer.py +0 -0
  98. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/serializer.py +0 -0
  99. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/spring_event_topology.py +0 -0
  100. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/spring_findings.py +0 -0
  101. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/spring_model.py +0 -0
  102. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/spring_security_audit.py +0 -0
  103. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/spring_semantic.py +0 -0
  104. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  105. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/summarizer.py +0 -0
  106. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/telemetry/__init__.py +0 -0
  107. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/telemetry/config.py +0 -0
  108. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/telemetry/consent.py +0 -0
  109. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/telemetry/events.py +0 -0
  110. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/telemetry/filters.py +0 -0
  111. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/telemetry/transport.py +0 -0
  112. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/tree_utils.py +0 -0
  113. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/validation_surface.py +0 -0
  114. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/version_check.py +0 -0
  115. {sourcecode-1.55.0 → sourcecode-1.56.0}/src/sourcecode/workspace.py +0 -0
  116. {sourcecode-1.55.0 → sourcecode-1.56.0}/supabase/functions/README.md +0 -0
  117. {sourcecode-1.55.0 → sourcecode-1.56.0}/supabase/functions/get-license/index.ts +0 -0
  118. {sourcecode-1.55.0 → sourcecode-1.56.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  119. {sourcecode-1.55.0 → sourcecode-1.56.0}/supabase/functions/telemetry/index.ts +0 -0
  120. {sourcecode-1.55.0 → sourcecode-1.56.0}/supabase/sql/license_event_ordering.sql +0 -0
  121. {sourcecode-1.55.0 → sourcecode-1.56.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.56.0] — 2026-06-19
4
+
5
+ ### Added
6
+ - **CH-007 — `impact-chain` recovers callers wired through an external interface.**
7
+ The flagship blast-radius gap (BroadleafCommerce #3124): a class wired by DI through
8
+ an external framework interface (e.g. a Spring Security `UserDetailsService` /
9
+ `RedirectStrategy` impl) reported `0 callers` because consumers inject the *interface*,
10
+ never the impl — so no reverse-graph edge names it and CH-001b (in-repo interface
11
+ expansion) cannot bridge. CH-005 only warned; it did not recover the callers.
12
+
13
+ `_recover_external_iface_callers()` now reads the raw dependency edges (which record
14
+ `<consumer> -[injects|calls|instantiates|returns]-> <external interface>` even though
15
+ those edges never reach `reverse_graph`) and attributes the in-repo wiring/consumer
16
+ classes as callers, then recomputes the endpoints/findings/surfaces/risk that depend
17
+ on them. In-repo implementors of the interface are counted: exactly one → unambiguous
18
+ binding (`confidence:medium`, `framework_di` dropped from `metadata.blind_spots`);
19
+ several → `metadata.external_iface_binding_ambiguous:true`, confidence stays `low`
20
+ with an ambiguity warning. No in-repo wiring → the honest CH-005 blind-spot path is
21
+ unchanged. New metadata: `external_iface_callers_recovered`, `external_iface_binding_ambiguous`.
22
+
23
+ - **Precision: concrete JDK base classes excluded from DI detection.** `class Foo extends
24
+ ArrayList`/`Stack`/`InputStream` is implementation reuse, not DI dispatch, and a consumer
25
+ holding an `ArrayList` field is not a caller of `Foo`. Added `_NON_DI_SUPERTYPES`
26
+ (java.util containers, java.io streams, java.lang bases) to the external-supertype
27
+ filter — found via real-repo validation, where it cut false recoveries from 12 to 7.
28
+
29
+ Validated on BroadleafCommerce (2762 files, 22 247 symbols): CH-007 recovers wiring
30
+ callers for 7 classes, all genuine DI interfaces; querying a `UserDetailsService`
31
+ implementation recovers the security-config + login-service classes that wire it.
32
+ Report: `.planning/benchmark-broadleaf-3124.md`. Tests: `TestExternalInterfaceDIBridge`
33
+ (recovery / ambiguity / no-wiring fallback).
34
+
3
35
  ## [1.55.0] — 2026-06-19
4
36
 
5
37
  ### Security
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.55.0
3
+ Version: 1.56.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
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
42
42
 
43
- ![Version](https://img.shields.io/badge/version-1.55.0-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.56.0-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
45
45
 
46
46
  ---
@@ -459,9 +459,12 @@ Unlike `impact` (which traces the caller graph), `impact-chain` builds on the Sp
459
459
  | `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
460
460
  | `risk_level` | `critical` \| `high` \| `medium` \| `low` |
461
461
  | `confidence` | `high` \| `medium` \| `low` — `low` on a detected blind spot, `medium` on partial resolution or capped traversal. Informational interface↔impl expansion notices do **not** lower it, so a clean resolved query stays `high`. |
462
- | `metadata.blind_spots` | `framework_di` and/or `value_type` when an empty result is unmodeled-edge driven, not real dead code |
462
+ | `metadata.blind_spots` | `framework_di` and/or `value_type` when an empty result is unmodeled-edge driven, not real dead code (CH-007 drops `framework_di` once it recovers the wiring callers) |
463
+ | `metadata.external_iface_callers_recovered` / `external_iface_binding_ambiguous` | CH-007 — count of in-repo wiring callers recovered through an external interface, and whether the impl→bean binding is ambiguous (multiple in-repo implementors) |
463
464
 
464
- **Framework/DI blind spot (CH-005).** An empty blast radius is ambiguous: genuinely unused, or invoked through an edge the static graph does not model. When the target class implements/extends an **external** framework type (e.g. Spring Security's `RedirectStrategy`, a servlet `Filter`) it is typically wired by framework DI/config and invoked polymorphically — no in-repo edge names its methods, so `direct_callers` is `0`. Rather than report that as `risk:low` at high confidence (a dangerous false negative that reads as "safe to change"), `impact-chain` detects the external supertype, drops `confidence` to `low`, lists it in `metadata.external_supertypes`, and emits a `CH-005` warning telling you to search the DI/security/config wiring for the supertype. Inert markers (`Serializable`, `Cloneable`) are excluded.
465
+ **Framework/DI blind spot (CH-005).** An empty blast radius is ambiguous: genuinely unused, or invoked through an edge the static graph does not model. When the target class implements/extends an **external** framework type (e.g. Spring Security's `RedirectStrategy`, a servlet `Filter`) it is typically wired by framework DI/config and invoked polymorphically — no in-repo edge names its methods, so `direct_callers` is `0`. Rather than report that as `risk:low` at high confidence (a dangerous false negative that reads as "safe to change"), `impact-chain` detects the external supertype, drops `confidence` to `low`, lists it in `metadata.external_supertypes`, and emits a `CH-005` warning telling you to search the DI/security/config wiring for the supertype. Inert markers (`Serializable`, `Cloneable`) and concrete JDK base classes extended for reuse (`ArrayList`, `InputStream`, …) are excluded.
466
+
467
+ **External-interface DI caller recovery (CH-007).** When the target is wired through an external interface, the consumers that inject that interface never name the target, so the static caller graph misses them — but the wiring sites are still in-repo. `impact-chain` reads the dependency edges to recover the in-repo classes that inject/use the external supertype and attributes them as callers (so their endpoints map too). If exactly one in-repo class implements the interface the binding is unambiguous (`confidence:medium`); if several do, `metadata.external_iface_binding_ambiguous` is `true` and confidence stays `low`. `metadata.external_iface_callers_recovered` reports the count. Recovered callers reach the target only if it is the bean configured for that interface (which may also have framework/third-party implementations) — the warning says so. Validated on BroadleafCommerce: querying a `UserDetailsService` implementation recovers the security-config and login-service classes that wire it.
465
468
 
466
469
  **Caller precision (CH-006).** `implements`/`extends` are structural type declarations, not calls — so they are excluded from the caller graph. Querying a class that implements a high-fanout interface (e.g. a 40-implementor `CustomEndpoint` or a shared `Mapper<E,D>` base) does **not** report its sibling implementors as callers; only real `injects`/`calls` edges count. This prevents a leaf class from being inflated to a large false blast radius.
467
470
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.55.0-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.56.0-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
7
7
 
8
8
  ---
@@ -421,9 +421,12 @@ Unlike `impact` (which traces the caller graph), `impact-chain` builds on the Sp
421
421
  | `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
422
422
  | `risk_level` | `critical` \| `high` \| `medium` \| `low` |
423
423
  | `confidence` | `high` \| `medium` \| `low` — `low` on a detected blind spot, `medium` on partial resolution or capped traversal. Informational interface↔impl expansion notices do **not** lower it, so a clean resolved query stays `high`. |
424
- | `metadata.blind_spots` | `framework_di` and/or `value_type` when an empty result is unmodeled-edge driven, not real dead code |
424
+ | `metadata.blind_spots` | `framework_di` and/or `value_type` when an empty result is unmodeled-edge driven, not real dead code (CH-007 drops `framework_di` once it recovers the wiring callers) |
425
+ | `metadata.external_iface_callers_recovered` / `external_iface_binding_ambiguous` | CH-007 — count of in-repo wiring callers recovered through an external interface, and whether the impl→bean binding is ambiguous (multiple in-repo implementors) |
425
426
 
426
- **Framework/DI blind spot (CH-005).** An empty blast radius is ambiguous: genuinely unused, or invoked through an edge the static graph does not model. When the target class implements/extends an **external** framework type (e.g. Spring Security's `RedirectStrategy`, a servlet `Filter`) it is typically wired by framework DI/config and invoked polymorphically — no in-repo edge names its methods, so `direct_callers` is `0`. Rather than report that as `risk:low` at high confidence (a dangerous false negative that reads as "safe to change"), `impact-chain` detects the external supertype, drops `confidence` to `low`, lists it in `metadata.external_supertypes`, and emits a `CH-005` warning telling you to search the DI/security/config wiring for the supertype. Inert markers (`Serializable`, `Cloneable`) are excluded.
427
+ **Framework/DI blind spot (CH-005).** An empty blast radius is ambiguous: genuinely unused, or invoked through an edge the static graph does not model. When the target class implements/extends an **external** framework type (e.g. Spring Security's `RedirectStrategy`, a servlet `Filter`) it is typically wired by framework DI/config and invoked polymorphically — no in-repo edge names its methods, so `direct_callers` is `0`. Rather than report that as `risk:low` at high confidence (a dangerous false negative that reads as "safe to change"), `impact-chain` detects the external supertype, drops `confidence` to `low`, lists it in `metadata.external_supertypes`, and emits a `CH-005` warning telling you to search the DI/security/config wiring for the supertype. Inert markers (`Serializable`, `Cloneable`) and concrete JDK base classes extended for reuse (`ArrayList`, `InputStream`, …) are excluded.
428
+
429
+ **External-interface DI caller recovery (CH-007).** When the target is wired through an external interface, the consumers that inject that interface never name the target, so the static caller graph misses them — but the wiring sites are still in-repo. `impact-chain` reads the dependency edges to recover the in-repo classes that inject/use the external supertype and attributes them as callers (so their endpoints map too). If exactly one in-repo class implements the interface the binding is unambiguous (`confidence:medium`); if several do, `metadata.external_iface_binding_ambiguous` is `true` and confidence stays `low`. `metadata.external_iface_callers_recovered` reports the count. Recovered callers reach the target only if it is the bean configured for that interface (which may also have framework/third-party implementations) — the warning says so. Validated on BroadleafCommerce: querying a `UserDetailsService` implementation recovers the security-config and login-service classes that wire it.
427
430
 
428
431
  **Caller precision (CH-006).** `implements`/`extends` are structural type declarations, not calls — so they are excluded from the caller graph. Querying a class that implements a high-fanout interface (e.g. a 40-implementor `CustomEndpoint` or a shared `Mapper<E,D>` base) does **not** report its sibling implementors as callers; only real `injects`/`calls` edges count. This prevents a leaf class from being inflated to a large false blast radius.
429
432
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.55.0"
7
+ version = "1.56.0"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.55.0"
3
+ __version__ = "1.56.0"
@@ -182,6 +182,25 @@ _INERT_MARKER_SUPERTYPES = frozenset({
182
182
  "Externalizable", "java.io.Externalizable",
183
183
  })
184
184
 
185
+ # Concrete JDK base classes extended for implementation reuse, NOT framework-DI
186
+ # interfaces. `class Foo extends ArrayList` is code reuse, not polymorphic wiring,
187
+ # and consumers that merely hold an ArrayList/InputStream field are not callers of
188
+ # Foo. Validated on BroadleafCommerce: without this, CH-007 mis-recovered consumers
189
+ # of ArrayList/Stack/InputStream as callers of EmptyFilterValues/IdentityUtilContext/
190
+ # ResourceInputStream. Simple-name match (the external type is, by definition, not in-repo).
191
+ _NON_DI_SUPERTYPES = frozenset({
192
+ # java.util containers
193
+ "ArrayList", "LinkedList", "Vector", "Stack", "HashMap", "LinkedHashMap",
194
+ "TreeMap", "HashSet", "LinkedHashSet", "TreeSet", "ArrayDeque", "Properties",
195
+ "AbstractList", "AbstractMap", "AbstractSet", "AbstractCollection", "EnumMap",
196
+ # java.io / java.nio streams + readers
197
+ "InputStream", "OutputStream", "Reader", "Writer", "FilterInputStream",
198
+ "FilterOutputStream", "ByteArrayInputStream", "ByteArrayOutputStream",
199
+ # java.lang base classes
200
+ "Object", "Thread", "Throwable", "Exception", "RuntimeException", "Error",
201
+ "Number", "Enum", "ClassLoader", "ThreadLocal",
202
+ })
203
+
185
204
 
186
205
  def _external_supertypes(cir, class_fqn: str) -> list[str]:
187
206
  """Return supertypes of class_fqn that are NOT in-repo symbols.
@@ -212,6 +231,8 @@ def _external_supertypes(cir, class_fqn: str) -> list[str]:
212
231
  simple = to.rsplit(".", 1)[1] if "." in to else to
213
232
  if simple in _INERT_MARKER_SUPERTYPES or to in _INERT_MARKER_SUPERTYPES:
214
233
  continue
234
+ if simple in _NON_DI_SUPERTYPES:
235
+ continue # concrete JDK base extended for reuse, not DI dispatch
215
236
  # Internal if it resolves to exactly one in-repo class (exact or simple-name).
216
237
  if to in known:
217
238
  continue
@@ -246,6 +267,51 @@ def _is_unmodeled_value_type(cir, class_fqn: str, model) -> bool:
246
267
  return True
247
268
 
248
269
 
270
+ # CH-007 — external-interface DI bridge (caller recovery)
271
+ # ---------------------------------------------------------------------------
272
+ # When a class is reachable only through an external interface it implements
273
+ # (the CH-005 blind spot), its consumers inject/reference the *external* type,
274
+ # never the impl — so no reverse-graph edge names the impl and CH-001b (in-repo
275
+ # interface expansion) cannot bridge. The wiring sites are still in-repo, though:
276
+ # the raw dependency edges record `<consumer> -[injects|calls|instantiates|returns]->
277
+ # <external interface>`. Read those to recover the consumer classes, and count how
278
+ # many in-repo classes implement/extend each external supertype: exactly one means
279
+ # the binding is unambiguous (this impl IS the injected bean); several means any
280
+ # could be, so recover all candidates and flag the ambiguity rather than assert.
281
+ _DI_USE_EDGE_TYPES = frozenset({"injects", "calls", "instantiates", "returns"})
282
+
283
+
284
+ def _recover_external_iface_callers(
285
+ cir, external_supertypes: list[str], impl_class_fqn: str
286
+ ) -> tuple[list[str], int]:
287
+ """Recover in-repo wiring/consumer classes for an impl wired via an external
288
+ interface (CH-007). See module note above.
289
+
290
+ Returns ``(recovered_caller_classes, max_impl_count)`` where max_impl_count is
291
+ the largest number of in-repo implementors across the matched supertypes — 1
292
+ means the impl→bean binding is unambiguous, >1 means it is not.
293
+ """
294
+ deps = getattr(cir, "dependencies", None) or []
295
+ ext = set(external_supertypes)
296
+ if not ext:
297
+ return [], 0
298
+ recovered: list[str] = []
299
+ impl_count: dict[str, int] = {e: 0 for e in ext}
300
+ for edge in deps:
301
+ to = (edge.get("to") or "").strip()
302
+ if to not in ext:
303
+ continue
304
+ et = edge.get("type")
305
+ if et in ("implements", "extends"):
306
+ impl_count[to] = impl_count.get(to, 0) + 1
307
+ elif et in _DI_USE_EDGE_TYPES:
308
+ owner = normalize_owner_fqn((edge.get("from") or "").strip())
309
+ if owner and owner != impl_class_fqn:
310
+ recovered.append(owner)
311
+ max_impl = max(impl_count.values()) if impl_count else 0
312
+ return list(dict.fromkeys(recovered)), max_impl
313
+
314
+
249
315
  # ---------------------------------------------------------------------------
250
316
  # Symbol resolution
251
317
  # ---------------------------------------------------------------------------
@@ -846,7 +912,57 @@ class ImpactOrchestrator:
846
912
  if empty_blast and class_level_seed:
847
913
  external_supertypes = _external_supertypes(cir, resolved_symbol)
848
914
  framework_di_blind_spot = bool(external_supertypes)
915
+
916
+ # CH-007: external-interface DI bridge. Before treating the empty result as a
917
+ # blind spot, recover the in-repo wiring/consumer classes that inject the
918
+ # external supertype (they never name this impl, so the BFS missed them).
919
+ di_recovered_callers: list[str] = []
920
+ di_binding_ambiguous = False
849
921
  if framework_di_blind_spot:
922
+ di_recovered_callers, _max_impl = _recover_external_iface_callers(
923
+ cir, external_supertypes, resolved_symbol
924
+ )
925
+ di_binding_ambiguous = _max_impl > 1
926
+ # Keep only genuinely new caller classes (not seeds / already found).
927
+ _known = set(seed_fqns) | set(direct_callers) | set(indirect_callers)
928
+ di_recovered_callers = [c for c in di_recovered_callers if c not in _known]
929
+
930
+ di_recovered = bool(di_recovered_callers)
931
+ if di_recovered:
932
+ # Merge recovered wiring sites into the chain and recompute the views
933
+ # that depend on the caller set (endpoints, findings, surfaces, risk).
934
+ direct_callers = direct_callers + di_recovered_callers
935
+ empty_blast = False
936
+ all_callers = direct_callers + indirect_callers
937
+ endpoints_affected = _collect_endpoints(all_callers, seed_fqns, model)
938
+ impact_findings_raw = _filter_findings(
939
+ all_findings, seed_fqns, direct_callers, indirect_callers, endpoints_affected
940
+ )
941
+ impact_findings_raw.sort(
942
+ key=lambda f: (SEVERITY_ORDER.get(f.severity, 9), f.symbol)
943
+ )
944
+ impact_findings = [f.to_dict() for f in impact_findings_raw]
945
+ security_surfaces = _build_security_surfaces(endpoints_affected, impact_findings_raw)
946
+ risk_level, risk_score = _compute_risk(
947
+ len(direct_callers), len(indirect_callers),
948
+ len(endpoints_affected), impact_findings_raw,
949
+ )
950
+ _ambig = (
951
+ " Binding is ambiguous (the external type has multiple in-repo "
952
+ "implementors); the recovered caller(s) wire one of them — verify "
953
+ "which bean is configured." if di_binding_ambiguous else ""
954
+ )
955
+ warnings.append(
956
+ f"External-interface DI bridge (CH-007): recovered "
957
+ f"{len(di_recovered_callers)} in-repo wiring/consumer class(es) that "
958
+ "inject the external supertype(s) [" + ", ".join(external_supertypes)
959
+ + "] this class implements. These callers reference the interface, not "
960
+ "this class directly, so the static call-graph missed them. They reach "
961
+ "this class only if it is the bean configured for that interface — the "
962
+ "interface may also have framework/third-party implementations; verify "
963
+ "the wiring." + _ambig
964
+ )
965
+ elif framework_di_blind_spot:
850
966
  warnings.append(
851
967
  "Framework/external-interface DI blind spot (CH-005): this class "
852
968
  "implements/extends external type(s) [" + ", ".join(external_supertypes)
@@ -900,6 +1016,10 @@ class ImpactOrchestrator:
900
1016
  confidence: str
901
1017
  if resolution == "not_found":
902
1018
  confidence = "low"
1019
+ elif di_recovered:
1020
+ # Callers recovered structurally via the external-interface DI bridge.
1021
+ # Unambiguous binding (single in-repo impl) → medium; ambiguous → low.
1022
+ confidence = "low" if di_binding_ambiguous else "medium"
903
1023
  elif framework_di_blind_spot or value_type_blind_spot or unresolved_ref_blind_spot:
904
1024
  confidence = "low"
905
1025
  elif resolution == "partial" or confidence_reducing:
@@ -931,11 +1051,14 @@ class ImpactOrchestrator:
931
1051
  "model_build_time_ms": model.build_time_ms,
932
1052
  "query_time_ms": elapsed_ms,
933
1053
  "blind_spots": (
934
- (["framework_di"] if framework_di_blind_spot else [])
1054
+ # framework_di stops being a blind spot once CH-007 recovers callers
1055
+ (["framework_di"] if framework_di_blind_spot and not di_recovered else [])
935
1056
  + (["value_type"] if value_type_blind_spot else [])
936
1057
  + (["unresolved_refs"] if unresolved_ref_blind_spot else [])
937
1058
  ),
938
1059
  "external_supertypes": external_supertypes,
1060
+ "external_iface_callers_recovered": len(di_recovered_callers),
1061
+ "external_iface_binding_ambiguous": di_binding_ambiguous,
939
1062
  },
940
1063
  )
941
1064
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes