sourcecode 1.70.0__tar.gz → 1.71.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.71.0}/CHANGELOG.md +63 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/PKG-INFO +16 -5
- {sourcecode-1.70.0 → sourcecode-1.71.0}/README.md +15 -4
- {sourcecode-1.70.0 → sourcecode-1.71.0}/pyproject.toml +1 -1
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/architecture_summary.py +27 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/java.py +40 -2
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/integration_detector.py +46 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/migrate_check.py +120 -11
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/repository_ir.py +32 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/.gitignore +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/.ruff.toml +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/LICENSE +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/SECURITY.md +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/raw +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.70.0 → sourcecode-1.71.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.71.0] — 2026-07-01
|
|
4
|
+
|
|
5
|
+
### Fixed — framework-framing & migration reliability on a large non-Boot Spring repo (Jenkins core field test)
|
|
6
|
+
|
|
7
|
+
A migration/architecture audit of **jenkins-ci/jenkins** (`e7eef8d2cf`, branch
|
|
8
|
+
`master`; ~1,900 production Java classes, Maven multi-module, uses
|
|
9
|
+
`spring-security-web` but **no Spring MVC, no Spring Boot, no DI container** —
|
|
10
|
+
Jenkins runs on its own Stapler web framework and ACL/SecurityRealm security SPI)
|
|
11
|
+
surfaced five reproducible defects. In every case the engine already computed the
|
|
12
|
+
*correct* internal signal (downgraded confidence, `spring_present`, `not_detected`,
|
|
13
|
+
a prose `coverage_note`) but that signal did not propagate to the high-level field
|
|
14
|
+
an automated consumer reads first. Each fix targets the root cause and adds
|
|
15
|
+
regression coverage. All findings were verified against direct `grep` over the same
|
|
16
|
+
repo.
|
|
17
|
+
|
|
18
|
+
- **Framework detection — phantom "Spring MVC / Spring AOP" from pom exclusion blocks (BUG #1).**
|
|
19
|
+
`_frameworks_from_pom` serialized the *entire* pom to text and substring-matched
|
|
20
|
+
framework tokens, so `<exclude>org.springframework:spring-web</exclude>` and
|
|
21
|
+
`<exclude>…:spring-aop</exclude>` inside an enforcer bytecode rule in
|
|
22
|
+
`war/pom.xml` (artifact **exclusions**, not dependencies) read as "uses Spring
|
|
23
|
+
MVC / Spring AOP". Detection is now restricted to real dependency / plugin /
|
|
24
|
+
parent **coordinates** (`<exclusion>`/`<exclude>` and comments carry none) — the
|
|
25
|
+
same discipline the Gradle path already applied (JobRunr fix). The Spring Security
|
|
26
|
+
token was also broadened to the true-positive `spring-security-web` /
|
|
27
|
+
`-config` coordinates (Jenkins' real dep), so the honest label survives while the
|
|
28
|
+
phantoms disappear.
|
|
29
|
+
- **`architecture_summary` — categorical "MVC pattern with … view layers" on a repo with zero controllers (BUG #1).**
|
|
30
|
+
The `mvc` pattern is a directory-name heuristic. On a Java/Kotlin repo it is now
|
|
31
|
+
cross-checked against the canonical endpoint extractor in the same run; with no
|
|
32
|
+
HTTP controllers detected it degrades to a directory-derived "Layered code
|
|
33
|
+
organization (… — no HTTP controllers detected)" note, dropping the unverified
|
|
34
|
+
"view" claim rather than asserting MVC.
|
|
35
|
+
- **`migrate-check` — `boot3` dimension applicable without any Spring Boot (BUG #2).**
|
|
36
|
+
The Boot 2→3 axis was gated on `spring_present` (any `org.springframework.*`
|
|
37
|
+
coordinate), so a repo carrying only `spring-security-web` received a
|
|
38
|
+
contaminated `readiness_score` from a migration that does not exist. Applicability
|
|
39
|
+
now requires **positive evidence**: a `spring-boot*` coordinate / Gradle plugin
|
|
40
|
+
(`@SpringBootApplication` always ships one), **or** a concrete `spring_boot_3` /
|
|
41
|
+
`spring_security_6` finding in main code. jakarta findings alone no longer enable
|
|
42
|
+
boot3. A new `spring_boot_present` flag is surfaced; when N/A the dimension is
|
|
43
|
+
excluded from the aggregate with a clear reason.
|
|
44
|
+
- **`migrate-check` — `MIG-002` (javax.servlet) counted `@Deprecated` frozen-legacy shims as high blockers (BUG #3).**
|
|
45
|
+
A namespace-migration finding on a **class-level `@Deprecated`** type (kept for
|
|
46
|
+
binary compat, with a live replacement — Jenkins `ChainedServletFilter` →
|
|
47
|
+
`ChainedServletFilter2`) is now classified `code_context="deprecated_shim"` and
|
|
48
|
+
excluded from `blocking_count`, readiness, and effort, alongside the existing
|
|
49
|
+
test/generated buckets. A `@Deprecated` **method** on a live class stays blocking.
|
|
50
|
+
On Jenkins this dropped `blocking_count` from 6 to 2.
|
|
51
|
+
- **`repo-ir` — `security_model` was a bare `"unknown"` string with no reason (BUG #4).**
|
|
52
|
+
A structured `security_model_detail` companion now mirrors the `not_detected`
|
|
53
|
+
pattern used elsewhere: `status`, `spring_security_filter_chain_detected`,
|
|
54
|
+
`annotation_policies_detected`, `custom_spi_detected`, and a `reason` that
|
|
55
|
+
explicitly notes absence of a *recognized* pattern is not evidence the repo is
|
|
56
|
+
unsecured (Jenkins uses a custom ACL/SecurityRealm SPI).
|
|
57
|
+
- **`export --integrations` — low count carried no structured coverage flag (BUG #5).**
|
|
58
|
+
The honest coverage caveat lived only in the prose `coverage_note`. A structured
|
|
59
|
+
`coverage_confidence` (`low` / `partial` / `high`) plus `coverage_confidence_reason`
|
|
60
|
+
is now emitted: a large repo (≥300 source files) with <10 recognized constructs is
|
|
61
|
+
flagged `low` so a consumer cannot read a low count as low external coupling
|
|
62
|
+
(Jenkins' custom remoting/SCM/Update-Center SPIs are invisible to static matching).
|
|
63
|
+
|
|
64
|
+
New regression coverage across five suites; full suite green (2,900+ tests).
|
|
65
|
+
|
|
3
66
|
## [1.70.0] — 2026-07-01
|
|
4
67
|
|
|
5
68
|
### 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.71.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",
|
|
@@ -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))
|
|
@@ -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 /
|
|
@@ -316,11 +323,50 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
316
323
|
# clients (DI, config-driven endpoints, JCA connectors) are invisible to static
|
|
317
324
|
# text matching. Report that explicitly instead of an authoritative "0".
|
|
318
325
|
confidence = "observed" if records else "not_analyzed"
|
|
326
|
+
|
|
327
|
+
# BUG #5 (Jenkins field test): the coverage caveat lived only inside the prose
|
|
328
|
+
# `coverage_note`. Propagate it as a STRUCTURED signal so an automated consumer
|
|
329
|
+
# does not read a low `count` as "low external coupling". A large repo with few
|
|
330
|
+
# recognized constructs (Jenkins: custom remoting/SCM/Update-Center SPIs, not
|
|
331
|
+
# RestTemplate/WebClient) is under-counted, not loosely coupled.
|
|
332
|
+
_repo_size = len(file_paths)
|
|
333
|
+
_large_repo = _repo_size >= _LARGE_REPO_FILE_THRESHOLD
|
|
334
|
+
if not records:
|
|
335
|
+
coverage_confidence = "low"
|
|
336
|
+
_cov_reason = (
|
|
337
|
+
"no recognized client-library construct found; custom protocol/SPI-based "
|
|
338
|
+
"or DI-wired integrations are not statically detectable by this analyzer. "
|
|
339
|
+
"A count of 0 does not imply the system has no outbound integrations."
|
|
340
|
+
)
|
|
341
|
+
elif _large_repo and len(records) < _LOW_COVERAGE_COUNT:
|
|
342
|
+
coverage_confidence = "low"
|
|
343
|
+
_cov_reason = (
|
|
344
|
+
f"only {len(records)} recognized construct(s) across {_repo_size} source "
|
|
345
|
+
"files; count reflects only recognized client-library patterns "
|
|
346
|
+
"(RestTemplate/WebClient/JDK/Apache/OkHttp/LDAP/JMS/…). Custom protocol/"
|
|
347
|
+
"SPI-based integrations are likely under-counted — do not read a low count "
|
|
348
|
+
"as low external coupling."
|
|
349
|
+
)
|
|
350
|
+
elif len(records) < _LOW_COVERAGE_COUNT:
|
|
351
|
+
coverage_confidence = "partial"
|
|
352
|
+
_cov_reason = (
|
|
353
|
+
"recognized client-library constructs detected; custom protocol/SPI-based "
|
|
354
|
+
"or DI-wired integrations, if any, are not statically visible."
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
coverage_confidence = "high"
|
|
358
|
+
_cov_reason = (
|
|
359
|
+
"recognized client-library constructs cover the observable integration "
|
|
360
|
+
"surface; runtime/DI-wired clients remain out of static scope."
|
|
361
|
+
)
|
|
362
|
+
|
|
319
363
|
return {
|
|
320
364
|
"integrations": records,
|
|
321
365
|
"by_kind": {k: by_kind[k] for k in sorted(by_kind)},
|
|
322
366
|
"count": len(records),
|
|
323
367
|
"confidence": confidence,
|
|
368
|
+
"coverage_confidence": coverage_confidence,
|
|
369
|
+
"coverage_confidence_reason": _cov_reason,
|
|
324
370
|
"coverage_note": (
|
|
325
371
|
"Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
|
|
326
372
|
"+ JNDI), DNS (JNDI DirContext w/ DnsContextFactory), SMTP (JavaMail, "
|
|
@@ -652,6 +652,13 @@ def _classify_code_context(finding: "MigrationFinding") -> str:
|
|
|
652
652
|
imp.rsplit(".", 1)[-1] == "Generated" for imp in finding.imports_found
|
|
653
653
|
):
|
|
654
654
|
return "generated"
|
|
655
|
+
# BUG #3 (Jenkins field test): a namespace-migration finding (javax→jakarta,
|
|
656
|
+
# e.g. javax.servlet) on a class-level @Deprecated type is a frozen-legacy
|
|
657
|
+
# compatibility shim — kept for binary compat, with a live replacement elsewhere
|
|
658
|
+
# (Jenkins ChainedServletFilter → ChainedServletFilter2). Not an active blocker:
|
|
659
|
+
# segregate it out of blocking_count like test/generated, with its own label.
|
|
660
|
+
if finding.enclosing_deprecated and finding.migration_target in _JAKARTA_TARGETS:
|
|
661
|
+
return "deprecated_shim"
|
|
655
662
|
return "main"
|
|
656
663
|
|
|
657
664
|
|
|
@@ -1181,6 +1188,11 @@ class MigrationFinding:
|
|
|
1181
1188
|
# harness), or "generated" (autogenerated source). Only "main" counts toward
|
|
1182
1189
|
# blocking_count / readiness; test+generated are reported in a separate bucket.
|
|
1183
1190
|
code_context: str = "main"
|
|
1191
|
+
# BUG #3 (Jenkins field test): the enclosing top-level TYPE carries a class-level
|
|
1192
|
+
# @Deprecated annotation. A namespace-migration finding on such a class is a
|
|
1193
|
+
# frozen-legacy compatibility shim (kept for binary compat, replaced elsewhere),
|
|
1194
|
+
# not an active migration blocker — reclassified out of blocking_count.
|
|
1195
|
+
enclosing_deprecated: bool = False
|
|
1184
1196
|
|
|
1185
1197
|
@staticmethod
|
|
1186
1198
|
def make_id(rule_id: str, source_file: str) -> str:
|
|
@@ -1294,6 +1306,12 @@ class MigrationReport:
|
|
|
1294
1306
|
spring_boot_version_detected: Optional[str] = None
|
|
1295
1307
|
# BUG #2: whether Spring is used AT RUNTIME. The Boot3 dimension is N/A without it.
|
|
1296
1308
|
spring_present: bool = True
|
|
1309
|
+
# BUG #2 (Jenkins field test): whether the repo is a Spring BOOT application
|
|
1310
|
+
# (spring-boot* coordinate / @SpringBootApplication), as opposed to merely having
|
|
1311
|
+
# a Spring library (e.g. spring-security-web) on the classpath. The boot3 (Boot
|
|
1312
|
+
# 2→3) migration dimension is applicable ONLY to Boot repos — a non-Boot Spring
|
|
1313
|
+
# repo has no Boot upgrade axis and must not receive a contaminated readiness.
|
|
1314
|
+
spring_boot_present: bool = False
|
|
1297
1315
|
# BUG #1/#2: Spring appears ONLY as a test dependency (e.g. spring-test). Reported
|
|
1298
1316
|
# so a consumer can see WHY boot3 is N/A despite an org.springframework coordinate.
|
|
1299
1317
|
spring_test_only: bool = False
|
|
@@ -1366,7 +1384,19 @@ class MigrationReport:
|
|
|
1366
1384
|
self.jakarta_readiness = _dimension_score(main_findings, _JAKARTA_TARGETS)
|
|
1367
1385
|
self.boot3_readiness = _dimension_score(main_findings, _BOOT3_MIGRATION_TARGETS)
|
|
1368
1386
|
self.jdk_modernization = _dimension_score(main_findings, None)
|
|
1369
|
-
|
|
1387
|
+
# BUG #2 (Jenkins field test): the boot3 axis applies only with POSITIVE
|
|
1388
|
+
# evidence — a Spring BOOT repo (spring-boot* coordinate / @SpringBootApplication),
|
|
1389
|
+
# OR a concrete Boot-3 / Security-6 migration finding in main code. A repo that
|
|
1390
|
+
# merely carries a Spring library (spring-security-web) with no Boot and no
|
|
1391
|
+
# Boot/Security migration findings (Jenkins) has no boot3 axis — jakarta-shaped
|
|
1392
|
+
# findings must not masquerade as a boot3 readiness deficit. Note: jakarta
|
|
1393
|
+
# findings do NOT make boot3 applicable; only genuine boot3/security-6 targets do.
|
|
1394
|
+
_boot3_migration_findings = any(
|
|
1395
|
+
f.migration_target in (_BOOT3_MIGRATION_TARGETS - _JAKARTA_TARGETS)
|
|
1396
|
+
for f in main_findings
|
|
1397
|
+
)
|
|
1398
|
+
_boot3_applies = self.spring_boot_present or _boot3_migration_findings
|
|
1399
|
+
if not _boot3_applies:
|
|
1370
1400
|
self.boot3_readiness = 100
|
|
1371
1401
|
|
|
1372
1402
|
# BUG #3/#4: the jakarta dimension is applicable ONLY with POSITIVE evidence the
|
|
@@ -1442,14 +1472,23 @@ class MigrationReport:
|
|
|
1442
1472
|
else "N/A — no migratable javax.* imports detected"),
|
|
1443
1473
|
},
|
|
1444
1474
|
"boot3": {
|
|
1445
|
-
"applicable":
|
|
1446
|
-
"score": self.boot3_readiness if
|
|
1447
|
-
"reason": (
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1475
|
+
"applicable": _boot3_applies,
|
|
1476
|
+
"score": self.boot3_readiness if _boot3_applies else None,
|
|
1477
|
+
"reason": (
|
|
1478
|
+
"Spring Boot 2→3 / Security 6 migration"
|
|
1479
|
+
if self.spring_boot_present
|
|
1480
|
+
else "Spring Security 6 migration (spring_security_6 / spring_boot_3 "
|
|
1481
|
+
"findings present) — no Spring Boot detected"
|
|
1482
|
+
if _boot3_applies
|
|
1483
|
+
else "N/A — Spring present only as a TEST dependency (spring-test); "
|
|
1484
|
+
"no runtime Spring, no Spring Boot"
|
|
1485
|
+
if self.spring_test_only
|
|
1486
|
+
else "N/A — Spring library on the classpath but no spring-boot "
|
|
1487
|
+
"dependency / @SpringBootApplication and no Boot/Security-6 "
|
|
1488
|
+
"migration finding — not a Spring Boot application"
|
|
1489
|
+
if self.spring_present
|
|
1490
|
+
else "N/A — no Spring usage detected (non-Spring stack)"
|
|
1491
|
+
),
|
|
1453
1492
|
},
|
|
1454
1493
|
"jdk_modernization": {"applicable": True, "score": self.jdk_modernization,
|
|
1455
1494
|
"reason": "orthogonal JDK modernization debt (excluded from "
|
|
@@ -1542,8 +1581,11 @@ class MigrationReport:
|
|
|
1542
1581
|
"count": sum(_nb_by_ctx.values()),
|
|
1543
1582
|
"by_context": _nb_by_ctx,
|
|
1544
1583
|
"by_rule": _nb_by_rule,
|
|
1545
|
-
"note": ("Findings in test/fixture harnesses
|
|
1546
|
-
"
|
|
1584
|
+
"note": ("Findings in test/fixture harnesses, autogenerated sources, or "
|
|
1585
|
+
"class-level @Deprecated frozen-legacy shims (code_context="
|
|
1586
|
+
"\"deprecated_shim\" — kept for binary compat with a live "
|
|
1587
|
+
"replacement; verify before removing). Excluded from "
|
|
1588
|
+
"blocking_count, readiness, and effort."),
|
|
1547
1589
|
}
|
|
1548
1590
|
|
|
1549
1591
|
self.summary = {
|
|
@@ -1585,6 +1627,7 @@ class MigrationReport:
|
|
|
1585
1627
|
"hygiene_findings": self.hygiene_findings,
|
|
1586
1628
|
"non_blocking": self.non_blocking,
|
|
1587
1629
|
"spring_present": self.spring_present,
|
|
1630
|
+
"spring_boot_present": self.spring_boot_present,
|
|
1588
1631
|
"spring_test_only": self.spring_test_only,
|
|
1589
1632
|
"spring_boot_2_detected": self.spring_boot_2_detected,
|
|
1590
1633
|
"spring_boot_version_detected": self.spring_boot_version_detected,
|
|
@@ -1746,12 +1789,35 @@ def _neutralize_non_spring_explanation(text: str) -> str:
|
|
|
1746
1789
|
return text
|
|
1747
1790
|
|
|
1748
1791
|
|
|
1792
|
+
# BUG #3 (Jenkins field test): the primary top-level type declaration in a .java
|
|
1793
|
+
# file. Used to test whether a class-level @Deprecated annotation precedes it.
|
|
1794
|
+
_PRIMARY_TYPE_DECL_RE: re.Pattern = re.compile(
|
|
1795
|
+
r"(?m)^[ \t]*(?:public\s+|final\s+|abstract\s+|sealed\s+|non-sealed\s+|strictfp\s+)*"
|
|
1796
|
+
r"(?:class|interface|enum|record)\s+\w"
|
|
1797
|
+
)
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
def _primary_type_is_deprecated(source: str) -> bool:
|
|
1801
|
+
"""True iff the file's primary top-level type carries a class-level @Deprecated
|
|
1802
|
+
annotation. Scoped to the region between the last import and the type keyword so
|
|
1803
|
+
a @Deprecated on a METHOD (below the class declaration) never counts — that is a
|
|
1804
|
+
live class with a deprecated member, not a frozen-legacy shim."""
|
|
1805
|
+
m = _PRIMARY_TYPE_DECL_RE.search(source)
|
|
1806
|
+
if not m:
|
|
1807
|
+
return False
|
|
1808
|
+
head = source[: m.start()]
|
|
1809
|
+
last_import = head.rfind("\nimport ")
|
|
1810
|
+
window = head[last_import:] if last_import != -1 else head
|
|
1811
|
+
return "@Deprecated" in window
|
|
1812
|
+
|
|
1813
|
+
|
|
1749
1814
|
def _scan_file(
|
|
1750
1815
|
source: str,
|
|
1751
1816
|
rel_path: str,
|
|
1752
1817
|
rules: list[_Rule],
|
|
1753
1818
|
) -> list[MigrationFinding]:
|
|
1754
1819
|
findings: list[MigrationFinding] = []
|
|
1820
|
+
type_deprecated = _primary_type_is_deprecated(source)
|
|
1755
1821
|
|
|
1756
1822
|
for rule in rules:
|
|
1757
1823
|
matched_imports: list[str] = []
|
|
@@ -1820,6 +1886,7 @@ def _scan_file(
|
|
|
1820
1886
|
fix_hint=rule.fix_hint,
|
|
1821
1887
|
migration_target=rule.migration_target,
|
|
1822
1888
|
openrewrite_recipe=rule.openrewrite_recipe,
|
|
1889
|
+
enclosing_deprecated=type_deprecated,
|
|
1823
1890
|
)
|
|
1824
1891
|
)
|
|
1825
1892
|
|
|
@@ -1941,6 +2008,7 @@ def run_migrate_check(
|
|
|
1941
2008
|
spring_present, spring_test_only = _detect_spring_usage(
|
|
1942
2009
|
root, spring_runtime_import_seen, spring_test_import_seen
|
|
1943
2010
|
)
|
|
2011
|
+
spring_boot_present = _detect_spring_boot_present(root)
|
|
1944
2012
|
|
|
1945
2013
|
# BUG #8: classify each finding's code context (main / test / generated) so the
|
|
1946
2014
|
# report can keep test fixtures and autogenerated sources out of blocking_count.
|
|
@@ -1965,6 +2033,7 @@ def run_migrate_check(
|
|
|
1965
2033
|
spring_boot_2_detected=spring_boot_2,
|
|
1966
2034
|
spring_boot_version_detected=spring_boot_version,
|
|
1967
2035
|
spring_present=spring_present,
|
|
2036
|
+
spring_boot_present=spring_boot_present,
|
|
1968
2037
|
spring_test_only=spring_test_only,
|
|
1969
2038
|
jakarta_namespace_adopted=jakarta_import_count > 0,
|
|
1970
2039
|
findings=all_findings,
|
|
@@ -2065,6 +2134,46 @@ def _extract_boot_versions(text: str) -> tuple[set[int], Optional[str]]:
|
|
|
2065
2134
|
return majors, full
|
|
2066
2135
|
|
|
2067
2136
|
|
|
2137
|
+
# BUG #2 (Jenkins field test): strip XML comments and exclusion/exclude blocks
|
|
2138
|
+
# before scanning for a spring-boot coordinate, so a `<exclude>...:spring-boot</…>`
|
|
2139
|
+
# or a comment never reads as "this repo uses Spring Boot" — the same exclusion
|
|
2140
|
+
# poison that produced phantom Spring MVC/AOP in framework detection.
|
|
2141
|
+
_XML_COMMENT_RE: re.Pattern = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
2142
|
+
_XML_EXCLUSION_EL_RE: re.Pattern = re.compile(r"<exclusion\b.*?</exclusion\s*>", re.DOTALL | re.IGNORECASE)
|
|
2143
|
+
_XML_EXCLUDE_EL_RE: re.Pattern = re.compile(r"<exclude\b.*?</exclude\s*>", re.DOTALL | re.IGNORECASE)
|
|
2144
|
+
# A spring-boot* Maven/Gradle artifact coordinate (starter, BOM, dependency, plugin).
|
|
2145
|
+
_SPRING_BOOT_ARTIFACT_RE: re.Pattern = re.compile(r"\bspring-boot(?:-[a-z0-9.\-]+)?\b", re.IGNORECASE)
|
|
2146
|
+
|
|
2147
|
+
|
|
2148
|
+
def _strip_xml_exclusions(text: str) -> str:
|
|
2149
|
+
"""Remove XML comments and exclusion/exclude elements so their coordinates are
|
|
2150
|
+
not mistaken for declared dependencies."""
|
|
2151
|
+
text = _XML_COMMENT_RE.sub(" ", text)
|
|
2152
|
+
text = _XML_EXCLUSION_EL_RE.sub(" ", text)
|
|
2153
|
+
text = _XML_EXCLUDE_EL_RE.sub(" ", text)
|
|
2154
|
+
return text
|
|
2155
|
+
|
|
2156
|
+
|
|
2157
|
+
def _detect_spring_boot_present(root: Path) -> bool:
|
|
2158
|
+
"""True iff there is POSITIVE Spring Boot evidence — a spring-boot* build
|
|
2159
|
+
coordinate (parent / BOM / dependency / plugin) or the Spring Boot Gradle
|
|
2160
|
+
plugin. Distinguishes a Spring Boot application from a repo that merely has a
|
|
2161
|
+
Spring library on the classpath (e.g. Jenkins' spring-security-web). Gates the
|
|
2162
|
+
boot3 (Boot 2→3) migration dimension. @SpringBootApplication is not scanned
|
|
2163
|
+
separately: its annotation class ships in spring-boot-autoconfigure, so a Boot
|
|
2164
|
+
app always carries a spring-boot* coordinate."""
|
|
2165
|
+
for abs_path, _rel in _find_build_files(root):
|
|
2166
|
+
try:
|
|
2167
|
+
text = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
2168
|
+
except OSError:
|
|
2169
|
+
continue
|
|
2170
|
+
if _SPRING_BOOT_PLUGIN_RE.search(text):
|
|
2171
|
+
return True
|
|
2172
|
+
if _SPRING_BOOT_ARTIFACT_RE.search(_strip_xml_exclusions(text)):
|
|
2173
|
+
return True
|
|
2174
|
+
return False
|
|
2175
|
+
|
|
2176
|
+
|
|
2068
2177
|
def _detect_spring_boot(root: Path, jakarta_import_count: int) -> tuple[Optional[bool], Optional[str]]:
|
|
2069
2178
|
"""Tri-state Spring Boot 2 detection. Returns (spring_boot_2_detected, version).
|
|
2070
2179
|
|
|
@@ -2872,12 +2872,35 @@ def _assemble(
|
|
|
2872
2872
|
else:
|
|
2873
2873
|
_security_model_asm = "unknown"
|
|
2874
2874
|
|
|
2875
|
+
# BUG #4 (Jenkins field test): the bare "unknown" string carried no reason or
|
|
2876
|
+
# sub-structure — inconsistent with the not_detected+reason pattern used
|
|
2877
|
+
# elsewhere (migrate-check hibernate). Emit a structured companion so a consumer
|
|
2878
|
+
# sees WHY, and never reads "unknown" as "unsecured". A repo with a custom
|
|
2879
|
+
# security SPI (Jenkins ACL/SecurityRealm) simply matched no RECOGNIZED pattern.
|
|
2880
|
+
_security_model_detail_asm = {
|
|
2881
|
+
"model": _security_model_asm,
|
|
2882
|
+
"status": "detected" if _security_model_asm != "unknown" else "not_detected",
|
|
2883
|
+
"spring_security_filter_chain_detected": _filter_based_asm,
|
|
2884
|
+
"annotation_policies_detected": _has_ann_sec_asm,
|
|
2885
|
+
"custom_spi_detected": [],
|
|
2886
|
+
"reason": (
|
|
2887
|
+
"recognized security model: " + _security_model_asm
|
|
2888
|
+
if _security_model_asm != "unknown"
|
|
2889
|
+
else "N/A — no Spring Security filter chain, no annotation-based policy, "
|
|
2890
|
+
"and no recognized custom security SPI matched. Absence of a "
|
|
2891
|
+
"recognized pattern is not evidence the repo is unsecured — it may "
|
|
2892
|
+
"use a framework-specific or custom access-control model this "
|
|
2893
|
+
"analyzer does not model."
|
|
2894
|
+
),
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2875
2897
|
return {
|
|
2876
2898
|
**_base,
|
|
2877
2899
|
"route_surface": _route_surface,
|
|
2878
2900
|
"spring_events": _spring_events,
|
|
2879
2901
|
"analysis_gaps": _analysis_gaps,
|
|
2880
2902
|
"security_model": _security_model_asm,
|
|
2903
|
+
"security_model_detail": _security_model_detail_asm,
|
|
2881
2904
|
"audit": {
|
|
2882
2905
|
"dropped_fields": dropped_fields,
|
|
2883
2906
|
},
|
|
@@ -3510,6 +3533,15 @@ def build_repo_ir(
|
|
|
3510
3533
|
ir["security_model"] = "xml_or_filter_chain"
|
|
3511
3534
|
elif _sec_model in ("annotation_based", "mixed"):
|
|
3512
3535
|
ir["security_model"] = "mixed"
|
|
3536
|
+
# BUG #4: keep the structured detail consistent with the rewritten string.
|
|
3537
|
+
_detail = ir.get("security_model_detail")
|
|
3538
|
+
if isinstance(_detail, dict):
|
|
3539
|
+
_detail["model"] = ir["security_model"]
|
|
3540
|
+
_detail["status"] = "detected"
|
|
3541
|
+
_detail["spring_security_filter_chain_detected"] = True
|
|
3542
|
+
_detail["reason"] = ("XML/filter-chain security configuration detected "
|
|
3543
|
+
"(declarative filter mapping in web.xml or Spring "
|
|
3544
|
+
"security XML)")
|
|
3513
3545
|
# Retag route_surface entries that have no security (would become none_detected in CIR)
|
|
3514
3546
|
for _r in ir.get("route_surface") or []:
|
|
3515
3547
|
_r_sec = _r.get("security_annotations")
|
|
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
|