sourcecode 1.68.0__tar.gz → 1.70.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.68.0 → sourcecode-1.70.0}/CHANGELOG.md +129 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/PKG-INFO +1 -1
- {sourcecode-1.68.0 → sourcecode-1.70.0}/pyproject.toml +1 -1
- sourcecode-1.70.0/scripts/generate_jdk_exports.py +132 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/architecture_analyzer.py +39 -6
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/architecture_summary.py +39 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/classifier.py +121 -1
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/java.py +36 -1
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/explain.py +83 -1
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/hibernate_strat.py +96 -21
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/integration_detector.py +42 -0
- sourcecode-1.70.0/src/sourcecode/jdk_exports.py +26 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/migrate_check.py +87 -9
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/repository_ir.py +81 -2
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/spring_impact.py +30 -3
- {sourcecode-1.68.0 → sourcecode-1.70.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/.gitignore +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/.ruff.toml +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/LICENSE +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/README.md +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/SECURITY.md +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/raw +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.68.0 → sourcecode-1.70.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,134 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.70.0] — 2026-07-01
|
|
4
|
+
|
|
5
|
+
### Fixed — reliability & internal-consistency on a large Spring 5 / Hibernate 5 repo (openmrs-core field test)
|
|
6
|
+
|
|
7
|
+
A full migration audit of **openmrs-core** (`7c0144ad2`, branch `2.9.x`; ~866
|
|
8
|
+
production files, Maven multi-module, Spring Framework 5 + Hibernate 5, classic
|
|
9
|
+
WAR, **no Spring Boot**) surfaced four reproducible defects where a headline value
|
|
10
|
+
was wrong or internally contradictory. Each fix targets the **root cause** and,
|
|
11
|
+
where certainty is not achievable, degrades to an explicit low-confidence / `null`
|
|
12
|
+
signal rather than asserting a concrete-but-false value. New regression coverage
|
|
13
|
+
across four suites.
|
|
14
|
+
|
|
15
|
+
- **`migrate-check` — Hibernate version confused a sibling artifact's version (BUG #1, CRITICAL).**
|
|
16
|
+
Hibernate ORM was resolved by scanning for *any* `<hibernate*version>` property
|
|
17
|
+
and keeping the **newest**, so on openmrs `${hibernateSearchVersion}=6.2.4.Final`
|
|
18
|
+
(Hibernate **Search**, a different artifact) beat `${hibernateVersion}=5.6.15.Final`
|
|
19
|
+
(Hibernate **ORM**). The tool declared the repo "already on Hibernate 6", set
|
|
20
|
+
`migration_applicable:false`, and silently **dropped the 28.9–95.6 person-day**
|
|
21
|
+
rewrite range from `estimated_effort_days` (under-reporting 1.5–2.6×). Resolution
|
|
22
|
+
is now **anchored to the `hibernate-core`/`hibernate-orm` dependency** — the
|
|
23
|
+
`<version>` on that coordinate, resolving a `${property}` to the *specific*
|
|
24
|
+
referenced property, and the Gradle coordinate. The property fallback (BOM-managed
|
|
25
|
+
repos) **excludes sibling artifacts** (Search/Validator/Envers/OGM/…). openmrs now
|
|
26
|
+
resolves `5.6.15.Final` / major 5 / `migration_applicable:true`, and
|
|
27
|
+
`estimated_effort_days` includes the Hibernate rewrite range via a new
|
|
28
|
+
`effort_breakdown` block. The same-name contradiction (`hibernate_readiness:100`
|
|
29
|
+
at root vs `0` nested) is removed: the nested field is renamed
|
|
30
|
+
**`rewrite_zone_readiness`** (raw rewrite-zone score, independent of applicability),
|
|
31
|
+
leaving the document-root `hibernate_readiness` as the single authoritative,
|
|
32
|
+
applicability-gated score.
|
|
33
|
+
|
|
34
|
+
- **`impact-chain` — a class's own methods counted as "direct callers" (BUG #2, HIGH).**
|
|
35
|
+
For a class-level seed, the BFS folds in every method key of the class, so internal
|
|
36
|
+
method→method calls leaked into `direct_callers`. On `ConceptServiceImpl` this
|
|
37
|
+
reported **24 direct callers (22 of them own members)**, inflating `risk_score` to
|
|
38
|
+
`20.0`/`high` — ~12× the real blast radius. Callers whose owning class is a seed
|
|
39
|
+
class are now excluded (they are *members*, not external callers); the count is
|
|
40
|
+
surfaced as `self_referential_excluded` plus an `analysis_warnings` entry.
|
|
41
|
+
`ConceptServiceImpl` now reports **2 external callers**, `risk_score` recomputed to
|
|
42
|
+
`8.5`/`medium`.
|
|
43
|
+
|
|
44
|
+
- **`export --integrations` — a user class classified as a JDK API by bare name (BUG #3, MEDIUM).**
|
|
45
|
+
`org.openmrs.util.HttpClient` (a thin wrapper over `java.net.HttpURLConnection`)
|
|
46
|
+
was labeled `jdk-httpclient` purely because its simple name matches
|
|
47
|
+
`java.net.http.HttpClient`. The client is now resolved by the **fully-qualified
|
|
48
|
+
import** — `jdk-httpclient` only when `java.net.http.HttpClient` is actually
|
|
49
|
+
imported and the file does not declare its own — otherwise it degrades to a
|
|
50
|
+
low-confidence **`custom-http-wrapper`**. Pure type-declaration sites (field,
|
|
51
|
+
parameter, return type such as `setImplementationIdHttpClient(HttpClient c)`) are
|
|
52
|
+
no longer emitted as network-invocation evidence; only constructions / static calls
|
|
53
|
+
count.
|
|
54
|
+
|
|
55
|
+
- **`--compact` — "rest api" headline unsupported by the `endpoints` command (BUG #4, LOW).**
|
|
56
|
+
The `architecture_summary` label is driven by HTTP-framework **presence** (Spring
|
|
57
|
+
MVC on the classpath), so openmrs was headlined "Java rest api" while the
|
|
58
|
+
authoritative `endpoints` command found **only 3 endpoints** (one a test module,
|
|
59
|
+
one medium-confidence `router_dsl`). The summary now consults the same endpoint
|
|
60
|
+
extractor and, below a high-confidence-endpoint threshold, degrades to a qualified,
|
|
61
|
+
consistent phrasing ("HTTP framework present; only N endpoints detected — see
|
|
62
|
+
`endpoints`") instead of overclaiming (Java/Kotlin repos only).
|
|
63
|
+
|
|
64
|
+
## [1.69.0] — 2026-06-30
|
|
65
|
+
|
|
66
|
+
### Fixed — framing & symbol-graph fidelity on a non-framework Java library (JobRunr field test)
|
|
67
|
+
|
|
68
|
+
A full architectural audit of **JobRunr** (Gradle multi-module, 707 classes, a
|
|
69
|
+
background-job-processing **library** that is framework-agnostic at its core and
|
|
70
|
+
integrates with Quarkus/Micronaut/Spring only through small optional adapter
|
|
71
|
+
modules) surfaced four reproducible defects. Each fix targets the **root cause** so
|
|
72
|
+
it generalizes to any equivalent repo, with new regression coverage across five
|
|
73
|
+
suites.
|
|
74
|
+
|
|
75
|
+
- **`migrate-check` MIG-011 — false positive on JDK-exported packages (BUG #1).**
|
|
76
|
+
The rule flagged *any* `sun.*` / `com.sun.*` import as a strongly-encapsulated
|
|
77
|
+
JDK internal on a pure prefix heuristic. On JobRunr this turned 14 benign
|
|
78
|
+
`com.sun.net.httpserver.*` imports into 14 `high`/`manual_migration` findings —
|
|
79
|
+
**100% of an inflated `blocking_count: 14`** on a healthy repo (the GO/NO-GO
|
|
80
|
+
metric). `com.sun.net.httpserver` is exported **unconditionally** by the
|
|
81
|
+
`jdk.httpserver` module (public since Java 6, basis of JEP 408) and needs no
|
|
82
|
+
`--add-exports`/`--add-opens`. MIG-011 now consults an allowlist of
|
|
83
|
+
unconditionally-exported JDK packages **generated from the running JDK** by
|
|
84
|
+
`scripts/generate_jdk_exports.py` (`src/sourcecode/jdk_exports.py`), never
|
|
85
|
+
hand-maintained. Genuinely-internal packages (`sun.misc.Unsafe`,
|
|
86
|
+
`com.sun.tools.*`, `com.sun.jdi.*`, `com.sun.source.*`) stay `high`.
|
|
87
|
+
`blocking_count` and `estimated_effort_days` recompute accordingly. JobRunr
|
|
88
|
+
`blocking_count` 14 → 0.
|
|
89
|
+
- **Phantom symbols extracted from prose (BUG #2).** `modernize`'s
|
|
90
|
+
`statically_unreferenced` / `framework_dispatched` lists (and the shared symbol
|
|
91
|
+
graph behind `repo-ir` / `impact` / `export --c4`) contained FQNs that match no
|
|
92
|
+
real type — e.g. `org.jobrunr.configuration.provides` (from the Javadoc "This
|
|
93
|
+
class **provides** the entry point") and `…details.instead` (from an exception
|
|
94
|
+
string "…**instead** of an actual implementation"). The non-annotated fast-path
|
|
95
|
+
symbol scan ran `(?:class|interface|enum)\s+(\w+)` over **raw source**, tokenizing
|
|
96
|
+
words inside comments and string literals. It now strips comments + string
|
|
97
|
+
literals first and requires a capitalized type name, matching the precision of the
|
|
98
|
+
annotated-file extractor. Fixing the shared parser fixes every downstream command.
|
|
99
|
+
- **`endpoints` — total false negative on imperative router DSLs (BUG #3).** JobRunr
|
|
100
|
+
exposes 12 live dashboard REST routes via `get("/jobs", handler)` /
|
|
101
|
+
`post(...)` / `delete(...)` calls; the annotation-only extractor returned **0
|
|
102
|
+
endpoints** (silently disabling `validation` downstream). A shape-based detector
|
|
103
|
+
now recognizes router-DSL registrations — an HTTP-verb method whose first argument
|
|
104
|
+
is a path literal **and** that has a second argument (the trailing comma
|
|
105
|
+
distinguishes a route from a 1-arg `Map.get`). Reported at `confidence: medium`
|
|
106
|
+
with `source: router_dsl`. Handles bare (Spark/static-import) and `app.get(...)`
|
|
107
|
+
(Javalin) forms. JobRunr `endpoints` 0 → 12.
|
|
108
|
+
- **Stack/architecture classification biased to the most-recognizable framework (BUG #4).**
|
|
109
|
+
JobRunr (19 modules, framework only in 3 small adapters, `core` ≈ 85% of code) was
|
|
110
|
+
reported as `project_type: "api"` / `frameworks: ["Quarkus"]`. Two root causes:
|
|
111
|
+
(a) the Gradle detector substring-matched `"quarkus"` inside an **exclusion filter**
|
|
112
|
+
(`configure(subprojects.findAll { !it.name.contains('quarkus') })`) — framework
|
|
113
|
+
tokens are now matched only inside genuine dependency/plugin lines; and (b)
|
|
114
|
+
presence-based typing — a framework confined to a minority adapter module (its
|
|
115
|
+
dominant source module uses no framework) no longer drives the project type, and a
|
|
116
|
+
multi-module JVM repo with no defining web/API framework classifies as `library`,
|
|
117
|
+
never `unknown`. A monolithic framework app is unaffected. `explain` no longer
|
|
118
|
+
emits the misleading "No stereotype detected — may be a plain class or utility" for
|
|
119
|
+
a structurally central non-annotated class: it infers a low-confidence stereotype
|
|
120
|
+
from in-degree, lifecycle methods, and naming (e.g. `BackgroundJobServer` →
|
|
121
|
+
"Likely server/orchestrator … lifecycle methods detected (start/stop)"). Finally
|
|
122
|
+
the `architecture_summary` no longer prints a phantom "MVC pattern with
|
|
123
|
+
controller, model, view layers" for a non-web library: MVC now requires a real
|
|
124
|
+
View layer (it was inferred from a `handlers` dir + a `model` dir with no view),
|
|
125
|
+
a bundled SPA under `src/main/resources/.../frontend` is no longer counted as a
|
|
126
|
+
backend view layer, `testFixtures` source roots are excluded, and View detection
|
|
127
|
+
recognizes template files (`.html`/etc.) that the code-extension filter dropped.
|
|
128
|
+
JobRunr `project_type` "api" → "library", `frameworks` `["Quarkus"]` → `[]`,
|
|
129
|
+
architecture "MVC … controller, model, view" → "Layered … controller, repository,
|
|
130
|
+
infrastructure".
|
|
131
|
+
|
|
3
132
|
## [1.68.0] — 2026-06-30
|
|
4
133
|
|
|
5
134
|
### Fixed — narrative/structured-data parity on non-Spring DI repos (Netflix Eureka field test)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate the JDK unconditional-export allowlist used by migrate-check MIG-011.
|
|
3
|
+
|
|
4
|
+
MIG-011 flags `sun.*` / `com.sun.*` imports as strongly-encapsulated JDK-internal
|
|
5
|
+
APIs. That heuristic is *prefix-based* and produces false positives on packages
|
|
6
|
+
that merely share the prefix but are in fact exported **unconditionally** by a
|
|
7
|
+
JDK module (no `to` clause) and therefore require NO `--add-exports` /
|
|
8
|
+
`--add-opens` on any classpath or module-path scenario. The canonical example is
|
|
9
|
+
`com.sun.net.httpserver.*` (module `jdk.httpserver`) — public and documented
|
|
10
|
+
since Java 6, the basis of JEP 408.
|
|
11
|
+
|
|
12
|
+
This script derives the allowlist directly from the running JDK by parsing
|
|
13
|
+
`java --describe-module` for every module, so the data tracks the real platform
|
|
14
|
+
instead of a hand-maintained list that silently rots across releases.
|
|
15
|
+
|
|
16
|
+
Two filters are applied:
|
|
17
|
+
|
|
18
|
+
1. Only `exports <pkg>` lines WITHOUT a `to <module>` qualifier are kept
|
|
19
|
+
(qualified exports still need `--add-exports` for arbitrary code).
|
|
20
|
+
|
|
21
|
+
2. Modules whose whole purpose is internal / tooling / deprecated access are
|
|
22
|
+
DENYLISTED — their `sun.*` / `com.sun.*` exports are genuine migration
|
|
23
|
+
concerns and must keep MIG-011's `high` severity:
|
|
24
|
+
- jdk.unsupported → sun.misc.Unsafe, sun.reflect (deprecated for removal)
|
|
25
|
+
- jdk.compiler → com.sun.source.*, com.sun.tools.javac (compiler internals)
|
|
26
|
+
- jdk.attach → com.sun.tools.attach.* (tooling)
|
|
27
|
+
- jdk.jdi → com.sun.jdi.* (debugger internals)
|
|
28
|
+
- jdk.jconsole / jdk.jdeps / jdk.javadoc / jdk.jshell → com.sun.tools.*
|
|
29
|
+
|
|
30
|
+
Run with the OLDEST supported target JDK to stay conservative (a package
|
|
31
|
+
exported in JDK 21 but not JDK 17 must not be allowlisted for a JDK 17 target).
|
|
32
|
+
Re-run after a JDK bump and commit the regenerated module.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
python scripts/generate_jdk_exports.py > src/sourcecode/jdk_exports.py
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import re
|
|
40
|
+
import subprocess
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
# Modules whose sun.*/com.sun.* exports are genuinely internal/tooling/deprecated.
|
|
44
|
+
# Their packages must NOT be allowlisted — MIG-011 keeps firing `high` for them.
|
|
45
|
+
_DENYLIST_MODULES: frozenset[str] = frozenset(
|
|
46
|
+
{
|
|
47
|
+
"jdk.unsupported",
|
|
48
|
+
"jdk.compiler",
|
|
49
|
+
"jdk.attach",
|
|
50
|
+
"jdk.jdi",
|
|
51
|
+
"jdk.jconsole",
|
|
52
|
+
"jdk.jdeps",
|
|
53
|
+
"jdk.javadoc",
|
|
54
|
+
"jdk.jshell",
|
|
55
|
+
"jdk.internal.ed",
|
|
56
|
+
"jdk.internal.le",
|
|
57
|
+
"jdk.internal.opt",
|
|
58
|
+
"jdk.internal.vm.ci",
|
|
59
|
+
"jdk.internal.vm.compiler",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_EXPORT_RE = re.compile(r"^exports\s+(\S+?)(?:\s+to\s+.*)?$")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _list_modules() -> list[str]:
|
|
67
|
+
out = subprocess.run(
|
|
68
|
+
["java", "--list-modules"], capture_output=True, text=True, check=True
|
|
69
|
+
).stdout
|
|
70
|
+
return [line.split("@", 1)[0].strip() for line in out.splitlines() if line.strip()]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _module_exports(module: str) -> list[tuple[str, bool]]:
|
|
74
|
+
"""Return (package, is_unconditional) for each `exports` line of a module."""
|
|
75
|
+
out = subprocess.run(
|
|
76
|
+
["java", "--describe-module", module],
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
).stdout
|
|
80
|
+
results: list[tuple[str, bool]] = []
|
|
81
|
+
for line in out.splitlines():
|
|
82
|
+
line = line.strip()
|
|
83
|
+
m = _EXPORT_RE.match(line)
|
|
84
|
+
if not m:
|
|
85
|
+
continue
|
|
86
|
+
pkg = m.group(1)
|
|
87
|
+
unconditional = " to " not in line
|
|
88
|
+
results.append((pkg, unconditional))
|
|
89
|
+
return results
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> int:
|
|
93
|
+
java_version = subprocess.run(
|
|
94
|
+
["java", "-version"], capture_output=True, text=True
|
|
95
|
+
).stderr.splitlines()[0]
|
|
96
|
+
|
|
97
|
+
allowlist: set[str] = set()
|
|
98
|
+
for module in _list_modules():
|
|
99
|
+
if module in _DENYLIST_MODULES:
|
|
100
|
+
continue
|
|
101
|
+
for pkg, unconditional in _module_exports(module):
|
|
102
|
+
if not unconditional:
|
|
103
|
+
continue
|
|
104
|
+
if pkg.startswith("sun.") or pkg.startswith("com.sun."):
|
|
105
|
+
allowlist.add(pkg)
|
|
106
|
+
|
|
107
|
+
packages = sorted(allowlist)
|
|
108
|
+
|
|
109
|
+
print('"""GENERATED FILE — do not edit by hand.')
|
|
110
|
+
print()
|
|
111
|
+
print("Allowlist of sun.* / com.sun.* packages exported UNCONDITIONALLY by the")
|
|
112
|
+
print("JDK (no `--add-exports` / `--add-opens` required on classpath or module")
|
|
113
|
+
print("path). Consumed by migrate-check MIG-011 to suppress false positives.")
|
|
114
|
+
print()
|
|
115
|
+
print(f"Regenerate with: python scripts/generate_jdk_exports.py > "
|
|
116
|
+
"src/sourcecode/jdk_exports.py")
|
|
117
|
+
print(f"Generated from: {java_version}")
|
|
118
|
+
print('"""')
|
|
119
|
+
print("from __future__ import annotations")
|
|
120
|
+
print()
|
|
121
|
+
print()
|
|
122
|
+
print("JDK_UNCONDITIONAL_EXPORTS: frozenset[str] = frozenset(")
|
|
123
|
+
print(" {")
|
|
124
|
+
for pkg in packages:
|
|
125
|
+
print(f' "{pkg}",')
|
|
126
|
+
print(" }")
|
|
127
|
+
print(")")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
sys.exit(main())
|
|
@@ -38,7 +38,15 @@ _CODE_EXTENSIONS = {
|
|
|
38
38
|
}
|
|
39
39
|
_GENERIC_NAMES = {"utils", "helpers", "common", "shared", "misc", "core", "root", ""}
|
|
40
40
|
|
|
41
|
-
_TEST_DIRS: frozenset[str] = frozenset({
|
|
41
|
+
_TEST_DIRS: frozenset[str] = frozenset({
|
|
42
|
+
"tests", "test", "spec", "specs", "__tests__", "e2e",
|
|
43
|
+
# Gradle/Maven test-fixture source roots are test code, not runtime architecture.
|
|
44
|
+
"testfixtures", "testfixture",
|
|
45
|
+
})
|
|
46
|
+
# Always-vendored asset trees — never a backend code layer at any depth.
|
|
47
|
+
_ASSET_DIRS: frozenset[str] = frozenset({
|
|
48
|
+
"node_modules", "bower_components",
|
|
49
|
+
})
|
|
42
50
|
_BENCHMARK_DIRS: frozenset[str] = frozenset({
|
|
43
51
|
"benchmark", "benchmarks", "bench",
|
|
44
52
|
"example", "examples",
|
|
@@ -50,7 +58,7 @@ _BENCHMARK_DIRS: frozenset[str] = frozenset({
|
|
|
50
58
|
_DOCS_DIRS: frozenset[str] = frozenset({"docs", "doc", "documentation", "wiki"})
|
|
51
59
|
_TOOLING_DIRS: frozenset[str] = frozenset({"scripts", "script", "tools", "tool", "ci"})
|
|
52
60
|
# All dirs that are not part of the runtime source architecture
|
|
53
|
-
_NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS
|
|
61
|
+
_NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS | _ASSET_DIRS
|
|
54
62
|
|
|
55
63
|
# Exact file stems that signal a specific architectural layer
|
|
56
64
|
_LAYER_STEM_EXACT: dict[str, str] = {
|
|
@@ -150,6 +158,15 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
150
158
|
},
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
# Layer keys a pattern MUST match to qualify, regardless of score. BUG (JobRunr
|
|
162
|
+
# field test): "mvc" was inferred from a controller-ish dir (`handlers`) plus a
|
|
163
|
+
# `model` dir, with NO view layer — that is not MVC (a library with no templates/
|
|
164
|
+
# pages/components is at most layered). MVC's defining trait is the View, so require
|
|
165
|
+
# it; without it the pattern falls through to layered / a weaker match.
|
|
166
|
+
_PATTERN_REQUIRED_KEYS: dict[str, frozenset[str]] = {
|
|
167
|
+
"mvc": frozenset({"view"}),
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
# Higher value = wins when score ties
|
|
154
171
|
_PATTERN_PRIORITY: dict[str, int] = {
|
|
155
172
|
"cqrs": 8,
|
|
@@ -245,8 +262,12 @@ class ArchitectureAnalyzer:
|
|
|
245
262
|
# Step 2: domain clustering
|
|
246
263
|
domains = self._cluster_domains(filtered)
|
|
247
264
|
|
|
248
|
-
# Step 3: layer detection
|
|
249
|
-
|
|
265
|
+
# Step 3: layer detection. Feed the FULL path list (not the code-extension
|
|
266
|
+
# `filtered` set): a View layer is template files (.html/.jinja/.ejs…) that
|
|
267
|
+
# `filtered` would drop, hiding the very layer that distinguishes MVC.
|
|
268
|
+
# _detect_layers applies its own non-source/asset/test dir filtering, so
|
|
269
|
+
# bundled frontend trees and test fixtures are still excluded.
|
|
270
|
+
pattern, layers = self._detect_layers(sm.file_paths)
|
|
250
271
|
if pattern in (None, "flat", "unknown"):
|
|
251
272
|
if pattern == "flat":
|
|
252
273
|
limitations.append("Layer pattern not detected: project has a flat directory structure")
|
|
@@ -518,10 +539,18 @@ class ArchitectureAnalyzer:
|
|
|
518
539
|
return domains
|
|
519
540
|
|
|
520
541
|
def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
|
|
521
|
-
# Exclude non-source paths (tests, benchmarks, docs, tooling
|
|
542
|
+
# Exclude non-source paths (tests, benchmarks, docs, tooling, vendored assets)
|
|
543
|
+
# from layer scoring. Also exclude anything under a `resources/` segment: in
|
|
544
|
+
# Maven/Gradle layouts `src/main/resources/**` is bundled config/assets — e.g.
|
|
545
|
+
# JobRunr ships a React dashboard SPA at
|
|
546
|
+
# `core/src/main/resources/org/jobrunr/dashboard/frontend/src/components`, whose
|
|
547
|
+
# `components` dir would otherwise be miscounted as a backend MVC "view" layer.
|
|
522
548
|
source_paths = [
|
|
523
549
|
p for p in paths
|
|
524
|
-
if not any(
|
|
550
|
+
if not any(
|
|
551
|
+
part.lower() in _NON_SOURCE_DIRS or part.lower() == "resources"
|
|
552
|
+
for part in p.replace("\\", "/").split("/")
|
|
553
|
+
)
|
|
525
554
|
]
|
|
526
555
|
if not source_paths:
|
|
527
556
|
return "unknown", []
|
|
@@ -544,6 +573,10 @@ class ArchitectureAnalyzer:
|
|
|
544
573
|
matched_dirs = [d for d in dir_names if d in keywords]
|
|
545
574
|
if matched_dirs:
|
|
546
575
|
matched[layer_key] = matched_dirs
|
|
576
|
+
# A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
|
|
577
|
+
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
578
|
+
if required and not required.issubset(matched.keys()):
|
|
579
|
+
continue
|
|
547
580
|
score = len(matched)
|
|
548
581
|
priority = _PATTERN_PRIORITY.get(pattern_name, 0)
|
|
549
582
|
if (score, priority) > (best_score, best_priority):
|
|
@@ -23,6 +23,14 @@ _JAVA_EXTENSIONS = {".java", ".kt", ".scala"}
|
|
|
23
23
|
|
|
24
24
|
_CORE_DETECTION_MODULES = {"scanner", "detectors", "classifier", "workspace"}
|
|
25
25
|
|
|
26
|
+
# BUG #4 (v1.70.0): the "REST API" project-type label is driven by HTTP-framework
|
|
27
|
+
# PRESENCE (Spring MVC / JAX-RS on the classpath), not by an actual endpoint count.
|
|
28
|
+
# The `--compact` summary is the first (often only) thing an agent reads, so it must
|
|
29
|
+
# not assert "rest api" when the authoritative `endpoints` command finds almost no
|
|
30
|
+
# high-confidence surface. Below this many high-confidence endpoints we degrade the
|
|
31
|
+
# headline to a qualified, consistent phrasing instead of overclaiming.
|
|
32
|
+
_MIN_REST_ENDPOINTS_FOR_LABEL = 5
|
|
33
|
+
|
|
26
34
|
_OPTIONAL_LABEL_MAP: dict[str, str] = {
|
|
27
35
|
"DependencyAnalyzer": "dependencias",
|
|
28
36
|
"GraphAnalyzer": "grafo de módulos",
|
|
@@ -41,6 +49,7 @@ class ArchitectureSummarizer:
|
|
|
41
49
|
|
|
42
50
|
def __init__(self, root: Path) -> None:
|
|
43
51
|
self.root = root
|
|
52
|
+
self._endpoint_support_cache: tuple[int, int] | None = None
|
|
44
53
|
|
|
45
54
|
def generate(self, sm: SourceMap) -> str | None:
|
|
46
55
|
try:
|
|
@@ -185,9 +194,39 @@ class ArchitectureSummarizer:
|
|
|
185
194
|
|
|
186
195
|
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
187
196
|
if runtime:
|
|
197
|
+
# BUG #4: never assert "rest api" in the headline unless the endpoints
|
|
198
|
+
# command actually backs it (Java/Kotlin only — that is where we have an
|
|
199
|
+
# authoritative extractor). Degrade to a qualified, consistent phrasing.
|
|
200
|
+
if sm.project_type == "api" and primary.stack in {"java", "kotlin"}:
|
|
201
|
+
total, high = self._endpoint_support()
|
|
202
|
+
if high < _MIN_REST_ENDPOINTS_FOR_LABEL:
|
|
203
|
+
plural = "s" if total != 1 else ""
|
|
204
|
+
return (
|
|
205
|
+
f"{stack_label} application{fw_str} "
|
|
206
|
+
f"(HTTP framework present; only {total} endpoint{plural} "
|
|
207
|
+
f"detected — see `endpoints`)."
|
|
208
|
+
)
|
|
188
209
|
return f"{stack_label} {runtime.lower()}{fw_str}."
|
|
189
210
|
return f"{stack_label} project{fw_str}."
|
|
190
211
|
|
|
212
|
+
def _endpoint_support(self) -> tuple[int, int]:
|
|
213
|
+
"""Return (total, high_confidence) endpoint counts from the canonical
|
|
214
|
+
Java endpoint extractor — the same source the `endpoints` command uses,
|
|
215
|
+
so the summary cannot diverge from it. Cached; failure degrades to (0, 0)."""
|
|
216
|
+
if self._endpoint_support_cache is not None:
|
|
217
|
+
return self._endpoint_support_cache
|
|
218
|
+
total, high = 0, 0
|
|
219
|
+
try:
|
|
220
|
+
from sourcecode.repository_ir import extract_java_endpoints
|
|
221
|
+
data = extract_java_endpoints(self.root)
|
|
222
|
+
eps = data.get("endpoints", [])
|
|
223
|
+
total = data.get("total", len(eps))
|
|
224
|
+
high = sum(1 for e in eps if (e.get("confidence") or "high") == "high")
|
|
225
|
+
except Exception:
|
|
226
|
+
total, high = 0, 0
|
|
227
|
+
self._endpoint_support_cache = (total, high)
|
|
228
|
+
return self._endpoint_support_cache
|
|
229
|
+
|
|
191
230
|
def _describe_arch_pattern(self, arch: Any) -> str:
|
|
192
231
|
pattern_labels = {
|
|
193
232
|
"clean": "Clean Architecture",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from collections.abc import Iterable, Sequence
|
|
4
5
|
from dataclasses import replace
|
|
5
6
|
from typing import Any, Literal
|
|
@@ -104,6 +105,21 @@ class TypeClassifier:
|
|
|
104
105
|
stack_names = {stack.stack for stack in stacks}
|
|
105
106
|
framework_names = {framework.name for stack in stacks for framework in stack.frameworks}
|
|
106
107
|
|
|
108
|
+
# BUG #4 (JobRunr field test): a framework present only in a small optional
|
|
109
|
+
# integration/adapter submodule must NOT label the whole repo as that
|
|
110
|
+
# framework's app type. JobRunr is a framework-agnostic background-job
|
|
111
|
+
# LIBRARY whose `core` module holds ~85% of the code; Quarkus/Micronaut/Spring
|
|
112
|
+
# appear only in tiny per-framework adapter modules — yet presence-based
|
|
113
|
+
# classification returned project_type="api"+Quarkus. We weight by code
|
|
114
|
+
# locality: if the DOMINANT source module (the one with the most source files)
|
|
115
|
+
# contains no evidence of the app-defining framework, the framework is an
|
|
116
|
+
# optional adapter and the repo is a library. A monolithic Spring app is
|
|
117
|
+
# unaffected — its dominant module *does* use the framework. Drop such
|
|
118
|
+
# localized frameworks from the set that drives the app-type decision below.
|
|
119
|
+
_app_frameworks = framework_names & (_WEB_FRAMEWORKS | _API_FRAMEWORKS)
|
|
120
|
+
_localized = self._localized_adapter_frameworks(file_tree, stacks, _app_frameworks)
|
|
121
|
+
framework_names = framework_names - _localized
|
|
122
|
+
|
|
107
123
|
if len(stack_names) >= 2 and self._is_fullstack(stacks):
|
|
108
124
|
return "fullstack"
|
|
109
125
|
|
|
@@ -129,9 +145,16 @@ class TypeClassifier:
|
|
|
129
145
|
if framework_names & _API_FRAMEWORKS:
|
|
130
146
|
return "api"
|
|
131
147
|
|
|
148
|
+
# All app-defining frameworks were localized to optional adapter submodules
|
|
149
|
+
# (multi-module library with per-framework integrations) — report library,
|
|
150
|
+
# never "unknown", when there is clearly source code present.
|
|
151
|
+
if _localized and not (framework_names & (_WEB_FRAMEWORKS | _API_FRAMEWORKS)):
|
|
152
|
+
return "library"
|
|
153
|
+
|
|
154
|
+
# Strong CLI signals: a CLI framework or an explicit cli entry point.
|
|
132
155
|
if framework_names & _CLI_FRAMEWORKS or any(
|
|
133
156
|
entry.kind == "cli" for entry in entry_points
|
|
134
|
-
)
|
|
157
|
+
):
|
|
135
158
|
return "cli"
|
|
136
159
|
|
|
137
160
|
if stack_names:
|
|
@@ -141,8 +164,105 @@ class TypeClassifier:
|
|
|
141
164
|
if single in {"cpp", "dotnet"} and any(entry.kind == "cli" for entry in entry_points):
|
|
142
165
|
return "cli"
|
|
143
166
|
|
|
167
|
+
# BUG #4 (JobRunr field test): a multi-module JVM repo with no app-defining
|
|
168
|
+
# web/API framework is a library/toolkit, not an "unknown" — never let the
|
|
169
|
+
# first command of an audit emit a vacuous classification for a clearly
|
|
170
|
+
# structured codebase (e.g. JobRunr: core + per-framework adapter modules).
|
|
171
|
+
# This is checked BEFORE the weak `bin/`-directory CLI heuristic so a build
|
|
172
|
+
# output / wrapper `bin/` dir does not mislabel a library as a CLI.
|
|
173
|
+
if stack_names & {"java", "kotlin", "scala"} and self._is_multi_module(file_tree):
|
|
174
|
+
return "library"
|
|
175
|
+
|
|
176
|
+
# Weak CLI heuristic: a top-level bin/ directory (only when nothing stronger).
|
|
177
|
+
if any(path.startswith("bin/") for path in flat_paths):
|
|
178
|
+
return "cli"
|
|
179
|
+
|
|
144
180
|
return "unknown" if stacks else None
|
|
145
181
|
|
|
182
|
+
def _is_multi_module(self, file_tree: dict[str, Any]) -> bool:
|
|
183
|
+
"""True when the repo has >1 source module (distinct `*/src/...` roots)."""
|
|
184
|
+
_CODE_EXTS = (".java", ".kt", ".kts", ".scala", ".groovy")
|
|
185
|
+
modules = {
|
|
186
|
+
self._module_of(p)
|
|
187
|
+
for p in flatten_file_tree(file_tree)
|
|
188
|
+
if p.endswith(_CODE_EXTS)
|
|
189
|
+
}
|
|
190
|
+
modules.discard("")
|
|
191
|
+
return len(modules) >= 2
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _module_of(path: str) -> str:
|
|
195
|
+
"""Group a source path into its module root.
|
|
196
|
+
|
|
197
|
+
For Maven/Gradle layouts the module is everything before `/src/`
|
|
198
|
+
(e.g. `framework-support/jobrunr-quarkus/src/main/java/...` →
|
|
199
|
+
`framework-support/jobrunr-quarkus`). Otherwise the top-level directory.
|
|
200
|
+
"""
|
|
201
|
+
norm = path.replace("\\", "/")
|
|
202
|
+
idx = norm.find("/src/")
|
|
203
|
+
if idx > 0:
|
|
204
|
+
return norm[:idx]
|
|
205
|
+
head, _, tail = norm.partition("/")
|
|
206
|
+
return head if tail else ""
|
|
207
|
+
|
|
208
|
+
_EVIDENCE_PATH_RE = re.compile(r"\(([^()]+)\)\s*$")
|
|
209
|
+
|
|
210
|
+
def _localized_adapter_frameworks(
|
|
211
|
+
self,
|
|
212
|
+
file_tree: dict[str, Any],
|
|
213
|
+
stacks: Sequence[StackDetection],
|
|
214
|
+
candidate_frameworks: set[str],
|
|
215
|
+
) -> set[str]:
|
|
216
|
+
"""Frameworks confined to a minority module while a framework-agnostic
|
|
217
|
+
module dominates the codebase (library + per-framework adapters).
|
|
218
|
+
|
|
219
|
+
Returns the subset of ``candidate_frameworks`` that should NOT drive the
|
|
220
|
+
project-type decision. A framework qualifies only when (a) the repo is
|
|
221
|
+
multi-module, (b) the framework's evidence files are all outside the
|
|
222
|
+
dominant source module, and (c) the framework has located evidence files
|
|
223
|
+
(a manifest-only/root detection applies repo-wide and never localizes).
|
|
224
|
+
"""
|
|
225
|
+
if not candidate_frameworks:
|
|
226
|
+
return set()
|
|
227
|
+
|
|
228
|
+
_CODE_EXTS = (".java", ".kt", ".kts", ".scala", ".groovy")
|
|
229
|
+
module_file_counts: dict[str, int] = {}
|
|
230
|
+
for p in flatten_file_tree(file_tree):
|
|
231
|
+
if not p.endswith(_CODE_EXTS):
|
|
232
|
+
continue
|
|
233
|
+
mod = self._module_of(p)
|
|
234
|
+
module_file_counts[mod] = module_file_counts.get(mod, 0) + 1
|
|
235
|
+
|
|
236
|
+
# Need a genuine multi-module repo to reason about locality.
|
|
237
|
+
if len(module_file_counts) < 2:
|
|
238
|
+
return set()
|
|
239
|
+
dominant_module = max(module_file_counts, key=lambda m: module_file_counts[m])
|
|
240
|
+
|
|
241
|
+
# Collect evidence file paths per framework from detected_via.
|
|
242
|
+
evidence: dict[str, set[str]] = {}
|
|
243
|
+
for stack in stacks:
|
|
244
|
+
for fw in stack.frameworks:
|
|
245
|
+
if fw.name not in candidate_frameworks:
|
|
246
|
+
continue
|
|
247
|
+
paths = evidence.setdefault(fw.name, set())
|
|
248
|
+
for ev in fw.detected_via:
|
|
249
|
+
if ev.startswith("manifest:"):
|
|
250
|
+
continue
|
|
251
|
+
m = self._EVIDENCE_PATH_RE.search(ev)
|
|
252
|
+
if m:
|
|
253
|
+
paths.add(m.group(1).strip())
|
|
254
|
+
|
|
255
|
+
localized: set[str] = set()
|
|
256
|
+
for fw_name in candidate_frameworks:
|
|
257
|
+
files = evidence.get(fw_name) or set()
|
|
258
|
+
if not files:
|
|
259
|
+
# No locatable evidence (manifest-only) → applies repo-wide.
|
|
260
|
+
continue
|
|
261
|
+
modules = {self._module_of(f) for f in files}
|
|
262
|
+
if dominant_module not in modules:
|
|
263
|
+
localized.add(fw_name)
|
|
264
|
+
return localized
|
|
265
|
+
|
|
146
266
|
def _is_fullstack(self, stacks: Sequence[StackDetection]) -> bool:
|
|
147
267
|
has_web = False
|
|
148
268
|
has_api = False
|
|
@@ -90,6 +90,19 @@ _GRADLE_JAVA_VERSION_RE = re.compile(
|
|
|
90
90
|
_GRADLE_JAVA_ENUM_RE = re.compile(
|
|
91
91
|
r"""(?:sourceCompatibility|targetCompatibility)\s*=\s*JavaVersion\.VERSION_(\d+)"""
|
|
92
92
|
)
|
|
93
|
+
# BUG #4: a gradle line that genuinely declares a dependency or plugin. Matches a
|
|
94
|
+
# quoted Maven coordinate (group:artifact, requires the colon), a plugins-block
|
|
95
|
+
# `id '...'`, an `apply plugin:`, or a `classpath`/`platform(`/`enforcedPlatform(`
|
|
96
|
+
# declaration. Deliberately does NOT match `it.name.contains('quarkus')`-style
|
|
97
|
+
# subproject filters (a bare quoted name with no colon and no plugin/dep keyword).
|
|
98
|
+
_GRADLE_DEP_LINE_RE = re.compile(
|
|
99
|
+
r"""['"][\w.\-]+:[\w.\-]+""" # maven coordinate "group:artifact..."
|
|
100
|
+
r"""|\bclasspath\b"""
|
|
101
|
+
r"""|\b(?:enforced)?platform\s*\("""
|
|
102
|
+
r"""|\bapply\s+plugin\s*:"""
|
|
103
|
+
r"""|\bid\s*[('"]""",
|
|
104
|
+
re.IGNORECASE,
|
|
105
|
+
)
|
|
93
106
|
|
|
94
107
|
|
|
95
108
|
class JavaDetector(AbstractDetector):
|
|
@@ -279,7 +292,29 @@ class JavaDetector(AbstractDetector):
|
|
|
279
292
|
original = "\n".join(read_text_lines(path))
|
|
280
293
|
content = original.lower()
|
|
281
294
|
sb_version = self._extract_gradle_sb_version(original)
|
|
282
|
-
|
|
295
|
+
# BUG #4 (JobRunr field test): framework tokens must be matched only inside
|
|
296
|
+
# genuine dependency / plugin declarations, never anywhere in the file. A
|
|
297
|
+
# multi-module root build.gradle that EXCLUDES a subproject by name —
|
|
298
|
+
# `configure(subprojects.findAll { !it.name.contains('quarkus') })` — must
|
|
299
|
+
# not be read as "this project uses Quarkus". Restrict the scan to lines
|
|
300
|
+
# carrying a Maven coordinate / plugin id; the exclusion filter has neither.
|
|
301
|
+
dep_text = self._gradle_dependency_text(content)
|
|
302
|
+
return self._detect_jvm_frameworks(dep_text, "build.gradle", sb_version=sb_version)
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def _gradle_dependency_text(content: str) -> str:
|
|
306
|
+
"""Keep only gradle lines that declare a dependency or plugin.
|
|
307
|
+
|
|
308
|
+
A real dependency carries a Maven coordinate (`group:artifact`...) and a
|
|
309
|
+
plugin carries an `id`/`plugin`/`classpath` token. Subproject-name filters
|
|
310
|
+
and arbitrary prose are dropped, so a framework substring there cannot
|
|
311
|
+
manufacture a phantom framework detection.
|
|
312
|
+
"""
|
|
313
|
+
kept: list[str] = []
|
|
314
|
+
for line in content.splitlines():
|
|
315
|
+
if _GRADLE_DEP_LINE_RE.search(line):
|
|
316
|
+
kept.append(line)
|
|
317
|
+
return "\n".join(kept)
|
|
283
318
|
|
|
284
319
|
def _extract_gradle_sb_version(self, content: str) -> str | None:
|
|
285
320
|
m = _GRADLE_SB_PLUGIN_RE.search(content)
|