sourcecode 1.63.0__tar.gz → 1.64.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.63.0 → sourcecode-1.64.0}/CHANGELOG.md +51 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/PKG-INFO +20 -2
- {sourcecode-1.63.0 → sourcecode-1.64.0}/README.md +19 -1
- {sourcecode-1.63.0 → sourcecode-1.64.0}/pyproject.toml +1 -1
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/hibernate_strat.py +194 -13
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/migrate_check.py +79 -1
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/repository_ir.py +70 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/.gitignore +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/.ruff.toml +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/LICENSE +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/SECURITY.md +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/raw +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/integration_detector.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.63.0 → sourcecode-1.64.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.64.0] — 2026-06-30
|
|
4
|
+
|
|
5
|
+
### Fixed — zero false-positive blockers (`migrate-check` / `endpoints`)
|
|
6
|
+
|
|
7
|
+
A Pro audit of a real enterprise repo (Alfresco Content Services Community, 6,549
|
|
8
|
+
classes) surfaced false blockers that would have invalidated a ~2 person-month
|
|
9
|
+
budget decision. A migration blocker is a high-cost claim: if the tool cannot
|
|
10
|
+
**prove** it from resolved AST/dependency signals, it must not be declared. This
|
|
11
|
+
release enforces that — blockers operate on parsed imports / fully-qualified type
|
|
12
|
+
references, never on a substring inside a comment, Javadoc, string literal, or
|
|
13
|
+
classpath resource path. The `hibernate` sub-`schema_version` is now **2.1**.
|
|
14
|
+
|
|
15
|
+
- **Phantom Hibernate eliminated.** Hibernate detection now requires real evidence:
|
|
16
|
+
an `org.hibernate:*` build dependency, a parsed `import org.hibernate.*`, or a
|
|
17
|
+
parsed `import {jakarta,javax}.persistence.*`. Absent all three →
|
|
18
|
+
`hibernate.detected = false`, no `hibernate_rewrite` headline, no phantom effort.
|
|
19
|
+
All scanning runs on source with comments + string/char literals stripped (line
|
|
20
|
+
numbers preserved), so an `org.hibernate.dialect.*` substring in Javadoc or an
|
|
21
|
+
iBatis resource path can no longer trigger a verdict. Over-generic Criteria tokens
|
|
22
|
+
(`Predicate`, `Root<`, `Criterion`, `Conjunction`, `Disjunction`) were dropped —
|
|
23
|
+
they collide with project-owned domain classes. SPI bare-name matches
|
|
24
|
+
(`implements XInterceptor`, `extends XEventListener`) now require an
|
|
25
|
+
`org.hibernate` import in the same file; FQN-anchored SPI matches still count
|
|
26
|
+
on their own. New `hibernate.evidence{}` records the proof + `confidence`.
|
|
27
|
+
- **MIG-004 / jakarta rules respect a JDK allowlist.** `javax.transaction.xa.*`
|
|
28
|
+
(the Java SE `java.transaction.xa` module), `javax.annotation.processing.*`,
|
|
29
|
+
`javax.sql.*`, `javax.xml.parsers/transform/...`, `javax.management.*`,
|
|
30
|
+
`javax.crypto.*`, and the other permanent JDK/JSR `javax` namespaces no longer
|
|
31
|
+
count as jakarta-migration findings. App-level `javax.transaction.Transactional`,
|
|
32
|
+
`javax.annotation.PostConstruct`, etc. are still flagged (true positives kept).
|
|
33
|
+
Matching is by fully-qualified import prefix, not simple name or partial token.
|
|
34
|
+
- **`readiness_score` no longer sinks on an N/A dimension.** A dimension that does
|
|
35
|
+
not apply (e.g. Hibernate on a repo with no Hibernate) is reported as N/A
|
|
36
|
+
(`score: null`), never as 0. New `applicable_dimensions{}` and `readiness_note`
|
|
37
|
+
make the aggregate's derivation explicit — read the per-dimension breakdown +
|
|
38
|
+
`blocking_count`, not the single number.
|
|
39
|
+
- **`endpoints` never implies "no API".** When the Spring-MVC surface is empty but
|
|
40
|
+
another REST framework is present (Alfresco WebScripts via `*.desc.xml` +
|
|
41
|
+
`AbstractWebScript`/`DeclarativeWebScript`, JAX-RS, or mapped Servlets),
|
|
42
|
+
`security_model` becomes `"undetermined"` (not `"unknown"`), a
|
|
43
|
+
`non_spring_rest_surface{}` block is emitted, and a warning states the surface is
|
|
44
|
+
unmodeled — so `total: 0` is never read as "this app exposes no endpoints".
|
|
45
|
+
|
|
46
|
+
### Tests
|
|
47
|
+
|
|
48
|
+
- New `tests/test_falsepositive_fixes.py` — positive **and** negative fixtures per
|
|
49
|
+
bug (phantom comment-only repo, project-owned `Dialect`/`Criteria`/`Interceptor`
|
|
50
|
+
classes, `javax.transaction.xa`, WebScripts) alongside the true positives that
|
|
51
|
+
must be preserved (real Hibernate, `javax.transaction.Transactional`,
|
|
52
|
+
`@RestController`). Full suite: 2827 passed.
|
|
53
|
+
|
|
3
54
|
## [1.63.0] — 2026-06-29
|
|
4
55
|
|
|
5
56
|
### Added — actionable Hibernate rewrite targets (`migrate-check`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.64.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
|
|
@@ -377,6 +377,8 @@ Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.)
|
|
|
377
377
|
|
|
378
378
|
**Functional / WebFlux routing (honest limitation).** Routes registered via the functional DSL — `route().GET("/path", handler)` / `RouterFunction` / `CustomEndpoint`, common in reactive Spring apps — are **not** modeled (their real paths depend on `nest()`/group-version prefixes that can't be resolved statically). Rather than emit partial paths that would mislead, the output reports a `functional_routing` block (`files`, `route_registrations`, `modeled: false`) plus a warning. When the annotation surface is empty but functional routes exist, the warning explicitly tells you not to read it as "no endpoints". Annotation-based (MVC/JAX-RS) repos are unaffected.
|
|
379
379
|
|
|
380
|
+
**Non-Spring REST frameworks (never "no API").** When the Spring-MVC surface is empty but the repo exposes REST another way — Alfresco WebScripts (`*.desc.xml` descriptors + classes extending `AbstractWebScript`/`DeclarativeWebScript`), JAX-RS, or mapped Servlets — `security_model` is reported as `"undetermined"` (never `"unknown"`, which downstream reads as "no security"), a `non_spring_rest_surface` block names the detected frameworks, and a warning states the surface is unmodeled. `total: 0` therefore can never be read as "this application exposes no API".
|
|
381
|
+
|
|
380
382
|
**Custom security annotations.** Enterprise repos often guard endpoints with a bespoke annotation instead of `@PreAuthorize`/`@Secured`. Drop a `sourcecode.config.json` at the repo root to teach the scanner about it — otherwise those endpoints report `policy: "none_detected"`:
|
|
381
383
|
|
|
382
384
|
```json
|
|
@@ -663,7 +665,17 @@ A Hibernate major upgrade is **not** a single dependency bump for systems that u
|
|
|
663
665
|
dynamic persistence. `migrate-check` stratifies Hibernate exposure into four
|
|
664
666
|
independent migration domains — never one aggregated score — and emits **actionable,
|
|
665
667
|
machine-readable rewrite targets** so a migration agent can consume the output
|
|
666
|
-
directly instead of re-parsing the repo. Sub-`schema_version`: `2.
|
|
668
|
+
directly instead of re-parsing the repo. Sub-`schema_version`: `2.1`.
|
|
669
|
+
|
|
670
|
+
> **Evidence-gated (no false blockers).** Hibernate detection requires real proof —
|
|
671
|
+
> an `org.hibernate:*` build dependency, a parsed `import org.hibernate.*`, or a
|
|
672
|
+
> parsed `import {jakarta,javax}.persistence.*`. Absent all three,
|
|
673
|
+
> `hibernate.detected` is `false` (no `hibernate_rewrite` headline, no phantom
|
|
674
|
+
> effort). Scanning runs on source with comments + string/char literals stripped, so
|
|
675
|
+
> an `org.hibernate.*` substring in Javadoc or a classpath resource path never
|
|
676
|
+
> triggers a verdict; bare-name SPI matches (`implements XInterceptor`) require an
|
|
677
|
+
> `org.hibernate` import in the same file. The proof is recorded under
|
|
678
|
+
> `hibernate.evidence{}` with a `confidence`.
|
|
667
679
|
|
|
668
680
|
**Four layers** (each on its own risk axis):
|
|
669
681
|
|
|
@@ -704,6 +716,12 @@ Hibernate is an orthogonal rewrite axis, so it does not sink the headline
|
|
|
704
716
|
`readiness_score`; instead, in a rewrite zone the top-level `headline_blocker` is set
|
|
705
717
|
to `"hibernate_rewrite"` so a reader of the headline score is not misled.
|
|
706
718
|
|
|
719
|
+
A dimension that does not apply (e.g. Hibernate on a repo with no Hibernate) is
|
|
720
|
+
reported as **N/A** (`score: null`), never as `0`. The top-level
|
|
721
|
+
`applicable_dimensions{}` records which dimensions apply and `readiness_note`
|
|
722
|
+
states that `readiness_score` is a derived aggregate over applicable dimensions
|
|
723
|
+
only — for decisions, read the per-dimension breakdown + `blocking_count`.
|
|
724
|
+
|
|
707
725
|
```bash
|
|
708
726
|
# inspect only the Hibernate rewrite targets
|
|
709
727
|
sourcecode migrate-check . --format json | jq '.hibernate.rewrite_targets[]'
|
|
@@ -339,6 +339,8 @@ Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.)
|
|
|
339
339
|
|
|
340
340
|
**Functional / WebFlux routing (honest limitation).** Routes registered via the functional DSL — `route().GET("/path", handler)` / `RouterFunction` / `CustomEndpoint`, common in reactive Spring apps — are **not** modeled (their real paths depend on `nest()`/group-version prefixes that can't be resolved statically). Rather than emit partial paths that would mislead, the output reports a `functional_routing` block (`files`, `route_registrations`, `modeled: false`) plus a warning. When the annotation surface is empty but functional routes exist, the warning explicitly tells you not to read it as "no endpoints". Annotation-based (MVC/JAX-RS) repos are unaffected.
|
|
341
341
|
|
|
342
|
+
**Non-Spring REST frameworks (never "no API").** When the Spring-MVC surface is empty but the repo exposes REST another way — Alfresco WebScripts (`*.desc.xml` descriptors + classes extending `AbstractWebScript`/`DeclarativeWebScript`), JAX-RS, or mapped Servlets — `security_model` is reported as `"undetermined"` (never `"unknown"`, which downstream reads as "no security"), a `non_spring_rest_surface` block names the detected frameworks, and a warning states the surface is unmodeled. `total: 0` therefore can never be read as "this application exposes no API".
|
|
343
|
+
|
|
342
344
|
**Custom security annotations.** Enterprise repos often guard endpoints with a bespoke annotation instead of `@PreAuthorize`/`@Secured`. Drop a `sourcecode.config.json` at the repo root to teach the scanner about it — otherwise those endpoints report `policy: "none_detected"`:
|
|
343
345
|
|
|
344
346
|
```json
|
|
@@ -625,7 +627,17 @@ A Hibernate major upgrade is **not** a single dependency bump for systems that u
|
|
|
625
627
|
dynamic persistence. `migrate-check` stratifies Hibernate exposure into four
|
|
626
628
|
independent migration domains — never one aggregated score — and emits **actionable,
|
|
627
629
|
machine-readable rewrite targets** so a migration agent can consume the output
|
|
628
|
-
directly instead of re-parsing the repo. Sub-`schema_version`: `2.
|
|
630
|
+
directly instead of re-parsing the repo. Sub-`schema_version`: `2.1`.
|
|
631
|
+
|
|
632
|
+
> **Evidence-gated (no false blockers).** Hibernate detection requires real proof —
|
|
633
|
+
> an `org.hibernate:*` build dependency, a parsed `import org.hibernate.*`, or a
|
|
634
|
+
> parsed `import {jakarta,javax}.persistence.*`. Absent all three,
|
|
635
|
+
> `hibernate.detected` is `false` (no `hibernate_rewrite` headline, no phantom
|
|
636
|
+
> effort). Scanning runs on source with comments + string/char literals stripped, so
|
|
637
|
+
> an `org.hibernate.*` substring in Javadoc or a classpath resource path never
|
|
638
|
+
> triggers a verdict; bare-name SPI matches (`implements XInterceptor`) require an
|
|
639
|
+
> `org.hibernate` import in the same file. The proof is recorded under
|
|
640
|
+
> `hibernate.evidence{}` with a `confidence`.
|
|
629
641
|
|
|
630
642
|
**Four layers** (each on its own risk axis):
|
|
631
643
|
|
|
@@ -666,6 +678,12 @@ Hibernate is an orthogonal rewrite axis, so it does not sink the headline
|
|
|
666
678
|
`readiness_score`; instead, in a rewrite zone the top-level `headline_blocker` is set
|
|
667
679
|
to `"hibernate_rewrite"` so a reader of the headline score is not misled.
|
|
668
680
|
|
|
681
|
+
A dimension that does not apply (e.g. Hibernate on a repo with no Hibernate) is
|
|
682
|
+
reported as **N/A** (`score: null`), never as `0`. The top-level
|
|
683
|
+
`applicable_dimensions{}` records which dimensions apply and `readiness_note`
|
|
684
|
+
states that `readiness_score` is a derived aggregate over applicable dimensions
|
|
685
|
+
only — for decisions, read the per-dimension breakdown + `blocking_count`.
|
|
686
|
+
|
|
669
687
|
```bash
|
|
670
688
|
# inspect only the Hibernate rewrite targets
|
|
671
689
|
sourcecode migrate-check . --format json | jq '.hibernate.rewrite_targets[]'
|
|
@@ -36,7 +36,7 @@ from typing import Optional
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
# Sub-schema version for the `hibernate` output section. Bump on shape changes.
|
|
39
|
-
HIBERNATE_SCHEMA_VERSION = "2.
|
|
39
|
+
HIBERNATE_SCHEMA_VERSION = "2.1"
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
# ---------------------------------------------------------------------------
|
|
@@ -107,14 +107,24 @@ _JPA_DEPRECATED_RE = re.compile(
|
|
|
107
107
|
)
|
|
108
108
|
|
|
109
109
|
# Layer 2 — JPA Criteria API + legacy Hibernate Criteria.
|
|
110
|
+
# BUG #1: dropped over-generic tokens (\bPredicate\b, \bRoot\s*<, \bCriterion\b,
|
|
111
|
+
# \bConjunction\b, \bDisjunction\b) that collide with project-owned domain classes
|
|
112
|
+
# (e.g. Alfresco's own query model has Predicate / Conjunction / Disjunction /
|
|
113
|
+
# Criterion classes with NO Hibernate dependency). Real JPA Criteria code always
|
|
114
|
+
# carries CriteriaBuilder / CriteriaQuery, so detection is preserved via those.
|
|
110
115
|
_CRITERIA_JPA_RE = re.compile(
|
|
111
116
|
r"\bCriteriaBuilder\b|\bCriteriaQuery\b|\bCriteriaUpdate\b|"
|
|
112
|
-
r"\bCriteriaDelete\b
|
|
117
|
+
r"\bCriteriaDelete\b|persistence\.criteria"
|
|
113
118
|
)
|
|
119
|
+
# Legacy Hibernate Criteria. org.hibernate.Criteria / org.hibernate.criterion.*
|
|
120
|
+
# are FQN-anchored (safe). The bare-name tokens (createCriteria, Restrictions,
|
|
121
|
+
# Projections, DetachedCriteria) only count when the file actually imports
|
|
122
|
+
# org.hibernate (see _file_imports_hibernate gate) — they are otherwise common
|
|
123
|
+
# enough to collide with unrelated builder APIs.
|
|
114
124
|
_CRITERIA_LEGACY_RE = re.compile(
|
|
115
|
-
r"org\.hibernate\.Criteria\b
|
|
116
|
-
r"\
|
|
117
|
-
r"\
|
|
125
|
+
r"org\.hibernate\.Criteria\b|org\.hibernate\.criterion\b|"
|
|
126
|
+
r"\.createCriteria\s*\(|\bDetachedCriteria\b|"
|
|
127
|
+
r"\bRestrictions\.|\bProjections\."
|
|
118
128
|
)
|
|
119
129
|
|
|
120
130
|
# Layer 3 — HQL / native string queries. createQuery requires a string literal so
|
|
@@ -131,20 +141,118 @@ _HQL_CONCAT_RE = re.compile(
|
|
|
131
141
|
)
|
|
132
142
|
|
|
133
143
|
# Layer 4 — Hibernate SPI / internal API (CRITICAL blocker).
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
144
|
+
# BUG #1: split into FQN-anchored matches (proof of Hibernate origin — always
|
|
145
|
+
# counted) and bare-name matches (e.g. `implements XInterceptor`, `extends
|
|
146
|
+
# XEventListener`) that collide with project-owned AOP interceptors / event
|
|
147
|
+
# listeners. Bare-name matches only count when the file actually imports
|
|
148
|
+
# org.hibernate (see _file_imports_hibernate gate).
|
|
149
|
+
_SPI_FQN_RE = re.compile(
|
|
137
150
|
r"\borg\.hibernate\.(?:type|engine|internal|persister|metamodel|"
|
|
138
|
-
r"boot\.spi|boot\.internal|event|tuple|property\.access|loader|sql\.ast
|
|
151
|
+
r"boot\.spi|boot\.internal|event|tuple|property\.access|loader|sql\.ast|"
|
|
152
|
+
r"usertype)\b|"
|
|
139
153
|
r"\bEmptyInterceptor\b|"
|
|
154
|
+
r"\bSessionFactoryImpl\b|\bSessionImplementor\b|"
|
|
155
|
+
r"\bSharedSessionContractImplementor\b"
|
|
156
|
+
)
|
|
157
|
+
_SPI_SIMPLE_RE = re.compile(
|
|
158
|
+
r"\b(?:implements|extends)\s+\w*(?:UserType|CompositeUserType|UserCollectionType)\b|"
|
|
140
159
|
r"\bimplements\s+\w*Interceptor\b|"
|
|
141
160
|
r"\b(?:implements|extends)\s+\w*EventListener\b|"
|
|
142
|
-
r"\bSessionFactoryImpl\b|\bSessionImplementor\b|"
|
|
143
|
-
r"\bSharedSessionContractImplementor\b|"
|
|
144
161
|
r"\bsetPropertyAccessStrategy\b|\bPropertyAccessStrategy\b|"
|
|
145
162
|
r"\bImplicitNamingStrategy\b|\bPhysicalNamingStrategy\b"
|
|
146
163
|
)
|
|
147
164
|
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# BUG #1 — comment/string stripping + real-import & dependency evidence
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
#
|
|
169
|
+
# A Hibernate 5→6 verdict is a high-cost blocker. It must be PROVEN from resolved
|
|
170
|
+
# AST signals, never from a substring inside a comment, Javadoc, string literal,
|
|
171
|
+
# or classpath resource path. Two guards enforce this:
|
|
172
|
+
# 1. _strip_comments_strings() — all pattern scanning runs on code with comments
|
|
173
|
+
# and string/char literal CONTENT removed (quotes/newlines preserved so line
|
|
174
|
+
# numbers and call-shape tokens survive).
|
|
175
|
+
# 2. _collect_hibernate_evidence() — detection only proceeds when there is real
|
|
176
|
+
# evidence: an org.hibernate:* build dependency, a parsed `import org.hibernate.*`,
|
|
177
|
+
# or a parsed `import {jakarta,javax}.persistence.*` (JPA provider in play).
|
|
178
|
+
|
|
179
|
+
_JAVA_TOKEN_RE = re.compile(
|
|
180
|
+
r"(//[^\n]*)" # line comment
|
|
181
|
+
r"|(/\*.*?\*/)" # block comment
|
|
182
|
+
r"|(\"(?:\\.|[^\"\\\n])*\")" # string literal
|
|
183
|
+
r"|('(?:\\.|[^'\\\n])*')", # char literal
|
|
184
|
+
re.DOTALL,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _strip_comments_strings(src: str) -> str:
|
|
189
|
+
"""Remove comment and string/char-literal CONTENT, preserving line count.
|
|
190
|
+
|
|
191
|
+
Line comments are blanked in place (the trailing newline is outside the match,
|
|
192
|
+
so line numbers are unaffected). Block comments are replaced by an equal number
|
|
193
|
+
of newlines. String/char literals collapse to empty `""` / `''` tokens so that
|
|
194
|
+
call-shape detection (e.g. createQuery("...")) and concatenation (`"" + x`) still
|
|
195
|
+
match, but no pattern can match a substring that lived inside a literal.
|
|
196
|
+
"""
|
|
197
|
+
def _repl(m: re.Match) -> str:
|
|
198
|
+
if m.group(1) is not None:
|
|
199
|
+
return ""
|
|
200
|
+
if m.group(2) is not None:
|
|
201
|
+
return "\n" * m.group(2).count("\n")
|
|
202
|
+
if m.group(3) is not None:
|
|
203
|
+
return '""'
|
|
204
|
+
if m.group(4) is not None:
|
|
205
|
+
return "''"
|
|
206
|
+
return m.group(0)
|
|
207
|
+
return _JAVA_TOKEN_RE.sub(_repl, src)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Real `import org.hibernate.*` — anchored to an import statement, not a substring.
|
|
211
|
+
_REAL_HIBERNATE_IMPORT_RE = re.compile(
|
|
212
|
+
r"^[ \t]*import\s+(?:static\s+)?org\.hibernate\.", re.MULTILINE
|
|
213
|
+
)
|
|
214
|
+
# Real JPA import (jakarta or javax persistence) — a JPA provider is in play.
|
|
215
|
+
_JPA_IMPORT_RE = re.compile(
|
|
216
|
+
r"^[ \t]*import\s+(?:static\s+)?(?:jakarta|javax)\.persistence\.", re.MULTILINE
|
|
217
|
+
)
|
|
218
|
+
# org.hibernate:* coordinate in a Maven/Gradle build descriptor.
|
|
219
|
+
_HIBERNATE_DEP_RE = re.compile(
|
|
220
|
+
r"<groupId>\s*org\.hibernate(?:\.orm)?\s*</groupId>"
|
|
221
|
+
r"|['\"]org\.hibernate(?:\.orm)?:",
|
|
222
|
+
re.IGNORECASE,
|
|
223
|
+
)
|
|
224
|
+
_BUILD_FILE_NAMES: tuple[str, ...] = ("pom.xml", "build.gradle", "build.gradle.kts")
|
|
225
|
+
_SKIP_BUILD_SCAN_DIRS: frozenset[str] = frozenset(
|
|
226
|
+
{"target", "build", ".git", ".gradle", "node_modules", "__pycache__", "out", "dist", "bin"}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _file_imports_hibernate(stripped_source: str) -> bool:
|
|
231
|
+
return bool(_REAL_HIBERNATE_IMPORT_RE.search(stripped_source))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _scan_build_dependency(root: Path) -> tuple[bool, list[str]]:
|
|
235
|
+
"""Return (org.hibernate dependency present, sample coordinate strings)."""
|
|
236
|
+
import os
|
|
237
|
+
samples: list[str] = []
|
|
238
|
+
found = False
|
|
239
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
240
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_BUILD_SCAN_DIRS]
|
|
241
|
+
for fname in filenames:
|
|
242
|
+
if fname not in _BUILD_FILE_NAMES:
|
|
243
|
+
continue
|
|
244
|
+
try:
|
|
245
|
+
text = (Path(dirpath) / fname).read_text(encoding="utf-8", errors="replace")
|
|
246
|
+
except OSError:
|
|
247
|
+
continue
|
|
248
|
+
text = _strip_comments_strings(text) if fname.endswith((".gradle", ".kts")) else text
|
|
249
|
+
for m in _HIBERNATE_DEP_RE.finditer(text):
|
|
250
|
+
found = True
|
|
251
|
+
snippet = m.group(0).strip()
|
|
252
|
+
if snippet not in samples and len(samples) < 5:
|
|
253
|
+
samples.append(snippet)
|
|
254
|
+
return found, samples
|
|
255
|
+
|
|
148
256
|
# Escalation markers — dynamic / reflection-based persistence construction.
|
|
149
257
|
_ABSTRACTION_CLASS_RE = re.compile(
|
|
150
258
|
r"\b(DynamicEntityDao|DynamicEntityDaoImpl|BasicPersistenceModule|"
|
|
@@ -294,11 +402,16 @@ class HibernateStratification:
|
|
|
294
402
|
total_effort_range_days: dict = field(default_factory=dict)
|
|
295
403
|
effort_model: dict = field(default_factory=dict)
|
|
296
404
|
findings: list[HibernateFinding] = field(default_factory=list)
|
|
405
|
+
# BUG #1: auditable proof behind the detected verdict. confidence is "high"
|
|
406
|
+
# when backed by a build dependency or a parsed org.hibernate import, "none"
|
|
407
|
+
# when detection was vetoed for lack of evidence.
|
|
408
|
+
evidence: dict = field(default_factory=dict)
|
|
297
409
|
|
|
298
410
|
def to_dict(self) -> dict:
|
|
299
411
|
return {
|
|
300
412
|
"schema_version": HIBERNATE_SCHEMA_VERSION,
|
|
301
413
|
"detected": self.detected,
|
|
414
|
+
"evidence": self.evidence,
|
|
302
415
|
"classification": self.classification,
|
|
303
416
|
"classification_label": self.classification_label,
|
|
304
417
|
"stratified": True,
|
|
@@ -607,15 +720,71 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
607
720
|
concat_query_global = False
|
|
608
721
|
incompatible: set[str] = set()
|
|
609
722
|
|
|
723
|
+
# ── BUG #1: evidence gate ───────────────────────────────────────────────
|
|
724
|
+
# Pre-read each Java source ONCE, strip comments + string/char literals, and
|
|
725
|
+
# record real-import evidence. Detection only proceeds when there is genuine
|
|
726
|
+
# proof of Hibernate/JPA — never a substring inside a comment or a classpath
|
|
727
|
+
# resource path (e.g. Alfresco's "org.hibernate.dialect.MySQLInnoDBDialect"
|
|
728
|
+
# appears only in Javadoc + iBatis resource directory names).
|
|
729
|
+
parsed: list[tuple[str, str, bool]] = [] # (rel_path, stripped_source, imports_hibernate)
|
|
730
|
+
any_hibernate_import = False
|
|
731
|
+
any_jpa_import = False
|
|
732
|
+
hibernate_import_sample: Optional[str] = None
|
|
610
733
|
for rel_path in sorted(file_paths):
|
|
611
734
|
if not rel_path.endswith(".java"):
|
|
612
735
|
continue
|
|
613
736
|
abs_path = root / rel_path
|
|
614
737
|
try:
|
|
615
|
-
|
|
738
|
+
raw = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
616
739
|
except OSError:
|
|
617
740
|
continue
|
|
741
|
+
source = _strip_comments_strings(raw)
|
|
742
|
+
imports_hib = _file_imports_hibernate(source)
|
|
743
|
+
if imports_hib:
|
|
744
|
+
any_hibernate_import = True
|
|
745
|
+
if hibernate_import_sample is None:
|
|
746
|
+
hm = _REAL_HIBERNATE_IMPORT_RE.search(source)
|
|
747
|
+
# capture the full import line for the evidence record
|
|
748
|
+
line = source[hm.start():].split("\n", 1)[0].strip().rstrip(";")
|
|
749
|
+
hibernate_import_sample = line
|
|
750
|
+
if _JPA_IMPORT_RE.search(source):
|
|
751
|
+
any_jpa_import = True
|
|
752
|
+
parsed.append((rel_path, source, imports_hib))
|
|
753
|
+
|
|
754
|
+
# No Java sources → nothing to stratify; skip the (potentially large) build walk.
|
|
755
|
+
if not parsed:
|
|
756
|
+
strat = HibernateStratification()
|
|
757
|
+
strat.detected = False
|
|
758
|
+
strat.classification = CLASS_NONE
|
|
759
|
+
strat.classification_label = _CLASS_LABELS[CLASS_NONE]
|
|
760
|
+
strat.evidence = {"dependency_present": False, "dependency_coordinates": [],
|
|
761
|
+
"hibernate_import_present": False, "hibernate_import_sample": None,
|
|
762
|
+
"jpa_import_present": False, "confidence": "none"}
|
|
763
|
+
return strat
|
|
764
|
+
|
|
765
|
+
dep_present, dep_samples = _scan_build_dependency(root)
|
|
618
766
|
|
|
767
|
+
has_evidence = dep_present or any_hibernate_import or any_jpa_import
|
|
768
|
+
evidence: dict = {
|
|
769
|
+
"dependency_present": dep_present,
|
|
770
|
+
"dependency_coordinates": dep_samples,
|
|
771
|
+
"hibernate_import_present": any_hibernate_import,
|
|
772
|
+
"hibernate_import_sample": hibernate_import_sample,
|
|
773
|
+
"jpa_import_present": any_jpa_import,
|
|
774
|
+
"confidence": "high" if has_evidence else "none",
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if not has_evidence:
|
|
778
|
+
# No resolved Hibernate/JPA evidence → never a blocker. Any org.hibernate
|
|
779
|
+
# substring lives in comments/strings/classpath paths only.
|
|
780
|
+
strat = HibernateStratification()
|
|
781
|
+
strat.detected = False
|
|
782
|
+
strat.classification = CLASS_NONE
|
|
783
|
+
strat.classification_label = _CLASS_LABELS[CLASS_NONE]
|
|
784
|
+
strat.evidence = evidence
|
|
785
|
+
return strat
|
|
786
|
+
|
|
787
|
+
for rel_path, source, file_imports_hib in parsed:
|
|
619
788
|
nl = _line_index(source)
|
|
620
789
|
src_lines = source.split("\n")
|
|
621
790
|
classes, methods = _index_symbols(source, nl)
|
|
@@ -736,7 +905,16 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
736
905
|
"Hibernate 6 HQL parser changes")
|
|
737
906
|
|
|
738
907
|
# ── Layer 4: Hibernate SPI / internal ──────────────────────────────
|
|
739
|
-
|
|
908
|
+
# FQN-anchored SPI matches are proof on their own. Bare-name matches
|
|
909
|
+
# (`implements XInterceptor`, `extends XEventListener`) only count when
|
|
910
|
+
# the file actually imports org.hibernate — otherwise they are the
|
|
911
|
+
# project's own AOP interceptors / event listeners (BUG #1).
|
|
912
|
+
spi_matches = list(_SPI_FQN_RE.finditer(source))
|
|
913
|
+
if file_imports_hib:
|
|
914
|
+
spi_matches = sorted(
|
|
915
|
+
spi_matches + list(_SPI_SIMPLE_RE.finditer(source)),
|
|
916
|
+
key=lambda m: m.start(),
|
|
917
|
+
)
|
|
740
918
|
if spi_matches:
|
|
741
919
|
spi_global = True
|
|
742
920
|
mod["has_spi"] = True
|
|
@@ -780,6 +958,7 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
780
958
|
|
|
781
959
|
strat = HibernateStratification()
|
|
782
960
|
strat.findings = findings
|
|
961
|
+
strat.evidence = evidence
|
|
783
962
|
strat.rewrite_targets = sorted(
|
|
784
963
|
targets, key=lambda t: (t.source_file, t.line_start, t.layer, t.id)
|
|
785
964
|
)
|
|
@@ -789,6 +968,8 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
789
968
|
)[:25]
|
|
790
969
|
|
|
791
970
|
if not findings:
|
|
971
|
+
# Evidence existed (dependency/import) but no concrete migration pattern
|
|
972
|
+
# fired — Hibernate present yet nothing in the rewrite/upgrade surface.
|
|
792
973
|
strat.detected = False
|
|
793
974
|
strat.classification = CLASS_NONE
|
|
794
975
|
strat.classification_label = _CLASS_LABELS[CLASS_NONE]
|
|
@@ -541,6 +541,47 @@ _ALL_RULES: list[_Rule] = (
|
|
|
541
541
|
|
|
542
542
|
SEVERITY_ORDER: dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
543
543
|
|
|
544
|
+
# BUG #2 — javax.* packages that are JDK / permanent JSR namespaces and do NOT
|
|
545
|
+
# migrate to jakarta. A jakarta-migration rule matching `javax.transaction` or
|
|
546
|
+
# `javax.annotation` must NOT fire on these FQN prefixes (e.g. javax.transaction.xa
|
|
547
|
+
# is the Java SE java.transaction.xa module; javax.annotation.processing is the
|
|
548
|
+
# annotation-processing API). The allowlist is keyed on fully-qualified import
|
|
549
|
+
# prefixes, never on simple class names or partial tokens.
|
|
550
|
+
_JAKARTA_NO_MIGRATE_PREFIXES: tuple[str, ...] = (
|
|
551
|
+
"javax.transaction.xa.",
|
|
552
|
+
"javax.annotation.processing.",
|
|
553
|
+
"javax.xml.parsers.",
|
|
554
|
+
"javax.xml.transform.",
|
|
555
|
+
"javax.xml.xpath.",
|
|
556
|
+
"javax.xml.stream.",
|
|
557
|
+
"javax.xml.datatype.",
|
|
558
|
+
"javax.xml.namespace.",
|
|
559
|
+
"javax.xml.validation.",
|
|
560
|
+
"javax.xml.catalog.",
|
|
561
|
+
"javax.xml.crypto.",
|
|
562
|
+
"javax.sql.",
|
|
563
|
+
"javax.management.",
|
|
564
|
+
"javax.naming.",
|
|
565
|
+
"javax.crypto.",
|
|
566
|
+
"javax.net.",
|
|
567
|
+
"javax.security.auth.",
|
|
568
|
+
"javax.security.cert.",
|
|
569
|
+
"javax.security.sasl.",
|
|
570
|
+
"javax.cache.",
|
|
571
|
+
"javax.tools.",
|
|
572
|
+
"javax.imageio.",
|
|
573
|
+
"javax.sound.",
|
|
574
|
+
"javax.print.",
|
|
575
|
+
"javax.accessibility.",
|
|
576
|
+
"javax.swing.",
|
|
577
|
+
"javax.lang.model.",
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _is_no_migrate_javax(fqn: str) -> bool:
|
|
582
|
+
"""True if a javax FQN belongs to a JDK/permanent namespace (no jakarta move)."""
|
|
583
|
+
return any(fqn.startswith(p) for p in _JAKARTA_NO_MIGRATE_PREFIXES)
|
|
584
|
+
|
|
544
585
|
# G-1: cap on total readiness deduction from low-severity (advisory, non-blocking)
|
|
545
586
|
# findings, so optional modernization cleanups cannot collapse the migration-readiness
|
|
546
587
|
# headline on a repo with zero blockers. See MigrationReport.finalize.
|
|
@@ -1061,6 +1102,11 @@ class MigrationReport:
|
|
|
1061
1102
|
# Names the dominant blocker class when one dimension dwarfs the headline
|
|
1062
1103
|
# score (e.g. "hibernate_rewrite") so a reader of readiness_score is not misled.
|
|
1063
1104
|
headline_blocker: Optional[str] = None
|
|
1105
|
+
# BUG #3: which readiness dimensions actually APPLY to this repo. A dimension
|
|
1106
|
+
# that does not apply (e.g. hibernate on a repo with no Hibernate) is N/A and
|
|
1107
|
+
# is excluded from the aggregate — it is never counted as 0. Maps dimension →
|
|
1108
|
+
# {"applicable": bool, "score": int|None, "reason": str}.
|
|
1109
|
+
applicable_dimensions: dict = field(default_factory=dict)
|
|
1064
1110
|
blocking_count: int = 0
|
|
1065
1111
|
estimated_effort_days: float = 0.0
|
|
1066
1112
|
# Tri-state: True = Boot 2 confirmed, False = Boot 3+ confirmed,
|
|
@@ -1142,11 +1188,32 @@ class MigrationReport:
|
|
|
1142
1188
|
# Hibernate is its own rewrite axis (orthogonal to jakarta/Boot3); it does
|
|
1143
1189
|
# NOT sink the headline readiness_score, but is surfaced as a dimension and,
|
|
1144
1190
|
# in a rewrite zone, as the headline_blocker so 62/100 is not read as "easy".
|
|
1145
|
-
|
|
1191
|
+
_hibernate_applies = self.hibernate is not None and self.hibernate.detected
|
|
1192
|
+
if _hibernate_applies:
|
|
1146
1193
|
self.hibernate_readiness = self.hibernate.readiness
|
|
1147
1194
|
if self.hibernate.classification == "rewrite_zone":
|
|
1148
1195
|
self.headline_blocker = "hibernate_rewrite"
|
|
1149
1196
|
|
|
1197
|
+
# BUG #3: declare which dimensions apply. jakarta/boot3/jdk are always
|
|
1198
|
+
# applicable to a Java/Spring repo; hibernate applies only when a real
|
|
1199
|
+
# Hibernate dependency/import was found. An inapplicable dimension is N/A
|
|
1200
|
+
# (score=None) and is excluded from the aggregate — never scored as 0.
|
|
1201
|
+
self.applicable_dimensions = {
|
|
1202
|
+
"jakarta": {"applicable": True, "score": self.jakarta_readiness,
|
|
1203
|
+
"reason": "javax→jakarta namespace migration"},
|
|
1204
|
+
"boot3": {"applicable": True, "score": self.boot3_readiness,
|
|
1205
|
+
"reason": "Spring Boot 2→3 / Security 6 migration"},
|
|
1206
|
+
"jdk_modernization": {"applicable": True, "score": self.jdk_modernization,
|
|
1207
|
+
"reason": "orthogonal JDK modernization debt"},
|
|
1208
|
+
"hibernate": {
|
|
1209
|
+
"applicable": _hibernate_applies,
|
|
1210
|
+
"score": self.hibernate_readiness if _hibernate_applies else None,
|
|
1211
|
+
"reason": ("Hibernate 5→6 rewrite axis"
|
|
1212
|
+
if _hibernate_applies
|
|
1213
|
+
else "N/A — no Hibernate dependency or import detected"),
|
|
1214
|
+
},
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1150
1217
|
self.estimated_effort_days = round(
|
|
1151
1218
|
len(critical_files) * 0.5
|
|
1152
1219
|
+ len(high_files) * 0.25
|
|
@@ -1175,6 +1242,12 @@ class MigrationReport:
|
|
|
1175
1242
|
"boot3_readiness": self.boot3_readiness,
|
|
1176
1243
|
"jdk_modernization": self.jdk_modernization,
|
|
1177
1244
|
"hibernate_readiness": self.hibernate_readiness,
|
|
1245
|
+
"applicable_dimensions": self.applicable_dimensions,
|
|
1246
|
+
"readiness_note": (
|
|
1247
|
+
"readiness_score is a DERIVED aggregate over applicable dimensions only "
|
|
1248
|
+
"(N/A dimensions excluded, never counted as 0). For migration decisions read "
|
|
1249
|
+
"the per-dimension breakdown + blocking_count, not the single number."
|
|
1250
|
+
),
|
|
1178
1251
|
"headline_blocker": self.headline_blocker,
|
|
1179
1252
|
"blocking_count": self.blocking_count,
|
|
1180
1253
|
"estimated_effort_days": self.estimated_effort_days,
|
|
@@ -1256,6 +1329,11 @@ def _scan_file(
|
|
|
1256
1329
|
|
|
1257
1330
|
if rule.import_pattern is not None:
|
|
1258
1331
|
matches = list(rule.import_pattern.finditer(source))
|
|
1332
|
+
# BUG #2: for jakarta-migration rules, drop imports whose FQN belongs
|
|
1333
|
+
# to a JDK/permanent javax namespace (javax.transaction.xa.*,
|
|
1334
|
+
# javax.annotation.processing.*, ...). These do NOT migrate to jakarta.
|
|
1335
|
+
if matches and rule.migration_target == "jakarta":
|
|
1336
|
+
matches = [m for m in matches if not _is_no_migrate_javax(m.group(1).strip())]
|
|
1259
1337
|
if matches:
|
|
1260
1338
|
import_first_line = source[: matches[0].start()].count("\n") + 1
|
|
1261
1339
|
matched_imports = [m.group(1) for m in matches]
|
|
@@ -4011,6 +4011,18 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
4011
4011
|
_fn_route_files: set[str] = set()
|
|
4012
4012
|
_fn_route_count = 0
|
|
4013
4013
|
|
|
4014
|
+
# BUG #4: detect REST surfaces exposed by NON-Spring-MVC frameworks. These are
|
|
4015
|
+
# NOT modeled by the annotation surface, so when the Spring surface is empty we
|
|
4016
|
+
# must still tell the consumer "REST present, just not Spring-annotated" rather
|
|
4017
|
+
# than let them infer "no API". Signal counts per framework.
|
|
4018
|
+
_WEBSCRIPT_RE = _re.compile(
|
|
4019
|
+
r"\bextends\s+\w*(?:AbstractWebScript|DeclarativeWebScript)\b"
|
|
4020
|
+
r"|org\.springframework\.extensions\.webscripts"
|
|
4021
|
+
)
|
|
4022
|
+
_JAXRS_PATH_RE = _re.compile(r"^[ \t]*import\s+(?:jakarta|javax)\.ws\.rs\.", _re.MULTILINE)
|
|
4023
|
+
_SERVLET_RE = _re.compile(r"\bextends\s+\w*HttpServlet\b")
|
|
4024
|
+
_nonspring: dict[str, int] = {"webscripts": 0, "jax_rs": 0, "servlets": 0}
|
|
4025
|
+
|
|
4014
4026
|
for jf in java_files:
|
|
4015
4027
|
try:
|
|
4016
4028
|
source = jf.read_text(encoding="utf-8", errors="replace")
|
|
@@ -4025,6 +4037,12 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
4025
4037
|
if _fn_hits:
|
|
4026
4038
|
_fn_route_files.add(rel)
|
|
4027
4039
|
_fn_route_count += _fn_hits
|
|
4040
|
+
if _WEBSCRIPT_RE.search(source):
|
|
4041
|
+
_nonspring["webscripts"] += 1
|
|
4042
|
+
if _JAXRS_PATH_RE.search(source) and "@Path" in source:
|
|
4043
|
+
_nonspring["jax_rs"] += 1
|
|
4044
|
+
if _SERVLET_RE.search(source):
|
|
4045
|
+
_nonspring["servlets"] += 1
|
|
4028
4046
|
_, symbols, _ = _extract_symbols(source, rel, extra_capture=_extra_capture)
|
|
4029
4047
|
for sym in symbols:
|
|
4030
4048
|
all_symbols.append(sym)
|
|
@@ -4253,6 +4271,58 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
4253
4271
|
"do NOT read it as 'no endpoints'; the app's HTTP surface is unmodeled."
|
|
4254
4272
|
)
|
|
4255
4273
|
result.setdefault("warnings", []).append(_fr_msg)
|
|
4274
|
+
|
|
4275
|
+
# BUG #4: non-Spring-MVC REST frameworks. Count Alfresco WebScript descriptors
|
|
4276
|
+
# (*.desc.xml: <url>/<authentication>/<transaction>) in addition to the Java
|
|
4277
|
+
# signals gathered above.
|
|
4278
|
+
_webscript_descriptors = 0
|
|
4279
|
+
for _df in root.rglob("*.desc.xml"):
|
|
4280
|
+
if "target/" in str(_df).replace("\\", "/"):
|
|
4281
|
+
continue
|
|
4282
|
+
try:
|
|
4283
|
+
_dt = _df.read_text(encoding="utf-8", errors="replace")
|
|
4284
|
+
except OSError:
|
|
4285
|
+
continue
|
|
4286
|
+
if "<url>" in _dt or "<webscript" in _dt:
|
|
4287
|
+
_webscript_descriptors += 1
|
|
4288
|
+
if _webscript_descriptors:
|
|
4289
|
+
_nonspring["webscripts"] += _webscript_descriptors
|
|
4290
|
+
|
|
4291
|
+
_nonspring_total = sum(_nonspring.values())
|
|
4292
|
+
if _nonspring_total:
|
|
4293
|
+
_frameworks = [k for k, v in _nonspring.items() if v]
|
|
4294
|
+
result["non_spring_rest_surface"] = {
|
|
4295
|
+
"detected": True,
|
|
4296
|
+
"frameworks": {k: v for k, v in _nonspring.items() if v},
|
|
4297
|
+
"webscript_descriptors": _webscript_descriptors,
|
|
4298
|
+
"modeled": False,
|
|
4299
|
+
}
|
|
4300
|
+
_fw_names = {
|
|
4301
|
+
"webscripts": "Alfresco WebScripts",
|
|
4302
|
+
"jax_rs": "JAX-RS",
|
|
4303
|
+
"servlets": "mapped Servlets",
|
|
4304
|
+
}
|
|
4305
|
+
_fw_label = ", ".join(_fw_names[f] for f in _frameworks)
|
|
4306
|
+
# When the Spring surface is empty, the security model is genuinely
|
|
4307
|
+
# UNDETERMINED — never "unknown" (which downstream reads as "no security")
|
|
4308
|
+
# and never let "total: 0" be read as "no API".
|
|
4309
|
+
if not endpoints:
|
|
4310
|
+
if security_model in ("unknown", "xml_or_filter_chain"):
|
|
4311
|
+
security_model = "undetermined"
|
|
4312
|
+
result["security_model"] = security_model
|
|
4313
|
+
_msg = (
|
|
4314
|
+
f"REST surface present but NOT Spring-MVC-annotated: detected {_fw_label}. "
|
|
4315
|
+
f"Static endpoint extraction is not supported for these frameworks, so this "
|
|
4316
|
+
f"surface is EMPTY — do NOT read 'total: 0' as 'this app exposes no API'. "
|
|
4317
|
+
f"security_model is 'undetermined', not 'unsecured'."
|
|
4318
|
+
)
|
|
4319
|
+
else:
|
|
4320
|
+
_msg = (
|
|
4321
|
+
f"Additional REST surface present outside Spring MVC: detected {_fw_label}. "
|
|
4322
|
+
f"These endpoints are NOT modeled here; the count covers Spring-MVC-annotated "
|
|
4323
|
+
f"endpoints only."
|
|
4324
|
+
)
|
|
4325
|
+
result.setdefault("warnings", []).append(_msg)
|
|
4256
4326
|
return result
|
|
4257
4327
|
|
|
4258
4328
|
|
|
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
|