sourcecode 1.55.0__tar.gz → 1.57.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-1.55.0 → sourcecode-1.57.0}/CHANGELOG.md +44 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/PKG-INFO +11 -4
- {sourcecode-1.55.0 → sourcecode-1.57.0}/README.md +10 -3
- {sourcecode-1.55.0 → sourcecode-1.57.0}/pyproject.toml +1 -1
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/license.py +15 -2
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/spring_impact.py +124 -1
- {sourcecode-1.55.0 → sourcecode-1.57.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/.gitignore +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/.ruff.toml +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/LICENSE +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/SECURITY.md +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/raw +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/integration_detector.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.55.0 → sourcecode-1.57.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.57.0] — 2026-06-19
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **TEMPORARY: Pro unlocked for everyone (early-adoption phase).** A new
|
|
7
|
+
`_PRO_UNLOCK_ALL` switch in `license.py` (env `SOURCECODE_PRO_UNLOCK`, default on)
|
|
8
|
+
floors `is_pro` to `True` at init, so anyone who installs gets Pro from the start —
|
|
9
|
+
removing onboarding friction to maximize adoption. The gate *logic*
|
|
10
|
+
(`require_feature` / `require_repo_or_pro` / `require_pro`, size limits, upgrade
|
|
11
|
+
prompts, telemetry) is left fully intact; this only raises the entitlement floor.
|
|
12
|
+
Real Pro license activation still works. To resume the paywall: set
|
|
13
|
+
`_PRO_UNLOCK_ALL = False` (or `SOURCECODE_PRO_UNLOCK=0`) — no other change needed.
|
|
14
|
+
|
|
15
|
+
## [1.56.0] — 2026-06-19
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **CH-007 — `impact-chain` recovers callers wired through an external interface.**
|
|
19
|
+
The flagship blast-radius gap (BroadleafCommerce #3124): a class wired by DI through
|
|
20
|
+
an external framework interface (e.g. a Spring Security `UserDetailsService` /
|
|
21
|
+
`RedirectStrategy` impl) reported `0 callers` because consumers inject the *interface*,
|
|
22
|
+
never the impl — so no reverse-graph edge names it and CH-001b (in-repo interface
|
|
23
|
+
expansion) cannot bridge. CH-005 only warned; it did not recover the callers.
|
|
24
|
+
|
|
25
|
+
`_recover_external_iface_callers()` now reads the raw dependency edges (which record
|
|
26
|
+
`<consumer> -[injects|calls|instantiates|returns]-> <external interface>` even though
|
|
27
|
+
those edges never reach `reverse_graph`) and attributes the in-repo wiring/consumer
|
|
28
|
+
classes as callers, then recomputes the endpoints/findings/surfaces/risk that depend
|
|
29
|
+
on them. In-repo implementors of the interface are counted: exactly one → unambiguous
|
|
30
|
+
binding (`confidence:medium`, `framework_di` dropped from `metadata.blind_spots`);
|
|
31
|
+
several → `metadata.external_iface_binding_ambiguous:true`, confidence stays `low`
|
|
32
|
+
with an ambiguity warning. No in-repo wiring → the honest CH-005 blind-spot path is
|
|
33
|
+
unchanged. New metadata: `external_iface_callers_recovered`, `external_iface_binding_ambiguous`.
|
|
34
|
+
|
|
35
|
+
- **Precision: concrete JDK base classes excluded from DI detection.** `class Foo extends
|
|
36
|
+
ArrayList`/`Stack`/`InputStream` is implementation reuse, not DI dispatch, and a consumer
|
|
37
|
+
holding an `ArrayList` field is not a caller of `Foo`. Added `_NON_DI_SUPERTYPES`
|
|
38
|
+
(java.util containers, java.io streams, java.lang bases) to the external-supertype
|
|
39
|
+
filter — found via real-repo validation, where it cut false recoveries from 12 to 7.
|
|
40
|
+
|
|
41
|
+
Validated on BroadleafCommerce (2762 files, 22 247 symbols): CH-007 recovers wiring
|
|
42
|
+
callers for 7 classes, all genuine DI interfaces; querying a `UserDetailsService`
|
|
43
|
+
implementation recovers the security-config + login-service classes that wire it.
|
|
44
|
+
Report: `.planning/benchmark-broadleaf-3124.md`. Tests: `TestExternalInterfaceDIBridge`
|
|
45
|
+
(recovery / ambiguity / no-wiring fallback).
|
|
46
|
+
|
|
3
47
|
## [1.55.0] — 2026-06-19
|
|
4
48
|
|
|
5
49
|
### Security
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.57.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
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -295,6 +295,10 @@ Specifically:
|
|
|
295
295
|
|
|
296
296
|
## Pricing
|
|
297
297
|
|
|
298
|
+
> **🎉 Early-adoption: Pro is currently unlocked for everyone.** During this phase
|
|
299
|
+
> every install runs with full Pro entitlements — no size gate, no key required. The
|
|
300
|
+
> tiers below describe the model the paywall will return to later.
|
|
301
|
+
|
|
298
302
|
Two tiers. **Gating is by repo size and automation — never by command.** Every
|
|
299
303
|
command runs at full power on Free for small and mid-size repos. You upgrade
|
|
300
304
|
when the work gets bigger or automated.
|
|
@@ -459,9 +463,12 @@ Unlike `impact` (which traces the caller graph), `impact-chain` builds on the Sp
|
|
|
459
463
|
| `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
|
|
460
464
|
| `risk_level` | `critical` \| `high` \| `medium` \| `low` |
|
|
461
465
|
| `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 |
|
|
466
|
+
| `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) |
|
|
467
|
+
| `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) |
|
|
468
|
+
|
|
469
|
+
**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.
|
|
463
470
|
|
|
464
|
-
**
|
|
471
|
+
**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
472
|
|
|
466
473
|
**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
474
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -257,6 +257,10 @@ Specifically:
|
|
|
257
257
|
|
|
258
258
|
## Pricing
|
|
259
259
|
|
|
260
|
+
> **🎉 Early-adoption: Pro is currently unlocked for everyone.** During this phase
|
|
261
|
+
> every install runs with full Pro entitlements — no size gate, no key required. The
|
|
262
|
+
> tiers below describe the model the paywall will return to later.
|
|
263
|
+
|
|
260
264
|
Two tiers. **Gating is by repo size and automation — never by command.** Every
|
|
261
265
|
command runs at full power on Free for small and mid-size repos. You upgrade
|
|
262
266
|
when the work gets bigger or automated.
|
|
@@ -421,9 +425,12 @@ Unlike `impact` (which traces the caller graph), `impact-chain` builds on the Sp
|
|
|
421
425
|
| `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
|
|
422
426
|
| `risk_level` | `critical` \| `high` \| `medium` \| `low` |
|
|
423
427
|
| `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 |
|
|
428
|
+
| `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) |
|
|
429
|
+
| `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) |
|
|
430
|
+
|
|
431
|
+
**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.
|
|
425
432
|
|
|
426
|
-
**
|
|
433
|
+
**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
434
|
|
|
428
435
|
**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
436
|
|
|
@@ -177,6 +177,17 @@ _FEATURE_INFO: dict[str, dict[str, str]] = {
|
|
|
177
177
|
_license_data: Optional[dict] = None
|
|
178
178
|
is_pro: bool = False
|
|
179
179
|
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# TEMPORARY PRO UNLOCK (early-adoption phase)
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# Floors `is_pro` to True at init so every gate passes — anyone who installs
|
|
184
|
+
# gets Pro from the start (remove onboarding friction, maximize adoption).
|
|
185
|
+
# The gate LOGIC below (require_feature / require_repo_or_pro / require_pro) is
|
|
186
|
+
# left fully intact; this only raises the entitlement floor. To resume the
|
|
187
|
+
# paywall: set _PRO_UNLOCK_ALL = False (or SOURCECODE_PRO_UNLOCK=0), no other
|
|
188
|
+
# change needed. Tests of the gated paths set SOURCECODE_PRO_UNLOCK=0.
|
|
189
|
+
_PRO_UNLOCK_ALL = os.environ.get("SOURCECODE_PRO_UNLOCK", "1") != "0"
|
|
190
|
+
|
|
180
191
|
|
|
181
192
|
def _secure_dir() -> None:
|
|
182
193
|
"""Create ~/.sourcecode owner-only (0700). Holds the license secret.
|
|
@@ -323,7 +334,7 @@ def _maybe_revalidate() -> None:
|
|
|
323
334
|
|
|
324
335
|
if not result.get("valid"):
|
|
325
336
|
_license_data = None
|
|
326
|
-
is_pro =
|
|
337
|
+
is_pro = _PRO_UNLOCK_ALL # TEMPORARY: keep Pro floor during unlock
|
|
327
338
|
try:
|
|
328
339
|
if _LICENSE_FILE.exists():
|
|
329
340
|
_LICENSE_FILE.unlink()
|
|
@@ -334,7 +345,7 @@ def _maybe_revalidate() -> None:
|
|
|
334
345
|
_license_data["plan"] = result.get("plan", "pro")
|
|
335
346
|
_license_data["features"] = result.get("features", [])
|
|
336
347
|
_license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
|
|
337
|
-
is_pro = _license_data.get("plan") == "pro"
|
|
348
|
+
is_pro = _PRO_UNLOCK_ALL or _license_data.get("plan") == "pro"
|
|
338
349
|
try:
|
|
339
350
|
_write_license_file(_license_data)
|
|
340
351
|
except Exception:
|
|
@@ -349,6 +360,8 @@ def _init() -> None:
|
|
|
349
360
|
and _license_data.get("plan") == "pro"
|
|
350
361
|
and _license_data.get("status", "active") != "inactive"
|
|
351
362
|
)
|
|
363
|
+
if _PRO_UNLOCK_ALL:
|
|
364
|
+
is_pro = True # TEMPORARY: early-adoption Pro unlock (see _PRO_UNLOCK_ALL)
|
|
352
365
|
|
|
353
366
|
|
|
354
367
|
_init()
|
|
@@ -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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|