sourcecode 1.65.0__tar.gz → 1.67.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.
Files changed (122) hide show
  1. {sourcecode-1.65.0 → sourcecode-1.67.0}/.gitignore +3 -1
  2. {sourcecode-1.65.0 → sourcecode-1.67.0}/CHANGELOG.md +83 -0
  3. {sourcecode-1.65.0 → sourcecode-1.67.0}/PKG-INFO +16 -4
  4. {sourcecode-1.65.0 → sourcecode-1.67.0}/README.md +15 -3
  5. {sourcecode-1.65.0 → sourcecode-1.67.0}/pyproject.toml +1 -1
  6. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/__init__.py +1 -1
  7. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/cli.py +113 -7
  8. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/context_summarizer.py +5 -1
  9. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/dependency_analyzer.py +50 -4
  10. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/orchestrator.py +1 -1
  11. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/registry.py +3 -3
  12. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/server.py +4 -4
  13. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/migrate_check.py +232 -83
  14. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/path_filters.py +31 -0
  15. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/serializer.py +54 -6
  16. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/summarizer.py +1 -1
  17. {sourcecode-1.65.0 → sourcecode-1.67.0}/.github/workflows/build-windows.yml +0 -0
  18. {sourcecode-1.65.0 → sourcecode-1.67.0}/.ruff.toml +0 -0
  19. {sourcecode-1.65.0 → sourcecode-1.67.0}/CONTRIBUTING.md +0 -0
  20. {sourcecode-1.65.0 → sourcecode-1.67.0}/LICENSE +0 -0
  21. {sourcecode-1.65.0 → sourcecode-1.67.0}/SECURITY.md +0 -0
  22. {sourcecode-1.65.0 → sourcecode-1.67.0}/raw +0 -0
  23. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/adaptive_scanner.py +0 -0
  24. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/architecture_analyzer.py +0 -0
  25. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/architecture_summary.py +0 -0
  26. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/ast_extractor.py +0 -0
  27. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/cache.py +0 -0
  28. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/canonical_ir.py +0 -0
  29. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/cir_graphs.py +0 -0
  30. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/classifier.py +0 -0
  31. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  32. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/confidence_analyzer.py +0 -0
  33. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/context_scorer.py +0 -0
  34. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/contract_model.py +0 -0
  35. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/contract_pipeline.py +0 -0
  36. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/coverage_parser.py +0 -0
  37. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/__init__.py +0 -0
  38. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/base.py +0 -0
  39. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  40. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/dart.py +0 -0
  41. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/dotnet.py +0 -0
  42. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/elixir.py +0 -0
  43. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/go.py +0 -0
  44. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/heuristic.py +0 -0
  45. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/hybrid.py +0 -0
  46. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/java.py +0 -0
  47. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  48. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/nodejs.py +0 -0
  49. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/parsers.py +0 -0
  50. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/php.py +0 -0
  51. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/project.py +0 -0
  52. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/python.py +0 -0
  53. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/ruby.py +0 -0
  54. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/rust.py +0 -0
  55. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/systems.py +0 -0
  56. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/terraform.py +0 -0
  57. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/detectors/tooling.py +0 -0
  58. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/doc_analyzer.py +0 -0
  59. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  60. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/env_analyzer.py +0 -0
  61. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/error_schema.py +0 -0
  62. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/explain.py +0 -0
  63. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/file_chunker.py +0 -0
  64. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/file_classifier.py +0 -0
  65. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/flow_analyzer.py +0 -0
  66. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/format_contract.py +0 -0
  67. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/fqn_utils.py +0 -0
  68. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/git_analyzer.py +0 -0
  69. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/graph_analyzer.py +0 -0
  70. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/hibernate_strat.py +0 -0
  71. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/integration_detector.py +0 -0
  72. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/license.py +0 -0
  73. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/__init__.py +0 -0
  74. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  75. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  76. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  77. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  78. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  79. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp/runner.py +0 -0
  80. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/mcp_nudge.py +0 -0
  81. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/metrics_analyzer.py +0 -0
  82. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/openapi_surface.py +0 -0
  83. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/output_budget.py +0 -0
  84. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  85. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/pr_impact.py +0 -0
  86. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/prepare_context.py +0 -0
  87. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/progress.py +0 -0
  88. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/ranking_engine.py +0 -0
  89. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/redactor.py +0 -0
  90. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/relevance_scorer.py +0 -0
  91. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/rename_refactor.py +0 -0
  92. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/repo_classifier.py +0 -0
  93. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/repository_ir.py +0 -0
  94. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/ris.py +0 -0
  95. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/runtime_classifier.py +0 -0
  96. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/scanner.py +0 -0
  97. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/schema.py +0 -0
  98. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/security_config.py +0 -0
  99. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/semantic_analyzer.py +0 -0
  100. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/spring_event_topology.py +0 -0
  101. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/spring_findings.py +0 -0
  102. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/spring_impact.py +0 -0
  103. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/spring_model.py +0 -0
  104. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/spring_security_audit.py +0 -0
  105. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/spring_semantic.py +0 -0
  106. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  107. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/telemetry/__init__.py +0 -0
  108. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/telemetry/config.py +0 -0
  109. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/telemetry/consent.py +0 -0
  110. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/telemetry/events.py +0 -0
  111. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/telemetry/filters.py +0 -0
  112. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/telemetry/transport.py +0 -0
  113. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/tree_utils.py +0 -0
  114. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/validation_surface.py +0 -0
  115. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/version_check.py +0 -0
  116. {sourcecode-1.65.0 → sourcecode-1.67.0}/src/sourcecode/workspace.py +0 -0
  117. {sourcecode-1.65.0 → sourcecode-1.67.0}/supabase/functions/README.md +0 -0
  118. {sourcecode-1.65.0 → sourcecode-1.67.0}/supabase/functions/get-license/index.ts +0 -0
  119. {sourcecode-1.65.0 → sourcecode-1.67.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  120. {sourcecode-1.65.0 → sourcecode-1.67.0}/supabase/functions/telemetry/index.ts +0 -0
  121. {sourcecode-1.65.0 → sourcecode-1.67.0}/supabase/sql/license_event_ordering.sql +0 -0
  122. {sourcecode-1.65.0 → sourcecode-1.67.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -36,4 +36,6 @@ Thumbs.db
36
36
  .planning
37
37
 
38
38
  # Supabase local temp
39
- supabase/.temp/
39
+ supabase/.temp/
40
+ # Internal agent playbook (not distributed)
41
+ docs/PLAYBOOK_V2.md
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.67.0] — 2026-06-30
4
+
5
+ ### Fixed — framing accuracy on non-Spring repos (Apache OFBiz field test)
6
+
7
+ A real audit of **Apache OFBiz** (release18.12, ~326k LOC, own framework + Service
8
+ Engine + widgets, **no Spring**) exposed seven defects where the tool collapsed
9
+ "absence of evidence" into high-confidence false framings — a less-critical LLM
10
+ consumer would have emitted a FALSE report (recommending a non-existent migration).
11
+ Guiding principle applied throughout: **degrade to N/A, never to a manufactured 0 or
12
+ 100; declare gaps instead of inventing certainty.** New golden fixture:
13
+ `tests/test_ofbiz_framing_regression.py`.
14
+
15
+ - **`spring-test` no longer implies runtime Spring (BUG #1/#2).** Spring presence is
16
+ now scope- and artifact-aware: a test support library (`spring-test`,
17
+ `spring-boot-*-test`, `spring-security-test`) — even declared under a legacy
18
+ `compile` block — and test-tree `org.springframework.test` imports are classified
19
+ `spring_test_only`, never runtime Spring. `migrate-check` reports `spring_present`
20
+ (runtime) vs `spring_test_only` separately.
21
+ - **No phantom `boot3 0/100` dimension (BUG #3).** The Boot 2→3 axis is `applicable`
22
+ only with runtime Spring; otherwise it is N/A (`score: null`) and excluded from the
23
+ aggregate — it can no longer drag the readiness headline to 0.
24
+ - **Applicability gating + N/A readiness (BUG #4).** `jakarta` is applicable only with
25
+ positive evidence (migratable `javax.*` imports, or the `jakarta.*` namespace already
26
+ adopted); a repo with no Spring, no migratable `javax.*`, and no Hibernate 5 now
27
+ reports `readiness_score: null` ("N/A — no migration target detected") instead of a
28
+ manufactured 100. A valid `javax.servlet` surface is kept and scored on the **Jakarta
29
+ EE / Tomcat 10** axis, not mislabeled "Spring Boot 3".
30
+ - **`modernize` no longer reports framework-dispatched classes as dead code (BUG #5).**
31
+ Zero-static-caller classes are renamed `statically_unreferenced` (never a confident
32
+ "dead zone") and partitioned: classes with a dynamic-entry signature (OFBiz Service
33
+ Engine `Map<String,Object> name(DispatchContext, Map<…>)`, servlet/event handlers,
34
+ annotation-dispatched beans) or referenced from XML/SPI/properties config are listed
35
+ under `framework_dispatched` and excluded.
36
+ - **Gradle dependency parsing fixed for glob strings (BUG #6).** A string-aware comment
37
+ stripper no longer treats `/*` inside an Ant-style glob (`'**/*.java'`) as a block
38
+ comment — which had silently eaten OFBiz's entire `dependencies {}` block (0 → 59
39
+ deps). When a `dependencies {}` block resolves to nothing, a GAP is reported instead
40
+ of a confident "0 dependencies".
41
+ - **Vendored web assets excluded from code notes (BUG #7).** `is_vendor_path` now flags
42
+ vendored JS/CSS libraries by file name (`jquery-3.5.1.js`, `bootstrap.bundle.js`, …),
43
+ so third-party comments are no longer reported as the project's own notes.
44
+ - **Vendor-neutral project-type label (BUG #1, `--compact`).** `web_mvc` (triggered by
45
+ server-side template engines such as FreeMarker/Thymeleaf) is labeled "Server-side
46
+ MVC web app", not "Spring MVC web app" — OFBiz now reads "Java server-side mvc web
47
+ app using FreeMarker" with no false Spring claim.
48
+
49
+ No-regression: a genuine Spring Boot 2 repo keeps `boot3` applicable and a computed
50
+ readiness; Keycloak/Petclinic golden fixtures still pass.
51
+
52
+ ## [1.66.0] — 2026-06-30
53
+
54
+ ### Fixed — Spring Petclinic (Boot 4.0.3 / Java 17) false positives + aggregate consistency
55
+
56
+ A Pro audit of the canonical **Spring Petclinic** (Spring Boot 4.0.3, Java 17,
57
+ jakarta namespace 100 %) surfaced three defects. Petclinic is now a golden
58
+ regression fixture (`tests/test_migrate_check_petclinic_regression.py`).
59
+
60
+ - **`javax.cache` no longer flagged as a jakarta migration risk (BUG #1).** A single
61
+ source of truth — `_JAVAX_PERMANENT_NAMESPACES` — lists JDK/JSR `javax.*` that keep
62
+ the `javax.*` prefix forever (`javax.cache` JSR-107, `javax.sql`, `javax.xml` JAXP,
63
+ `javax.naming`, `javax.management`, `javax.crypto`, `javax.net`, `javax.security.auth`,
64
+ `javax.annotation.processing`, `javax.tools`, `javax.lang.model`, …). The
65
+ `javax-to-jakarta-migration-risk` dependency flag now decides via **longest-prefix
66
+ match** across the renamed list and this allowlist, so `javax.xml.bind` (JAXB —
67
+ moved) still flags while `javax.xml` (JAXP) does not, and `javax.annotation`
68
+ (JSR-250 — moved) still flags while `javax.annotation.processing` (JSR-269) does not.
69
+ The allowlist can never silence a legitimate migration (`javax.persistence`,
70
+ `javax.servlet`, `javax.validation`, … still flag).
71
+ - **Hibernate axis is version-driven, never a heuristic on absent data (BUG #2).**
72
+ When the Hibernate version is not declared, it is inferred from the Spring Boot BOM:
73
+ Boot ≥ 3 → Hibernate ORM ≥ 6 → **N/A** (`status: managed_ge6`); Boot 2 → Hibernate
74
+ 5.x → **applicable** (`status: managed_h5`); no BOM → `status: unresolved`,
75
+ `applicable: false`, `score: null` — **never** a fabricated number like `91`. The
76
+ `reason` distinguishes "managed ≥ 6" from "unresolved". (Petclinic: hibernate N/A,
77
+ `managed_ge6` via Boot 4.0.3, no phantom 91.)
78
+ - **`readiness_score` is now a traceable aggregate (BUG #3).** It is defined as
79
+ `min` over the applicable **migration** dimensions (`jakarta` / `boot3` / `hibernate`)
80
+ — `jdk_modernization` is an orthogonal upkeep axis, reported but **excluded** from
81
+ the headline. The exact inputs are surfaced in a new `readiness_aggregate{}` block,
82
+ and a consistency invariant (`readiness_score == min(applicable migration dims)`) is
83
+ asserted in `finalize()` and covered by a unit test. This removes the prior
84
+ contradiction (headline 100 while an applicable dimension scored 91).
85
+
3
86
  ## [1.65.0] — 2026-06-30
4
87
 
5
88
  ### Fixed — non-Spring / already-modernized repos no longer fabricate blockers (`migrate-check`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.65.0
3
+ Version: 1.67.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
@@ -738,9 +738,21 @@ evidence. `migrate-check` enforces:
738
738
  usage; Quarkus / Micronaut / Helidon / Jakarta-pure repos report `boot3` as N/A
739
739
  (`spring_present: false`), not a contradictory `applicable: true`.
740
740
  - **Permanent `javax.*` are never jakarta debt.** JDK/JSR namespaces
741
- (`javax.xml.*`, `javax.crypto.*`, `javax.naming.*`, `javax.sql.*`,
742
- `javax.management.*`, `javax.security.auth.*`, …) are allowlisted across every
743
- jakarta scorer.
741
+ (`javax.cache`, `javax.sql`, `javax.xml` JAXP, `javax.crypto`, `javax.naming`,
742
+ `javax.management`, `javax.security.auth`, `javax.annotation.processing`, …) are
743
+ allowlisted across every jakarta scorer (single source of truth:
744
+ `serializer._JAVAX_PERMANENT_NAMESPACES` + `migrate_check._JAKARTA_NO_MIGRATE_PREFIXES`).
745
+ The `javax→jakarta` dependency flag decides via **longest-prefix match**, so
746
+ `javax.xml.bind` (JAXB — moved) flags while `javax.xml` (JAXP) does not, and
747
+ `javax.annotation` (JSR-250 — moved) flags while `javax.annotation.processing`
748
+ (JSR-269) does not. The allowlist never silences a real migration.
749
+ - **`readiness_score` is a traceable aggregate.** It is `min` over the applicable
750
+ **migration** dimensions (`jakarta` / `boot3` / `hibernate`); `jdk_modernization`
751
+ is orthogonal upkeep and is excluded. The exact inputs are in `readiness_aggregate{}`
752
+ and an invariant (`readiness_score == min(applicable migration dims)`) is asserted in
753
+ code. When the Hibernate version is undeclared it is inferred from the Spring Boot
754
+ BOM (Boot ≥3 → Hibernate ≥6 → N/A; Boot 2 → Hibernate 5 → applicable; no BOM →
755
+ `status: unresolved`, never a heuristic score).
744
756
  - **Three buckets: blocker ≠ hygiene ≠ test.** Only product (`main`) code counts
745
757
  toward `blocking_count`, readiness, and effort. Test harnesses
746
758
  (`testsuite/`, `test-framework/`, `integration-arquillian/`, `*-test*`,
@@ -700,9 +700,21 @@ evidence. `migrate-check` enforces:
700
700
  usage; Quarkus / Micronaut / Helidon / Jakarta-pure repos report `boot3` as N/A
701
701
  (`spring_present: false`), not a contradictory `applicable: true`.
702
702
  - **Permanent `javax.*` are never jakarta debt.** JDK/JSR namespaces
703
- (`javax.xml.*`, `javax.crypto.*`, `javax.naming.*`, `javax.sql.*`,
704
- `javax.management.*`, `javax.security.auth.*`, …) are allowlisted across every
705
- jakarta scorer.
703
+ (`javax.cache`, `javax.sql`, `javax.xml` JAXP, `javax.crypto`, `javax.naming`,
704
+ `javax.management`, `javax.security.auth`, `javax.annotation.processing`, …) are
705
+ allowlisted across every jakarta scorer (single source of truth:
706
+ `serializer._JAVAX_PERMANENT_NAMESPACES` + `migrate_check._JAKARTA_NO_MIGRATE_PREFIXES`).
707
+ The `javax→jakarta` dependency flag decides via **longest-prefix match**, so
708
+ `javax.xml.bind` (JAXB — moved) flags while `javax.xml` (JAXP) does not, and
709
+ `javax.annotation` (JSR-250 — moved) flags while `javax.annotation.processing`
710
+ (JSR-269) does not. The allowlist never silences a real migration.
711
+ - **`readiness_score` is a traceable aggregate.** It is `min` over the applicable
712
+ **migration** dimensions (`jakarta` / `boot3` / `hibernate`); `jdk_modernization`
713
+ is orthogonal upkeep and is excluded. The exact inputs are in `readiness_aggregate{}`
714
+ and an invariant (`readiness_score == min(applicable migration dims)`) is asserted in
715
+ code. When the Hibernate version is undeclared it is inferred from the Spring Boot
716
+ BOM (Boot ≥3 → Hibernate ≥6 → N/A; Boot 2 → Hibernate 5 → applicable; no BOM →
717
+ `status: unresolved`, never a heuristic score).
706
718
  - **Three buckets: blocker ≠ hygiene ≠ test.** Only product (`main`) code counts
707
719
  toward `blocking_count`, readiness, and effort. Test harnesses
708
720
  (`testsuite/`, `test-framework/`, `integration-arquillian/`, `*-test*`,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.65.0"
7
+ version = "1.67.0"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.65.0"
3
+ __version__ = "1.67.0"
@@ -4896,7 +4896,8 @@ def migrate_check_cmd(
4896
4896
  output, output_path, copy,
4897
4897
  success_msg=(
4898
4898
  f"Migration check written to {output_path} "
4899
- f"(score: {report.readiness_score}/100, {_total} findings)"
4899
+ f"(score: {report.readiness_score if report.readiness_score is not None else 'N/A'}"
4900
+ f"{'/100' if report.readiness_score is not None else ''}, {_total} findings)"
4900
4901
  ),
4901
4902
  )
4902
4903
 
@@ -5524,6 +5525,91 @@ def fix_bug_cmd(
5524
5525
  )
5525
5526
 
5526
5527
 
5528
+ # Method signatures that are almost always FRAMEWORK ENTRY POINTS, not dead code —
5529
+ # invoked by a dispatcher (reflection / XML / SPI), invisible to a static Java
5530
+ # call-graph. Generalizes beyond any one framework.
5531
+ _DYNAMIC_ENTRY_SIGNATURE_RE = __import__("re").compile(
5532
+ r"\(\s*DispatchContext\b" # OFBiz Service Engine service
5533
+ r"|HttpServletRequest\s+\w+\s*,\s*HttpServletResponse" # OFBiz event / servlet handler
5534
+ r"|@(?:Scheduled|PostConstruct|PreDestroy|EventListener|Bean|"
5535
+ r"RequestMapping|GetMapping|PostMapping|Path|GET|POST|Provider|"
5536
+ r"ApplicationScoped|Singleton)\b", # annotation-dispatched entry
5537
+ )
5538
+ # Config file extensions where a framework wires classes by name (XML/props/yaml).
5539
+ _CONFIG_REF_EXTS: frozenset = frozenset({".xml", ".properties", ".yml", ".yaml", ".groovy"})
5540
+ _CONFIG_SCAN_MAX_FILES: int = 12000
5541
+ _CONFIG_SCAN_MAX_BYTES: int = 256 * 1024
5542
+
5543
+
5544
+ def _partition_static_unreferenced(nodes: list[dict], root: Path) -> tuple[list[dict], list[dict]]:
5545
+ """Split zero-degree classes into (truly_unreferenced, framework_dispatched).
5546
+
5547
+ A class with no static callers is NOT necessarily dead: frameworks invoke
5548
+ classes via reflection, XML/SPI config, or annotations that a static call-graph
5549
+ cannot see (e.g. Apache OFBiz Service Engine services, JAX-RS resources,
5550
+ ServiceLoader providers, scheduled beans). We exclude a candidate when EITHER:
5551
+ 1. its source declares a dynamic-entry method signature, OR
5552
+ 2. its simple name / FQN is referenced from a non-Java config file.
5553
+ Whatever survives is reported as *statically_unreferenced* — never a confident
5554
+ "dead zone".
5555
+ """
5556
+ import os
5557
+ if not nodes:
5558
+ return [], []
5559
+ by_simple: dict[str, list[dict]] = {}
5560
+ for n in nodes:
5561
+ simple = (n.get("fqn") or "").rsplit(".", 1)[-1]
5562
+ if simple:
5563
+ by_simple.setdefault(simple, []).append(n)
5564
+
5565
+ dispatched_fqns: set[str] = set()
5566
+
5567
+ # 1. Source-signature allowlist (bounded — candidate set is small).
5568
+ for n in nodes:
5569
+ src = n.get("source_file")
5570
+ if not src:
5571
+ continue
5572
+ try:
5573
+ txt = (root / src).read_text(encoding="utf-8", errors="replace")
5574
+ except OSError:
5575
+ continue
5576
+ if _DYNAMIC_ENTRY_SIGNATURE_RE.search(txt):
5577
+ dispatched_fqns.add(n["fqn"])
5578
+
5579
+ # 2. Config-reference scan — find candidate names wired from XML/props/yaml.
5580
+ unresolved_simple = {s for s, ns in by_simple.items()
5581
+ if any(x["fqn"] not in dispatched_fqns for x in ns)}
5582
+ if unresolved_simple:
5583
+ files_scanned = 0
5584
+ for dirpath, dirnames, filenames in os.walk(root):
5585
+ dirnames[:] = [d for d in dirnames
5586
+ if d not in {".git", "build", "out", "target", "node_modules", ".gradle"}]
5587
+ for fname in filenames:
5588
+ ext = os.path.splitext(fname)[1].lower()
5589
+ if ext not in _CONFIG_REF_EXTS:
5590
+ continue
5591
+ if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
5592
+ break
5593
+ fpath = os.path.join(dirpath, fname)
5594
+ try:
5595
+ with open(fpath, "r", encoding="utf-8", errors="replace") as fh:
5596
+ text = fh.read(_CONFIG_SCAN_MAX_BYTES)
5597
+ except OSError:
5598
+ continue
5599
+ files_scanned += 1
5600
+ for simple in list(unresolved_simple):
5601
+ if simple in text:
5602
+ for x in by_simple.get(simple, []):
5603
+ dispatched_fqns.add(x["fqn"])
5604
+ unresolved_simple.discard(simple)
5605
+ if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
5606
+ break
5607
+
5608
+ unreferenced = [n for n in nodes if n["fqn"] not in dispatched_fqns]
5609
+ dispatched = [n for n in nodes if n["fqn"] in dispatched_fqns]
5610
+ return unreferenced, dispatched
5611
+
5612
+
5527
5613
  @app.command("modernize")
5528
5614
  def modernize_cmd(
5529
5615
  path: Path = typer.Argument(
@@ -5625,13 +5711,21 @@ def modernize_cmd(
5625
5711
  key=lambda n: (-n.get("in_degree", 0), n.get("fqn", "")),
5626
5712
  )[:20]
5627
5713
 
5628
- # Dead zones: symbols with zero in-degree AND zero out-degree (isolated)
5629
- dead_zones = sorted(
5714
+ # Statically-unreferenced zones: classes with zero in-degree AND zero out-degree
5715
+ # in the Java call-graph. These are NOT necessarily dead — framework dispatch
5716
+ # (reflection / XML / SPI / annotations) is invisible to a static graph — so we
5717
+ # partition out framework-dispatched entry points before reporting, and never
5718
+ # call the survivors "dead". (Defect 5: OFBiz Service-Engine services and event
5719
+ # handlers were false-positive "dead zones".)
5720
+ _zero_degree = sorted(
5630
5721
  [n for n in graph_nodes
5631
5722
  if n.get("in_degree", 0) == 0 and n.get("out_degree", 0) == 0
5632
5723
  and n.get("type") in ("class", "interface")],
5633
5724
  key=lambda n: n.get("fqn", ""),
5634
- )[:20]
5725
+ )
5726
+ dead_zones, framework_dispatched = _partition_static_unreferenced(_zero_degree, root)
5727
+ dead_zones = dead_zones[:20]
5728
+ framework_dispatched = framework_dispatched[:20]
5635
5729
 
5636
5730
  # Hotspot candidates: high in-degree service/repository/controller nodes,
5637
5731
  # ranked by composite score (in_degree × 2 + git_churn) for volatility signal.
@@ -5680,7 +5774,8 @@ def modernize_cmd(
5680
5774
  "total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
5681
5775
  "total_subsystems": len(subsystems),
5682
5776
  "high_coupling_nodes": len(coupling_nodes),
5683
- "dead_zone_candidates": len(dead_zones),
5777
+ "statically_unreferenced": len(dead_zones),
5778
+ "framework_dispatched": len(framework_dispatched),
5684
5779
  }
5685
5780
  _subsystem_summary = [
5686
5781
  {
@@ -5723,10 +5818,20 @@ def modernize_cmd(
5723
5818
  {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
5724
5819
  for n in coupling_nodes
5725
5820
  ],
5726
- "dead_zone_candidates": [
5821
+ "statically_unreferenced": [
5727
5822
  {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
5728
5823
  for n in dead_zones
5729
5824
  ],
5825
+ "statically_unreferenced_note": (
5826
+ "Zero static callers in the Java call-graph. NOT confirmed dead: verify "
5827
+ "no framework dispatch (reflection, XML/SPI config, annotations) before "
5828
+ "removing. Classes detected as framework-dispatched are listed separately "
5829
+ "under framework_dispatched and excluded from this list."
5830
+ ),
5831
+ "framework_dispatched": [
5832
+ {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
5833
+ for n in framework_dispatched
5834
+ ],
5730
5835
  "subsystem_summary": _subsystem_summary,
5731
5836
  "cross_module_tangles": [
5732
5837
  {
@@ -5742,7 +5847,8 @@ def modernize_cmd(
5742
5847
  if hotspots else
5743
5848
  "high_coupling_nodes shows the most-referenced classes — start there. "
5744
5849
  )
5745
- + "Dead zones are safe to remove or refactor. "
5850
+ + "statically_unreferenced lists classes with no Java callers review "
5851
+ + "for framework dispatch (XML/reflection/SPI) before removing. "
5746
5852
  + "Cross-module tangles indicate coupling worth decomposing."
5747
5853
  ),
5748
5854
  }
@@ -28,7 +28,11 @@ _STACK_LABELS: dict[str, str] = {
28
28
 
29
29
  _TYPE_LABELS: dict[str, str] = {
30
30
  "api": "REST API",
31
- "web_mvc": "Spring MVC web app",
31
+ # Vendor-neutral: web_mvc is triggered by server-side template engines
32
+ # (Thymeleaf/FreeMarker), which are NOT exclusive to Spring (e.g. Apache OFBiz
33
+ # uses FreeMarker with its own framework). The detected frameworks list carries
34
+ # the actual stack; the type label must not assert "Spring".
35
+ "web_mvc": "Server-side MVC web app",
32
36
  "webapp": "Web app",
33
37
  "fullstack": "Full-stack app",
34
38
  "cli": "CLI tool",
@@ -1278,9 +1278,42 @@ class DependencyAnalyzer:
1278
1278
  return records, limitations
1279
1279
 
1280
1280
  def _strip_gradle_comments(self, content: str) -> str:
1281
- content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
1282
- content = re.sub(r"//[^\n]*", "", content)
1283
- return content
1281
+ """Strip // and /* */ comments WITHOUT touching string literals.
1282
+
1283
+ A naive regex treats `/*` inside a string as a block-comment open. Gradle
1284
+ build files routinely embed `/*` in Ant-style glob strings (e.g.
1285
+ '**/*.java') and `//` in URL strings — a regex stripper then eats the entire
1286
+ dependencies block up to the next literal `*/` (real defect on Apache OFBiz,
1287
+ where ~100 deps vanished). This scanner skips quoted strings (single, double,
1288
+ and triple-quoted, with backslash escapes) so only genuine comments are removed.
1289
+ """
1290
+ out: list[str] = []
1291
+ i, n = 0, len(content)
1292
+ quote: Optional[str] = None # active string delimiter
1293
+ while i < n:
1294
+ if quote is not None:
1295
+ # Inside a string literal — copy verbatim until the matching delimiter.
1296
+ if len(quote) == 1 and content[i] == "\\" and i + 1 < n:
1297
+ out.append(content[i:i + 2]); i += 2; continue
1298
+ if content.startswith(quote, i):
1299
+ out.append(quote); i += len(quote); quote = None; continue
1300
+ out.append(content[i]); i += 1; continue
1301
+ if content.startswith("//", i):
1302
+ j = content.find("\n", i)
1303
+ if j == -1:
1304
+ break
1305
+ i = j; continue
1306
+ if content.startswith("/*", i):
1307
+ j = content.find("*/", i + 2)
1308
+ if j == -1:
1309
+ break
1310
+ i = j + 2; continue
1311
+ for q in ('"""', "'''", '"', "'"):
1312
+ if content.startswith(q, i):
1313
+ quote = q; out.append(q); i += len(q); break
1314
+ else:
1315
+ out.append(content[i]); i += 1
1316
+ return "".join(out)
1284
1317
 
1285
1318
  def _analyze_gradle(self, root: Path) -> tuple[list[DependencyRecord], list[str]]:
1286
1319
  for filename in ("build.gradle", "build.gradle.kts"):
@@ -1307,7 +1340,20 @@ class DependencyAnalyzer:
1307
1340
  except OSError:
1308
1341
  pass
1309
1342
 
1310
- return self._dedupe(records), ["gradle: no compatible lockfile found; transitive dependencies unavailable"]
1343
+ deduped = self._dedupe(records)
1344
+ gradle_limitations = [
1345
+ "gradle: no compatible lockfile found; transitive dependencies unavailable"
1346
+ ]
1347
+ # Honest-zero guard: if the build file declares a dependencies block but
1348
+ # nothing was resolved, report a GAP rather than a confident "0 deps".
1349
+ if not deduped and re.search(r"\bdependencies\s*\{", content):
1350
+ jar_count = sum(1 for _ in root.glob("lib/**/*.jar"))
1351
+ gap = ("gradle: dependencies declared but none resolved by static "
1352
+ "parsing (non-standard declaration syntax)")
1353
+ if jar_count:
1354
+ gap += f"; {jar_count} jar(s) present under lib/ (not parsed)"
1355
+ gradle_limitations.append(gap)
1356
+ return deduped, gradle_limitations
1311
1357
  return [], []
1312
1358
 
1313
1359
  def _parse_gradle_properties(self, root: Path, content: str) -> dict[str, str]:
@@ -774,7 +774,7 @@ def run_migrate_flow_impl(repo_path: str, min_severity: str = "low") -> dict[str
774
774
  if not _is_java_repo(repo_path):
775
775
  quality_warnings.append(
776
776
  "not_a_java_repo: migration analysis targets Spring Boot / Java repos. "
777
- "Result will be empty (readiness_score=100, no findings)."
777
+ "Result will be empty (readiness_score=null/N/A, no findings)."
778
778
  )
779
779
 
780
780
  report = _exec(["migrate-check", repo_path, "--min-severity", min_severity])
@@ -798,7 +798,7 @@ depth: BFS depth for indirect caller traversal (1–8, default: 4).
798
798
  Analyzes codebase for modernization opportunities: dead zones, hotspot scores, upgrade candidates.
799
799
 
800
800
  Maps to: sourcecode modernize <repo_path>
801
- Returns: hotspot_candidates (high fan-in + git churn), dead_zone_candidates (isolated classes),
801
+ Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (zero-caller classes — NOT confirmed dead; verify no framework dispatch) + framework_dispatched,
802
802
  high_coupling_nodes, subsystem_summary, cross_module_tangles, recommendation.
803
803
 
804
804
  Best for: refactor planning, identifying where to start, finding safe removal candidates.
@@ -1403,7 +1403,7 @@ Spring Boot 2→3 migration readiness: javax→jakarta namespace blockers. JAVA
1403
1403
  When to call: when asked about Spring Boot migration readiness, javax vs jakarta imports,
1404
1404
  or upgrading from Spring Boot 2.x to 3.x. Use BEFORE get_spring_audit when the goal
1405
1405
  is migration planning rather than ongoing Spring semantic audit.
1406
- Do NOT call on non-Java repositories — returns readiness_score=100 with no findings.
1406
+ Do NOT call on non-Java repositories — returns readiness_score=null (N/A) with no findings.
1407
1407
 
1408
1408
  Rules detected:
1409
1409
  MIG-001 critical — javax.persistence imports (JPA; will not compile after migration)
@@ -1415,7 +1415,7 @@ Rules detected:
1415
1415
  MIG-007 medium — javax.inject imports (DI annotations)
1416
1416
  MIG-008 medium — javax.ws.rs imports (JAX-RS API)
1417
1417
 
1418
- Returns: schema_version, readiness_score (0–100; 100=ready to migrate),
1418
+ Returns: schema_version, readiness_score (0–100, or null=N/A when no migration target applies; 100=ready to migrate),
1419
1419
  jakarta_readiness / boot3_readiness / jdk_modernization (per-dimension 0–100),
1420
1420
  blocking_count, estimated_effort_days, spring_boot_2_detected (true|false|null —
1421
1421
  null=undetermined, never assumed true), spring_boot_version_detected,
@@ -517,7 +517,7 @@ def run_migrate_flow(repo_path: str = ".", min_severity: str = "low") -> dict:
517
517
  Primary high-value entry point for migration planning. Wraps migrate-check and
518
518
  lifts the headline numbers to the top level so the agent can plan a 2→3 upgrade
519
519
  without parsing the full report:
520
- - readiness_score (0–100; 100 = ready), blocking_count, estimated_effort_days
520
+ - readiness_score (0–100, or null=N/A when no migration target applies; 100 = ready), blocking_count, estimated_effort_days
521
521
  - by_severity and by_target breakdown (jakarta / spring_security_6 / java_11)
522
522
 
523
523
  Use this instead of calling get_migration_readiness and interpreting it by hand.
@@ -788,10 +788,10 @@ def get_migration_readiness(repo_path: str = ".", min_severity: str = "low") ->
788
788
  When to call: when asked about Spring Boot migration readiness, javax vs jakarta imports,
789
789
  or upgrading from Spring Boot 2.x to 3.x. Call this BEFORE get_spring_audit when
790
790
  the goal is migration planning — not ongoing audit.
791
- Do NOT call on non-Java repositories — returns readiness_score=100 with no findings.
791
+ Do NOT call on non-Java repositories — returns readiness_score=null (N/A) with no findings.
792
792
 
793
793
  Maps to: sourcecode migrate-check <repo_path> --min-severity <min_severity>
794
- Returns: MigrationReport with schema_version, readiness_score (0–100; 100=ready to migrate),
794
+ Returns: MigrationReport with schema_version, readiness_score (0–100, or null=N/A when no migration target applies; 100=ready to migrate),
795
795
  jakarta_readiness / boot3_readiness / jdk_modernization / hibernate_readiness
796
796
  (per-dimension 0–100), headline_blocker (e.g. "hibernate_rewrite" or null),
797
797
  hibernate (Hibernate 5→6 stratified model: 4-layer risk_matrix, rewrite_targets[]
@@ -1335,7 +1335,7 @@ def modernize_context(repo_path: str = ".", format: str = "json") -> dict:
1335
1335
  """Analyzes codebase for modernization opportunities: dead zones, hotspot scores, upgrade candidates.
1336
1336
 
1337
1337
  Maps to: sourcecode modernize <repo_path>
1338
- Returns: hotspot_candidates (high fan-in + git churn), dead_zone_candidates (isolated classes),
1338
+ Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (zero-caller classes — NOT confirmed dead; verify no framework dispatch) + framework_dispatched,
1339
1339
  high_coupling_nodes, subsystem_summary, cross_module_tangles, recommendation.
1340
1340
 
1341
1341
  Best for: refactor planning, identifying where to start, finding safe removal candidates.