sourcecode 1.70.0__tar.gz → 1.72.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.70.0 → sourcecode-1.72.0}/CHANGELOG.md +125 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/PKG-INFO +16 -5
- {sourcecode-1.70.0 → sourcecode-1.72.0}/README.md +15 -4
- {sourcecode-1.70.0 → sourcecode-1.72.0}/pyproject.toml +1 -1
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/architecture_summary.py +27 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/canonical_ir.py +14 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/cli.py +77 -13
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/java.py +40 -2
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/explain.py +7 -2
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/integration_detector.py +57 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/migrate_check.py +170 -12
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/repository_ir.py +343 -10
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/spring_security_audit.py +11 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/spring_tx_analyzer.py +70 -9
- {sourcecode-1.70.0 → sourcecode-1.72.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/.gitignore +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/.ruff.toml +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/LICENSE +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/SECURITY.md +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/raw +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.70.0 → sourcecode-1.72.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,130 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.72.0] — 2026-07-01
|
|
4
|
+
|
|
5
|
+
### Fixed — correctness on a large real-world Spring Boot 3 monolith (Broadleaf Commerce CE field test)
|
|
6
|
+
|
|
7
|
+
A downstream architecture audit of **BroadleafCommerce** (`97705634ac`, branch
|
|
8
|
+
`develop-7.0.x`; ~3,000 Java classes, 13 Maven poms, Spring Boot 3.5 / Spring 6.2 /
|
|
9
|
+
Hibernate 5.6) hand-verified against source surfaced seven reproducible
|
|
10
|
+
correctness defects across `modernize`, `spring-audit`, `migrate-check`, and
|
|
11
|
+
`export --integrations`. Each fix targets the root cause and adds regression
|
|
12
|
+
coverage; all were re-verified against the live repo.
|
|
13
|
+
|
|
14
|
+
- **`spring-audit` TX-005 — 100% false-positive rate (4/4) on expected-exception idioms.**
|
|
15
|
+
The "swallowed exception in `@Transactional`" rule flagged the standard JPA
|
|
16
|
+
control-flow idiom (`Query.getSingleResult()` → `catch (NoResultException)` →
|
|
17
|
+
insert; tolerant insert → `catch (EntityExistsException)`) and catches carrying an
|
|
18
|
+
explicit `// do nothing — acceptable condition` intent comment. TX-005 now skips
|
|
19
|
+
catches whose caught type is a well-known expected-control-flow exception
|
|
20
|
+
(`NoResultException`, `NonUniqueResultException`, `EntityExistsException`,
|
|
21
|
+
`EmptyResultDataAccessException`, …) and catches whose body documents a deliberate
|
|
22
|
+
no-op, while still flagging a genuine unexpected-exception swallow.
|
|
23
|
+
- **`modernize` `statically_unreferenced` — ~95% false positives on annotation-constant nested classes.**
|
|
24
|
+
Two compounding defects: (a) the pre-scan fast path emitted nested types as
|
|
25
|
+
`pkg.Inner`, dropping the enclosing class, so distinct nested types with the same
|
|
26
|
+
simple name (Broadleaf has 25 `GroupName`s) collapsed onto one colliding FQN;
|
|
27
|
+
(b) static-constant reads inside annotation arguments
|
|
28
|
+
(`@AdminPresentation(group = GroupName.General)`, incl. the fully-qualified
|
|
29
|
+
`Outer.Nested.CONST` form and class-level annotations) were never counted as
|
|
30
|
+
references, so the constant holders read as zero-caller dead code. Now: nested
|
|
31
|
+
symbols are binary-qualified (`Outer.Inner`); a new `references` edge pass resolves
|
|
32
|
+
annotation-value reads (bare, inherited-nested via supertypes, and qualified
|
|
33
|
+
chains) in both parsed and pre-scan-skipped files. Presentation-holder false
|
|
34
|
+
positives on Broadleaf went 11→0 (the survivors are genuinely-unused declarations).
|
|
35
|
+
- **`modernize` `cross_module_tangles` — re-emitted the subsystem list instead of coupling.**
|
|
36
|
+
The field echoed `{label, class_count, member_count}` per subsystem with no edge
|
|
37
|
+
data. It now measures real directed inter-subsystem coupling from structural graph
|
|
38
|
+
edges, with `edge_count`, `reverse_edge_count`, and `mutual` (bidirectional/cyclic
|
|
39
|
+
= the actual tangle) — e.g. Broadleaf `profile↔common` is now correctly flagged
|
|
40
|
+
mutual.
|
|
41
|
+
- **`migrate-check` MIG-031 — test-scope config counted as a production blocker + wrong explanation.**
|
|
42
|
+
A test-only `bl-applicationContext-test-security.xml` (under `src/main/resources`,
|
|
43
|
+
loaded only by test `@ContextConfiguration`) inflated `blocking_count`; XML config
|
|
44
|
+
with a `test` filename marker is now classified `code_context: "test"`
|
|
45
|
+
(blocking_count 1→0). The explanation was a static OR-template asserting both
|
|
46
|
+
triggers; it now reports only the sub-pattern that actually matched (auto-config
|
|
47
|
+
vs versioned legacy schema).
|
|
48
|
+
- **`export --integrations` — missed in-code Spring Security LDAP (false negative).**
|
|
49
|
+
Subclassing `LdapUserDetailsMapper` and using `DirContextOperations` — the
|
|
50
|
+
idiomatic Spring Security LDAP auth mapper — was undetected (Broadleaf reported 0
|
|
51
|
+
LDAP). Added `LdapUserDetailsMapper` / `DirContextOperations` / context-source
|
|
52
|
+
tokens; Broadleaf now reports 5 LDAP integration points.
|
|
53
|
+
- **`explain` vs `modernize` in-degree — silently contradictory numbers.**
|
|
54
|
+
The same class showed different fan-in in the two commands with no explanation.
|
|
55
|
+
Both metrics are now self-describing: `modernize.high_coupling_nodes` documents
|
|
56
|
+
`in_degree` as the raw all-edge-type incoming count; `explain` labels its value
|
|
57
|
+
as "N distinct caller classes".
|
|
58
|
+
- **`spring-audit` vs `endpoints` — unreconcilable endpoint counts.**
|
|
59
|
+
The canonical IR (feeding `spring-audit`, `impact`, `validation`) did not apply the
|
|
60
|
+
`endpoints` command's filter for framework dynamic-admin FQN-shaped paths, so its
|
|
61
|
+
count was silently inflated. The filter is now shared; `endpoints_analyzed` is
|
|
62
|
+
documented as the deduplicated canonical count (≤ `endpoints` total, difference =
|
|
63
|
+
multi-prefix collapse), reconciling 225 vs 220 on Broadleaf.
|
|
64
|
+
|
|
65
|
+
## [1.71.0] — 2026-07-01
|
|
66
|
+
|
|
67
|
+
### Fixed — framework-framing & migration reliability on a large non-Boot Spring repo (Jenkins core field test)
|
|
68
|
+
|
|
69
|
+
A migration/architecture audit of **jenkins-ci/jenkins** (`e7eef8d2cf`, branch
|
|
70
|
+
`master`; ~1,900 production Java classes, Maven multi-module, uses
|
|
71
|
+
`spring-security-web` but **no Spring MVC, no Spring Boot, no DI container** —
|
|
72
|
+
Jenkins runs on its own Stapler web framework and ACL/SecurityRealm security SPI)
|
|
73
|
+
surfaced five reproducible defects. In every case the engine already computed the
|
|
74
|
+
*correct* internal signal (downgraded confidence, `spring_present`, `not_detected`,
|
|
75
|
+
a prose `coverage_note`) but that signal did not propagate to the high-level field
|
|
76
|
+
an automated consumer reads first. Each fix targets the root cause and adds
|
|
77
|
+
regression coverage. All findings were verified against direct `grep` over the same
|
|
78
|
+
repo.
|
|
79
|
+
|
|
80
|
+
- **Framework detection — phantom "Spring MVC / Spring AOP" from pom exclusion blocks (BUG #1).**
|
|
81
|
+
`_frameworks_from_pom` serialized the *entire* pom to text and substring-matched
|
|
82
|
+
framework tokens, so `<exclude>org.springframework:spring-web</exclude>` and
|
|
83
|
+
`<exclude>…:spring-aop</exclude>` inside an enforcer bytecode rule in
|
|
84
|
+
`war/pom.xml` (artifact **exclusions**, not dependencies) read as "uses Spring
|
|
85
|
+
MVC / Spring AOP". Detection is now restricted to real dependency / plugin /
|
|
86
|
+
parent **coordinates** (`<exclusion>`/`<exclude>` and comments carry none) — the
|
|
87
|
+
same discipline the Gradle path already applied (JobRunr fix). The Spring Security
|
|
88
|
+
token was also broadened to the true-positive `spring-security-web` /
|
|
89
|
+
`-config` coordinates (Jenkins' real dep), so the honest label survives while the
|
|
90
|
+
phantoms disappear.
|
|
91
|
+
- **`architecture_summary` — categorical "MVC pattern with … view layers" on a repo with zero controllers (BUG #1).**
|
|
92
|
+
The `mvc` pattern is a directory-name heuristic. On a Java/Kotlin repo it is now
|
|
93
|
+
cross-checked against the canonical endpoint extractor in the same run; with no
|
|
94
|
+
HTTP controllers detected it degrades to a directory-derived "Layered code
|
|
95
|
+
organization (… — no HTTP controllers detected)" note, dropping the unverified
|
|
96
|
+
"view" claim rather than asserting MVC.
|
|
97
|
+
- **`migrate-check` — `boot3` dimension applicable without any Spring Boot (BUG #2).**
|
|
98
|
+
The Boot 2→3 axis was gated on `spring_present` (any `org.springframework.*`
|
|
99
|
+
coordinate), so a repo carrying only `spring-security-web` received a
|
|
100
|
+
contaminated `readiness_score` from a migration that does not exist. Applicability
|
|
101
|
+
now requires **positive evidence**: a `spring-boot*` coordinate / Gradle plugin
|
|
102
|
+
(`@SpringBootApplication` always ships one), **or** a concrete `spring_boot_3` /
|
|
103
|
+
`spring_security_6` finding in main code. jakarta findings alone no longer enable
|
|
104
|
+
boot3. A new `spring_boot_present` flag is surfaced; when N/A the dimension is
|
|
105
|
+
excluded from the aggregate with a clear reason.
|
|
106
|
+
- **`migrate-check` — `MIG-002` (javax.servlet) counted `@Deprecated` frozen-legacy shims as high blockers (BUG #3).**
|
|
107
|
+
A namespace-migration finding on a **class-level `@Deprecated`** type (kept for
|
|
108
|
+
binary compat, with a live replacement — Jenkins `ChainedServletFilter` →
|
|
109
|
+
`ChainedServletFilter2`) is now classified `code_context="deprecated_shim"` and
|
|
110
|
+
excluded from `blocking_count`, readiness, and effort, alongside the existing
|
|
111
|
+
test/generated buckets. A `@Deprecated` **method** on a live class stays blocking.
|
|
112
|
+
On Jenkins this dropped `blocking_count` from 6 to 2.
|
|
113
|
+
- **`repo-ir` — `security_model` was a bare `"unknown"` string with no reason (BUG #4).**
|
|
114
|
+
A structured `security_model_detail` companion now mirrors the `not_detected`
|
|
115
|
+
pattern used elsewhere: `status`, `spring_security_filter_chain_detected`,
|
|
116
|
+
`annotation_policies_detected`, `custom_spi_detected`, and a `reason` that
|
|
117
|
+
explicitly notes absence of a *recognized* pattern is not evidence the repo is
|
|
118
|
+
unsecured (Jenkins uses a custom ACL/SecurityRealm SPI).
|
|
119
|
+
- **`export --integrations` — low count carried no structured coverage flag (BUG #5).**
|
|
120
|
+
The honest coverage caveat lived only in the prose `coverage_note`. A structured
|
|
121
|
+
`coverage_confidence` (`low` / `partial` / `high`) plus `coverage_confidence_reason`
|
|
122
|
+
is now emitted: a large repo (≥300 source files) with <10 recognized constructs is
|
|
123
|
+
flagged `low` so a consumer cannot read a low count as low external coupling
|
|
124
|
+
(Jenkins' custom remoting/SCM/Update-Center SPIs are invisible to static matching).
|
|
125
|
+
|
|
126
|
+
New regression coverage across five suites; full suite green (2,900+ tests).
|
|
127
|
+
|
|
3
128
|
## [1.70.0] — 2026-07-01
|
|
4
129
|
|
|
5
130
|
### Fixed — reliability & internal-consistency on a large Spring 5 / Hibernate 5 repo (openmrs-core field test)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.72.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
|
|
@@ -411,7 +411,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
|
|
|
411
411
|
|------|--------|
|
|
412
412
|
| `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
|
|
413
413
|
| `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
|
|
414
|
-
| `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`)
|
|
414
|
+
| `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`), a structured `coverage_confidence` (`low` / `partial` / `high`) with `coverage_confidence_reason`, and a prose `coverage_note`: a count of `0` — or a low count on a large repo (≥300 files, <10 constructs) — is flagged `low`, so a low count is never read as low external coupling (custom protocol/SPI clients are not statically visible). |
|
|
415
415
|
| `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
|
|
416
416
|
|
|
417
417
|
The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
|
|
@@ -734,9 +734,20 @@ evidence. `migrate-check` enforces:
|
|
|
734
734
|
`hibernate_rewrite` headline. An **unresolved** version degrades to a
|
|
735
735
|
low-confidence hypothesis with no headline blocker. See
|
|
736
736
|
`hibernate.effective_version` / `version_major` / `version_confidence`.
|
|
737
|
-
- **Boot3
|
|
738
|
-
|
|
739
|
-
|
|
737
|
+
- **Boot3 needs Spring _Boot_, not just Spring.** `boot3.applicable` requires
|
|
738
|
+
positive Spring Boot evidence — a `spring-boot*` coordinate / Gradle plugin (or
|
|
739
|
+
`@SpringBootApplication`), **or** a concrete `spring_boot_3` / `spring_security_6`
|
|
740
|
+
finding. A repo that merely carries a Spring library (e.g. `spring-security-web`
|
|
741
|
+
with no Boot — Jenkins core) reports `boot3` as N/A with a reason and is excluded
|
|
742
|
+
from the aggregate; `spring_present: true` but `spring_boot_present: false`. Quarkus
|
|
743
|
+
/ Micronaut / Helidon / Jakarta-pure repos likewise report `boot3` N/A. jakarta
|
|
744
|
+
findings alone never enable boot3.
|
|
745
|
+
- **Frozen-legacy `@Deprecated` shims aren't blockers.** A `javax→jakarta` finding
|
|
746
|
+
on a **class-level `@Deprecated`** type (kept for binary compat, replaced
|
|
747
|
+
elsewhere) is classified `code_context: "deprecated_shim"` and excluded from
|
|
748
|
+
`blocking_count` / readiness / effort — surfaced under `non_blocking` with a
|
|
749
|
+
"verify before removing" note. A `@Deprecated` **method** on a live class still
|
|
750
|
+
blocks.
|
|
740
751
|
- **Permanent `javax.*` are never jakarta debt.** JDK/JSR namespaces
|
|
741
752
|
(`javax.cache`, `javax.sql`, `javax.xml` JAXP, `javax.crypto`, `javax.naming`,
|
|
742
753
|
`javax.management`, `javax.security.auth`, `javax.annotation.processing`, …) are
|
|
@@ -373,7 +373,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
|
|
|
373
373
|
|------|--------|
|
|
374
374
|
| `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
|
|
375
375
|
| `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
|
|
376
|
-
| `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`)
|
|
376
|
+
| `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`), a structured `coverage_confidence` (`low` / `partial` / `high`) with `coverage_confidence_reason`, and a prose `coverage_note`: a count of `0` — or a low count on a large repo (≥300 files, <10 constructs) — is flagged `low`, so a low count is never read as low external coupling (custom protocol/SPI clients are not statically visible). |
|
|
377
377
|
| `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
|
|
378
378
|
|
|
379
379
|
The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
|
|
@@ -696,9 +696,20 @@ evidence. `migrate-check` enforces:
|
|
|
696
696
|
`hibernate_rewrite` headline. An **unresolved** version degrades to a
|
|
697
697
|
low-confidence hypothesis with no headline blocker. See
|
|
698
698
|
`hibernate.effective_version` / `version_major` / `version_confidence`.
|
|
699
|
-
- **Boot3
|
|
700
|
-
|
|
701
|
-
|
|
699
|
+
- **Boot3 needs Spring _Boot_, not just Spring.** `boot3.applicable` requires
|
|
700
|
+
positive Spring Boot evidence — a `spring-boot*` coordinate / Gradle plugin (or
|
|
701
|
+
`@SpringBootApplication`), **or** a concrete `spring_boot_3` / `spring_security_6`
|
|
702
|
+
finding. A repo that merely carries a Spring library (e.g. `spring-security-web`
|
|
703
|
+
with no Boot — Jenkins core) reports `boot3` as N/A with a reason and is excluded
|
|
704
|
+
from the aggregate; `spring_present: true` but `spring_boot_present: false`. Quarkus
|
|
705
|
+
/ Micronaut / Helidon / Jakarta-pure repos likewise report `boot3` N/A. jakarta
|
|
706
|
+
findings alone never enable boot3.
|
|
707
|
+
- **Frozen-legacy `@Deprecated` shims aren't blockers.** A `javax→jakarta` finding
|
|
708
|
+
on a **class-level `@Deprecated`** type (kept for binary compat, replaced
|
|
709
|
+
elsewhere) is classified `code_context: "deprecated_shim"` and excluded from
|
|
710
|
+
`blocking_count` / readiness / effort — surfaced under `non_blocking` with a
|
|
711
|
+
"verify before removing" note. A `@Deprecated` **method** on a live class still
|
|
712
|
+
blocks.
|
|
702
713
|
- **Permanent `javax.*` are never jakarta debt.** JDK/JSR namespaces
|
|
703
714
|
(`javax.cache`, `javax.sql`, `javax.xml` JAXP, `javax.crypto`, `javax.naming`,
|
|
704
715
|
`javax.management`, `javax.security.auth`, `javax.annotation.processing`, …) are
|
|
@@ -142,6 +142,7 @@ class ArchitectureSummarizer:
|
|
|
142
142
|
# Line 2: architecture pattern + layers
|
|
143
143
|
if arch and arch.pattern not in (None, "unknown", "flat"):
|
|
144
144
|
arch_line = self._describe_arch_pattern(arch)
|
|
145
|
+
arch_line = self._qualify_web_pattern(arch, arch_line, sm)
|
|
145
146
|
if arch_line:
|
|
146
147
|
lines.append(arch_line)
|
|
147
148
|
|
|
@@ -227,6 +228,32 @@ class ArchitectureSummarizer:
|
|
|
227
228
|
self._endpoint_support_cache = (total, high)
|
|
228
229
|
return self._endpoint_support_cache
|
|
229
230
|
|
|
231
|
+
def _qualify_web_pattern(self, arch: Any, arch_line: str, sm: SourceMap) -> str:
|
|
232
|
+
"""BUG #1 (Jenkins field test): the "mvc" pattern is a directory-name
|
|
233
|
+
heuristic (dirs matching controller/model/view keywords). On a Java/Kotlin
|
|
234
|
+
repo, do not assert "MVC pattern with ... view layers" in prose when the
|
|
235
|
+
canonical endpoint extractor finds no HTTP controllers in the same run —
|
|
236
|
+
Jenkins (Stapler web framework) matches model/view-ish dirs but declares
|
|
237
|
+
zero Spring MVC controllers. Cross-check against `endpoints` and degrade to
|
|
238
|
+
a consistent, non-committal phrasing instead of a categorical MVC claim."""
|
|
239
|
+
if not arch_line or arch.pattern not in ("mvc", "spring_mvc_layered"):
|
|
240
|
+
return arch_line
|
|
241
|
+
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
242
|
+
if primary is None or primary.stack not in {"java", "kotlin"}:
|
|
243
|
+
return arch_line
|
|
244
|
+
_total, high = self._endpoint_support()
|
|
245
|
+
if high >= _MIN_REST_ENDPOINTS_FOR_LABEL:
|
|
246
|
+
return arch_line
|
|
247
|
+
# No controller backing → the MVC/view claim is unverified. Report the
|
|
248
|
+
# directory-derived layers without the framework label or the "view" claim.
|
|
249
|
+
layer_names = [l.name for l in arch.layers[:4] if l.name != "view"] if arch.layers else []
|
|
250
|
+
if layer_names:
|
|
251
|
+
return (
|
|
252
|
+
f"Layered code organization ({', '.join(layer_names)}; "
|
|
253
|
+
f"directory-based — no HTTP controllers detected)."
|
|
254
|
+
)
|
|
255
|
+
return ""
|
|
256
|
+
|
|
230
257
|
def _describe_arch_pattern(self, arch: Any) -> str:
|
|
231
258
|
pattern_labels = {
|
|
232
259
|
"clean": "Clean Architecture",
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import hashlib
|
|
18
18
|
import json
|
|
19
|
+
import re
|
|
19
20
|
from dataclasses import dataclass, field
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import Any, Optional
|
|
@@ -37,6 +38,11 @@ IR_SCHEMA_VERSION = "1.0.0"
|
|
|
37
38
|
# Edge types excluded from reverse graph (mirrors repository_ir._REVERSE_EXCLUDE)
|
|
38
39
|
_REVERSE_EXCLUDE: frozenset[str] = frozenset({"annotated_with", "mapped_to"})
|
|
39
40
|
|
|
41
|
+
# A route path that looks like a Java FQN is framework dynamic-admin routing, not a
|
|
42
|
+
# real REST endpoint (mirrors the filter in repository_ir.extract_java_endpoints so
|
|
43
|
+
# the CIR endpoint count reconciles with the `endpoints` command). See BUG #7.
|
|
44
|
+
_FQN_PATH_RE: re.Pattern = re.compile(r"/(org|com|net|io|edu)\.[a-z][a-z0-9]*\.[a-zA-Z]")
|
|
45
|
+
|
|
40
46
|
|
|
41
47
|
# ---------------------------------------------------------------------------
|
|
42
48
|
# CanonicalSecurity
|
|
@@ -324,9 +330,17 @@ def ir_dict_to_canonical(
|
|
|
324
330
|
# Build canonical endpoints from route_surface — stable ordering
|
|
325
331
|
route_surface = ir.get("route_surface") or []
|
|
326
332
|
# Deduplicate by endpoint id (route_surface can have duplicates from multi-prefix)
|
|
333
|
+
# BUG #7 (Broadleaf field test): the `endpoints` command (extract_java_endpoints)
|
|
334
|
+
# drops routes whose path looks like a Java FQN — framework dynamic-admin routing
|
|
335
|
+
# (Broadleaf @AdminSection registers entity class FQNs as URL segments) is not a
|
|
336
|
+
# real REST surface. The CIR endpoint list feeds `spring-audit` (endpoints_analyzed),
|
|
337
|
+
# `impact` and `validation`; it must apply the SAME filter so its count reconciles
|
|
338
|
+
# with `endpoints` instead of being silently inflated by those pseudo-routes.
|
|
327
339
|
_seen_ids: set[str] = set()
|
|
328
340
|
raw_endpoints: list[CanonicalEndpoint] = []
|
|
329
341
|
for r in route_surface:
|
|
342
|
+
if _FQN_PATH_RE.search(r.get("path", "") or ""):
|
|
343
|
+
continue
|
|
330
344
|
ep = _route_to_canonical_endpoint(r)
|
|
331
345
|
if ep.id not in _seen_ids:
|
|
332
346
|
_seen_ids.add(ep.id)
|
|
@@ -5764,11 +5764,63 @@ def modernize_cmd(
|
|
|
5764
5764
|
key=lambda h: (-h["hotspot_score"], h["fqn"]),
|
|
5765
5765
|
)[:15]
|
|
5766
5766
|
|
|
5767
|
-
# Cross-module tangles:
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
)
|
|
5767
|
+
# Cross-module tangles: actual inter-subsystem coupling measured from the
|
|
5768
|
+
# dependency graph, NOT a re-labelled subsystem list. For every structural
|
|
5769
|
+
# edge whose endpoints live in two different subsystems we tally a directed
|
|
5770
|
+
# (from_subsystem → to_subsystem) coupling count; pairs coupled in BOTH
|
|
5771
|
+
# directions are the real tangles (bidirectional/cyclic module coupling).
|
|
5772
|
+
_graph_edges: list = (ir.get("graph") or {}).get("edges") or []
|
|
5773
|
+
# Structural coupling edge types. annotated_with / mapped_to / event edges are
|
|
5774
|
+
# excluded (metadata + separate topology, not module-decomposition coupling).
|
|
5775
|
+
_TANGLE_EDGE_TYPES = frozenset({
|
|
5776
|
+
"imports", "injects", "extends", "implements",
|
|
5777
|
+
"references", "calls", "instantiates", "returns",
|
|
5778
|
+
})
|
|
5779
|
+
_fqn_to_pkg: dict[str, str] = {}
|
|
5780
|
+
_pkg_to_label: dict[str, str] = {}
|
|
5781
|
+
for _s in subsystems:
|
|
5782
|
+
_pp = _s.get("package_prefix") or _s.get("pkg") or ""
|
|
5783
|
+
if not _pp:
|
|
5784
|
+
continue
|
|
5785
|
+
_pkg_to_label[_pp] = _s.get("label") or _s.get("name") or _pp
|
|
5786
|
+
for _m in (_s.get("members") or []):
|
|
5787
|
+
_fqn_to_pkg[_m] = _pp
|
|
5788
|
+
|
|
5789
|
+
from collections import Counter as _Counter
|
|
5790
|
+
_pair_counts: "_Counter[tuple[str, str]]" = _Counter()
|
|
5791
|
+
_pair_example: dict[tuple[str, str], str] = {}
|
|
5792
|
+
for _e in _graph_edges:
|
|
5793
|
+
if _e.get("type") not in _TANGLE_EDGE_TYPES:
|
|
5794
|
+
continue
|
|
5795
|
+
_a = _fqn_to_pkg.get(_e.get("from"))
|
|
5796
|
+
_b = _fqn_to_pkg.get(_e.get("to"))
|
|
5797
|
+
if not _a or not _b or _a == _b:
|
|
5798
|
+
continue
|
|
5799
|
+
_pair_counts[(_a, _b)] += 1
|
|
5800
|
+
_pair_example.setdefault((_a, _b), f"{_e.get('from')} → {_e.get('to')}")
|
|
5801
|
+
|
|
5802
|
+
_cross_module_tangles = []
|
|
5803
|
+
for (_a, _b), _cnt in sorted(
|
|
5804
|
+
_pair_counts.items(), key=lambda kv: (-kv[1], kv[0])
|
|
5805
|
+
):
|
|
5806
|
+
_reverse = _pair_counts.get((_b, _a), 0)
|
|
5807
|
+
_cross_module_tangles.append({
|
|
5808
|
+
"from_subsystem": _pkg_to_label.get(_a, _a),
|
|
5809
|
+
"to_subsystem": _pkg_to_label.get(_b, _b),
|
|
5810
|
+
"from_package": _a,
|
|
5811
|
+
"to_package": _b,
|
|
5812
|
+
"edge_count": _cnt,
|
|
5813
|
+
"reverse_edge_count": _reverse,
|
|
5814
|
+
# A mutual (cyclic) dependency is the actual "tangle" — both modules
|
|
5815
|
+
# reference each other, so neither can be extracted independently.
|
|
5816
|
+
"mutual": _reverse > 0,
|
|
5817
|
+
"example": _pair_example.get((_a, _b), ""),
|
|
5818
|
+
})
|
|
5819
|
+
# Surface mutual tangles first (highest decomposition risk), then by strength.
|
|
5820
|
+
_cross_module_tangles.sort(
|
|
5821
|
+
key=lambda t: (not t["mutual"], -t["edge_count"], t["from_package"])
|
|
5822
|
+
)
|
|
5823
|
+
_cross_module_tangles = _cross_module_tangles[:15]
|
|
5772
5824
|
|
|
5773
5825
|
_summary = {
|
|
5774
5826
|
"total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
|
|
@@ -5837,6 +5889,17 @@ def modernize_cmd(
|
|
|
5837
5889
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
5838
5890
|
for n in coupling_nodes
|
|
5839
5891
|
],
|
|
5892
|
+
# BUG #6 (Broadleaf field test): make the in_degree metric self-describing
|
|
5893
|
+
# so it is not confused with `explain`'s caller count. They differ by design.
|
|
5894
|
+
"high_coupling_nodes_note": (
|
|
5895
|
+
"in_degree = raw count of incoming graph edges across ALL edge types "
|
|
5896
|
+
"(imports, injects, extends/implements, references, annotations), "
|
|
5897
|
+
"counted at symbol level. This is deliberately larger than and NOT "
|
|
5898
|
+
"equal to `sourcecode explain`'s caller count, which reports DISTINCT "
|
|
5899
|
+
"dependent classes (DI dependents + reverse-call-graph callers, "
|
|
5900
|
+
"deduplicated to class level). Use in_degree for blast-radius ranking, "
|
|
5901
|
+
"explain's caller list for the concrete dependents to inspect."
|
|
5902
|
+
),
|
|
5840
5903
|
"statically_unreferenced": [
|
|
5841
5904
|
{"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
|
|
5842
5905
|
for n in dead_zones
|
|
@@ -5853,14 +5916,15 @@ def modernize_cmd(
|
|
|
5853
5916
|
],
|
|
5854
5917
|
"subsystem_summary": _subsystem_summary,
|
|
5855
5918
|
"subsystem_summary_note": _subsystem_summary_note,
|
|
5856
|
-
"cross_module_tangles":
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5919
|
+
"cross_module_tangles": _cross_module_tangles,
|
|
5920
|
+
"cross_module_tangles_note": (
|
|
5921
|
+
"Directed inter-subsystem coupling measured from structural graph "
|
|
5922
|
+
"edges (imports/injects/extends/implements/references/calls). "
|
|
5923
|
+
"edge_count = edges from_package→to_package; mutual=true marks a "
|
|
5924
|
+
"bidirectional (cyclic) dependency — the actual tangle that blocks "
|
|
5925
|
+
"independent module extraction. Empty means no cross-subsystem "
|
|
5926
|
+
"structural coupling was detected."
|
|
5927
|
+
),
|
|
5864
5928
|
# BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
|
|
5865
5929
|
"recommendation": (
|
|
5866
5930
|
(
|
|
@@ -284,10 +284,43 @@ class JavaDetector(AbstractDetector):
|
|
|
284
284
|
if parent_artifact == "spring-boot-starter-parent":
|
|
285
285
|
sb_version = (parent_elem.findtext(f"{ns}version") or "").strip() or None
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
# BUG #1 (Jenkins field test): framework tokens must be matched only inside
|
|
288
|
+
# genuine dependency / plugin / parent coordinates, never anywhere in the
|
|
289
|
+
# serialized pom. Jenkins' war/pom.xml lists `<exclude>...:spring-web</exclude>`
|
|
290
|
+
# and `<exclude>...:spring-aop</exclude>` in an enforcer-plugin bytecode rule
|
|
291
|
+
# — those are exclusions of transitive artifacts, not declared dependencies.
|
|
292
|
+
# Serializing the whole tree let them read as "uses Spring MVC / Spring AOP".
|
|
293
|
+
# Restrict the scan to real dependency/plugin/parent artifact coordinates;
|
|
294
|
+
# `<exclusion>` and `<exclude>` elements carry neither.
|
|
295
|
+
text = self._pom_coordinate_text(root_elem, ns)
|
|
288
296
|
frameworks = self._detect_jvm_frameworks(text, "pom.xml", sb_version=sb_version)
|
|
289
297
|
return frameworks
|
|
290
298
|
|
|
299
|
+
def _pom_coordinate_text(self, root_elem: ElementTree.Element, ns: str) -> str:
|
|
300
|
+
"""Collect groupId:artifactId coordinates from real dependency, plugin, and
|
|
301
|
+
parent declarations only. Skips `<exclusion>`/`<exclude>` (transitive-artifact
|
|
302
|
+
removals) and comments, which must never be read as usage signal."""
|
|
303
|
+
coords: list[str] = []
|
|
304
|
+
|
|
305
|
+
def _coord(elem: ElementTree.Element) -> None:
|
|
306
|
+
gid = (elem.findtext(f"{ns}groupId") or "").strip()
|
|
307
|
+
aid = (elem.findtext(f"{ns}artifactId") or "").strip()
|
|
308
|
+
if aid:
|
|
309
|
+
coords.append(f"{gid}:{aid}" if gid else aid)
|
|
310
|
+
|
|
311
|
+
# <dependency> nodes anywhere (dependencies, dependencyManagement, profiles).
|
|
312
|
+
# ElementTree never yields <dependency> under <exclusions> (those are
|
|
313
|
+
# <exclusion>), so exclusion coordinates are structurally excluded.
|
|
314
|
+
for dep in root_elem.iter(f"{ns}dependency"):
|
|
315
|
+
_coord(dep)
|
|
316
|
+
for plugin in root_elem.iter(f"{ns}plugin"):
|
|
317
|
+
_coord(plugin)
|
|
318
|
+
parent_elem = root_elem.find(f"{ns}parent")
|
|
319
|
+
if parent_elem is not None:
|
|
320
|
+
_coord(parent_elem)
|
|
321
|
+
|
|
322
|
+
return "\n".join(coords).lower()
|
|
323
|
+
|
|
291
324
|
def _frameworks_from_gradle(self, path: Path) -> list[FrameworkDetection]:
|
|
292
325
|
original = "\n".join(read_text_lines(path))
|
|
293
326
|
content = original.lower()
|
|
@@ -361,7 +394,12 @@ class JavaDetector(AbstractDetector):
|
|
|
361
394
|
frameworks.append(FrameworkDetection(name="Thymeleaf", source=source))
|
|
362
395
|
if "freemarker" in text:
|
|
363
396
|
frameworks.append(FrameworkDetection(name="FreeMarker", source=source))
|
|
364
|
-
if
|
|
397
|
+
if (
|
|
398
|
+
"spring-boot-starter-security" in text
|
|
399
|
+
or "spring-security-core" in text
|
|
400
|
+
or "spring-security-web" in text
|
|
401
|
+
or "spring-security-config" in text
|
|
402
|
+
):
|
|
365
403
|
frameworks.append(FrameworkDetection(name="Spring Security", source=source))
|
|
366
404
|
if "spring-boot-starter-data-jpa" in text or "spring-data-jpa" in text:
|
|
367
405
|
frameworks.append(FrameworkDetection(name="Spring Data JPA", source=source))
|
|
@@ -298,10 +298,15 @@ def _structural_purpose(
|
|
|
298
298
|
head = f"Likely {role} (no DI annotations found)" if role else \
|
|
299
299
|
"Likely a structurally significant class (no DI annotations found)"
|
|
300
300
|
signals: list[str] = []
|
|
301
|
+
# BUG #6 (Broadleaf field test): this counts DISTINCT dependent classes (DI
|
|
302
|
+
# dependents + reverse-call-graph callers, deduped to class level), which is a
|
|
303
|
+
# different metric from modernize's node.in_degree (raw incoming-edge count
|
|
304
|
+
# across ALL edge types, symbol-level). Name it explicitly so the two numbers
|
|
305
|
+
# are not read as a contradiction when cross-referenced.
|
|
301
306
|
if in_degree >= 3:
|
|
302
|
-
signals.append(f"high in
|
|
307
|
+
signals.append(f"high fan-in ({in_degree} distinct caller classes)")
|
|
303
308
|
elif in_degree:
|
|
304
|
-
signals.append(f"
|
|
309
|
+
signals.append(f"{in_degree} distinct caller class{'es' if in_degree != 1 else ''}")
|
|
305
310
|
if lifecycle:
|
|
306
311
|
signals.append(f"lifecycle methods detected ({'/'.join(lifecycle)})")
|
|
307
312
|
suffix = f" — {', '.join(signals)}" if signals else ""
|
|
@@ -58,6 +58,13 @@ _ATTR_URL_RE = re.compile(r'url\s*=\s*"([^"]*)"')
|
|
|
58
58
|
_ATTR_NAME_RE = re.compile(r'(?:name|value)\s*=\s*"([^"]*)"')
|
|
59
59
|
_FIRST_LITERAL_RE = re.compile(r'^\s*"([^"]*)"')
|
|
60
60
|
|
|
61
|
+
# BUG #5 (Jenkins field test): thresholds for the structured coverage_confidence
|
|
62
|
+
# signal. A repo at/above _LARGE_REPO_FILE_THRESHOLD source files with fewer than
|
|
63
|
+
# _LOW_COVERAGE_COUNT recognized integration constructs is flagged "low" coverage —
|
|
64
|
+
# the count almost certainly under-represents custom-protocol/SPI integrations.
|
|
65
|
+
_LARGE_REPO_FILE_THRESHOLD: int = 300
|
|
66
|
+
_LOW_COVERAGE_COUNT: int = 10
|
|
67
|
+
|
|
61
68
|
# token -> (kind, client). Matched as whole-word usage outside imports/comments.
|
|
62
69
|
# Covers Spring AND plain-Java/Jakarta stacks (Quarkus, Micronaut, Keycloak SPI):
|
|
63
70
|
# the detector must not be Spring-centric, or a non-Spring repo with real LDAP /
|
|
@@ -76,6 +83,17 @@ _TOKEN_CLIENTS: "tuple[tuple[str, str, str], ...]" = (
|
|
|
76
83
|
("InitialLdapContext", "ldap", "jndi-ldap"),
|
|
77
84
|
("InitialDirContext", "ldap", "jndi-ldap"),
|
|
78
85
|
("LdapContext", "ldap", "jndi-ldap"),
|
|
86
|
+
# Spring Security LDAP authentication (BUG #5, Broadleaf field test): the
|
|
87
|
+
# idiomatic auth integration is a UserDetails mapper that extends
|
|
88
|
+
# LdapUserDetailsMapper and consumes DirContextOperations — neither uses
|
|
89
|
+
# LdapTemplate/ContextSource directly, so the connection-type tokens above
|
|
90
|
+
# miss it entirely. These types come from spring-security-ldap / spring-ldap
|
|
91
|
+
# and are unambiguous LDAP integration points.
|
|
92
|
+
("LdapUserDetailsMapper", "ldap", "spring-security-ldap"),
|
|
93
|
+
("DirContextOperations", "ldap", "spring-ldap"),
|
|
94
|
+
("LdapContextSource", "ldap", "spring-ldap"),
|
|
95
|
+
("DefaultSpringSecurityContextSource", "ldap", "spring-security-ldap"),
|
|
96
|
+
("BindAuthenticator", "ldap", "spring-security-ldap"),
|
|
79
97
|
# Mail / SMTP (JavaMail / Jakarta Mail)
|
|
80
98
|
("JavaMailSender", "smtp", "spring-mail"),
|
|
81
99
|
("MimeMessage", "smtp", "javamail"),
|
|
@@ -316,11 +334,50 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
316
334
|
# clients (DI, config-driven endpoints, JCA connectors) are invisible to static
|
|
317
335
|
# text matching. Report that explicitly instead of an authoritative "0".
|
|
318
336
|
confidence = "observed" if records else "not_analyzed"
|
|
337
|
+
|
|
338
|
+
# BUG #5 (Jenkins field test): the coverage caveat lived only inside the prose
|
|
339
|
+
# `coverage_note`. Propagate it as a STRUCTURED signal so an automated consumer
|
|
340
|
+
# does not read a low `count` as "low external coupling". A large repo with few
|
|
341
|
+
# recognized constructs (Jenkins: custom remoting/SCM/Update-Center SPIs, not
|
|
342
|
+
# RestTemplate/WebClient) is under-counted, not loosely coupled.
|
|
343
|
+
_repo_size = len(file_paths)
|
|
344
|
+
_large_repo = _repo_size >= _LARGE_REPO_FILE_THRESHOLD
|
|
345
|
+
if not records:
|
|
346
|
+
coverage_confidence = "low"
|
|
347
|
+
_cov_reason = (
|
|
348
|
+
"no recognized client-library construct found; custom protocol/SPI-based "
|
|
349
|
+
"or DI-wired integrations are not statically detectable by this analyzer. "
|
|
350
|
+
"A count of 0 does not imply the system has no outbound integrations."
|
|
351
|
+
)
|
|
352
|
+
elif _large_repo and len(records) < _LOW_COVERAGE_COUNT:
|
|
353
|
+
coverage_confidence = "low"
|
|
354
|
+
_cov_reason = (
|
|
355
|
+
f"only {len(records)} recognized construct(s) across {_repo_size} source "
|
|
356
|
+
"files; count reflects only recognized client-library patterns "
|
|
357
|
+
"(RestTemplate/WebClient/JDK/Apache/OkHttp/LDAP/JMS/…). Custom protocol/"
|
|
358
|
+
"SPI-based integrations are likely under-counted — do not read a low count "
|
|
359
|
+
"as low external coupling."
|
|
360
|
+
)
|
|
361
|
+
elif len(records) < _LOW_COVERAGE_COUNT:
|
|
362
|
+
coverage_confidence = "partial"
|
|
363
|
+
_cov_reason = (
|
|
364
|
+
"recognized client-library constructs detected; custom protocol/SPI-based "
|
|
365
|
+
"or DI-wired integrations, if any, are not statically visible."
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
coverage_confidence = "high"
|
|
369
|
+
_cov_reason = (
|
|
370
|
+
"recognized client-library constructs cover the observable integration "
|
|
371
|
+
"surface; runtime/DI-wired clients remain out of static scope."
|
|
372
|
+
)
|
|
373
|
+
|
|
319
374
|
return {
|
|
320
375
|
"integrations": records,
|
|
321
376
|
"by_kind": {k: by_kind[k] for k in sorted(by_kind)},
|
|
322
377
|
"count": len(records),
|
|
323
378
|
"confidence": confidence,
|
|
379
|
+
"coverage_confidence": coverage_confidence,
|
|
380
|
+
"coverage_confidence_reason": _cov_reason,
|
|
324
381
|
"coverage_note": (
|
|
325
382
|
"Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
|
|
326
383
|
"+ JNDI), DNS (JNDI DirContext w/ DnsContextFactory), SMTP (JavaMail, "
|