sourcecode 1.71.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.71.0 → sourcecode-1.72.0}/CHANGELOG.md +62 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/PKG-INFO +1 -1
- {sourcecode-1.71.0 → sourcecode-1.72.0}/pyproject.toml +1 -1
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/canonical_ir.py +14 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/cli.py +77 -13
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/explain.py +7 -2
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/integration_detector.py +11 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/migrate_check.py +50 -1
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/repository_ir.py +311 -10
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/spring_security_audit.py +11 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/spring_tx_analyzer.py +70 -9
- {sourcecode-1.71.0 → sourcecode-1.72.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/.gitignore +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/.ruff.toml +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/LICENSE +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/README.md +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/SECURITY.md +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/raw +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.71.0 → sourcecode-1.72.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,67 @@
|
|
|
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
|
+
|
|
3
65
|
## [1.71.0] — 2026-07-01
|
|
4
66
|
|
|
5
67
|
### Fixed — framework-framing & migration reliability on a large non-Boot Spring repo (Jenkins core field test)
|
|
@@ -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
|
(
|
|
@@ -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 ""
|
|
@@ -83,6 +83,17 @@ _TOKEN_CLIENTS: "tuple[tuple[str, str, str], ...]" = (
|
|
|
83
83
|
("InitialLdapContext", "ldap", "jndi-ldap"),
|
|
84
84
|
("InitialDirContext", "ldap", "jndi-ldap"),
|
|
85
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"),
|
|
86
97
|
# Mail / SMTP (JavaMail / Jakarta Mail)
|
|
87
98
|
("JavaMailSender", "smtp", "spring-mail"),
|
|
88
99
|
("MimeMessage", "smtp", "javamail"),
|
|
@@ -645,6 +645,18 @@ def _classify_code_context(finding: "MigrationFinding") -> str:
|
|
|
645
645
|
if is_test_or_fixture_path(path):
|
|
646
646
|
return "test"
|
|
647
647
|
norm = path.replace("\\", "/").lower()
|
|
648
|
+
# BUG #4 (Broadleaf field test): a Spring XML config file can live under
|
|
649
|
+
# src/main/resources yet be test-only scaffolding loaded exclusively by
|
|
650
|
+
# test @ContextConfiguration (e.g. bl-applicationContext-test-security.xml).
|
|
651
|
+
# is_test_or_fixture_path deliberately ignores the filename, so a `test`
|
|
652
|
+
# marker in the config file's own name is not seen. For XML config, a `test`
|
|
653
|
+
# token in the base filename is a strong, low-FP test-scope signal —
|
|
654
|
+
# production context files are not named `-test-`. Keep it out of
|
|
655
|
+
# blocking_count so test scaffolding never inflates the headline blocker.
|
|
656
|
+
if norm.endswith(".xml"):
|
|
657
|
+
base = norm.rsplit("/", 1)[-1]
|
|
658
|
+
if re.search(r"(?:^|[-_.])test(?:[-_.]|$)", base):
|
|
659
|
+
return "test"
|
|
648
660
|
if any(frag in norm for frag in _GENERATED_PATH_FRAGMENTS):
|
|
649
661
|
return "generated"
|
|
650
662
|
# javax.annotation.Generated is the marker emitted into autogenerated code.
|
|
@@ -936,6 +948,42 @@ def _find_non_java_files(
|
|
|
936
948
|
return xml_files, build_files
|
|
937
949
|
|
|
938
950
|
|
|
951
|
+
def _xml_explanation_for(rule: "_XmlRule", matches: list) -> str:
|
|
952
|
+
"""Return an explanation reflecting the sub-pattern that actually matched.
|
|
953
|
+
|
|
954
|
+
BUG #4 (Broadleaf field test): MIG-031's pattern is an OR of two independent
|
|
955
|
+
triggers — old-style `<http auto-config="true">` and a versioned legacy schema
|
|
956
|
+
(spring-security-[2-5].x.xsd). The static template asserted BOTH, so a file
|
|
957
|
+
that only used auto-config (its schema line already being the unversioned
|
|
958
|
+
spring-security.xsd) got a factually wrong explanation claiming a versioned
|
|
959
|
+
schema. Emit only the condition(s) present in this file.
|
|
960
|
+
"""
|
|
961
|
+
if rule.id != "MIG-031":
|
|
962
|
+
return rule.explanation
|
|
963
|
+
matched_text = " ".join(m.group(0).lower() for m in matches)
|
|
964
|
+
has_autoconfig = "auto-config" in matched_text
|
|
965
|
+
has_versioned_schema = bool(re.search(r"spring-security-[2345]\.", matched_text))
|
|
966
|
+
triggers: list[str] = []
|
|
967
|
+
if has_autoconfig:
|
|
968
|
+
triggers.append(
|
|
969
|
+
"uses the old-style <http auto-config='true'> shortcut, which was "
|
|
970
|
+
"removed in Spring Security 6"
|
|
971
|
+
)
|
|
972
|
+
if has_versioned_schema:
|
|
973
|
+
triggers.append(
|
|
974
|
+
"references a versioned legacy schema (spring-security-[2-5].x.xsd) "
|
|
975
|
+
"whose namespace attributes changed in Spring Security 6"
|
|
976
|
+
)
|
|
977
|
+
if not triggers: # defensive — pattern matched but neither branch classified
|
|
978
|
+
return rule.explanation
|
|
979
|
+
return (
|
|
980
|
+
"XML-based Spring Security configuration in this file "
|
|
981
|
+
+ " and ".join(triggers)
|
|
982
|
+
+ ". Spring Security 6 (Spring Boot 3) requires migrating this to "
|
|
983
|
+
"Java-based @Configuration with a SecurityFilterChain @Bean."
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
|
|
939
987
|
def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
|
|
940
988
|
"""Apply XML rules to raw XML text. Returns one finding per matched rule."""
|
|
941
989
|
findings: list[MigrationFinding] = []
|
|
@@ -947,6 +995,7 @@ def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
|
|
|
947
995
|
continue
|
|
948
996
|
first_line = text[: matches[0].start()].count("\n") + 1
|
|
949
997
|
snippets = [m.group(0)[:120].strip() for m in matches[:5]]
|
|
998
|
+
explanation = _xml_explanation_for(rule, matches)
|
|
950
999
|
findings.append(
|
|
951
1000
|
MigrationFinding(
|
|
952
1001
|
id=MigrationFinding.make_id(rule.id, rel_path),
|
|
@@ -956,7 +1005,7 @@ def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
|
|
|
956
1005
|
source_file=rel_path,
|
|
957
1006
|
first_line=first_line,
|
|
958
1007
|
imports_found=snippets,
|
|
959
|
-
explanation=
|
|
1008
|
+
explanation=explanation,
|
|
960
1009
|
fix_hint=rule.fix_hint,
|
|
961
1010
|
migration_target=rule.migration_target,
|
|
962
1011
|
openrewrite_recipe=rule.openrewrite_recipe,
|