sourcecode 1.69.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.69.0 → sourcecode-1.70.0}/CHANGELOG.md +71 -2
- {sourcecode-1.69.0 → sourcecode-1.70.0}/PKG-INFO +1 -1
- {sourcecode-1.69.0 → sourcecode-1.70.0}/pyproject.toml +1 -1
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/architecture_analyzer.py +39 -6
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/architecture_summary.py +39 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/hibernate_strat.py +96 -21
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/integration_detector.py +42 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/migrate_check.py +24 -3
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/spring_impact.py +30 -3
- {sourcecode-1.69.0 → sourcecode-1.70.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/.gitignore +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/.ruff.toml +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/LICENSE +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/README.md +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/SECURITY.md +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/raw +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.69.0 → sourcecode-1.70.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,66 @@
|
|
|
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
|
+
|
|
3
64
|
## [1.69.0] — 2026-06-30
|
|
4
65
|
|
|
5
66
|
### Fixed — framing & symbol-graph fidelity on a non-framework Java library (JobRunr field test)
|
|
@@ -57,8 +118,16 @@ suites.
|
|
|
57
118
|
emits the misleading "No stereotype detected — may be a plain class or utility" for
|
|
58
119
|
a structurally central non-annotated class: it infers a low-confidence stereotype
|
|
59
120
|
from in-degree, lifecycle methods, and naming (e.g. `BackgroundJobServer` →
|
|
60
|
-
"Likely server/orchestrator … lifecycle methods detected (start/stop)").
|
|
61
|
-
`
|
|
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".
|
|
62
131
|
|
|
63
132
|
## [1.68.0] — 2026-06-30
|
|
64
133
|
|
|
@@ -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",
|
|
@@ -262,15 +262,33 @@ def _scan_build_dependency(root: Path) -> tuple[bool, list[str]]:
|
|
|
262
262
|
# ≥ 6 the 5→6 axis does NOT apply (the migration is already done). When it cannot
|
|
263
263
|
# be resolved the axis degrades to a low-confidence hypothesis (no headline).
|
|
264
264
|
_HIB_VER = r"(\d+\.\d+(?:\.\d+)?(?:[.\-][\w]+)*)" # 6.2.13.Final, 5.6.15, 6.2
|
|
265
|
+
_HIB_VER_ANCHORED = re.compile(r"^" + _HIB_VER + r"$")
|
|
265
266
|
_HIB_PROP_RE = re.compile(
|
|
266
267
|
r"<((?:[\w.\-]*hibernate[\w.\-]*?)\.?version)>\s*" + _HIB_VER + r"\s*</\1>",
|
|
267
268
|
re.IGNORECASE,
|
|
268
269
|
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
# A Hibernate property is ONLY a proxy for the ORM version when it is not the
|
|
271
|
+
# version of a sibling Hibernate artifact (Search, Validator, Envers, OGM, …).
|
|
272
|
+
# Those ship on their own version line (e.g. hibernate-search 6.x on a
|
|
273
|
+
# hibernate-core 5.x project) and must never be mistaken for the ORM version.
|
|
274
|
+
# BUG #1 (v1.70.0): openmrs pinned hibernate-core to 5.6.15 via ${hibernateVersion}
|
|
275
|
+
# while ${hibernateSearchVersion}=6.2.4 — the old "newest wins" scan picked the
|
|
276
|
+
# Search version and declared the project already on Hibernate 6.
|
|
277
|
+
_HIB_NON_CORE_PROP_RE = re.compile(
|
|
278
|
+
r"search|validator|envers|ogm|reactive|spatial|jpamodelgen|"
|
|
279
|
+
r"jpa-?model|gradle|tool|commons|metamodel",
|
|
280
|
+
re.IGNORECASE,
|
|
281
|
+
)
|
|
282
|
+
# Anchor: capture the <version> declared inside the hibernate-core/orm
|
|
283
|
+
# <dependency> block (the value may be a literal or a ${property} reference).
|
|
284
|
+
_HIB_CORE_DEP_RE = re.compile(
|
|
285
|
+
r"<dependency>(?P<body>(?:(?!</dependency>).)*?"
|
|
286
|
+
r"<artifactId>\s*hibernate-(?:core|orm)\s*</artifactId>"
|
|
287
|
+
r"(?:(?!</dependency>).)*?)</dependency>",
|
|
272
288
|
re.IGNORECASE | re.DOTALL,
|
|
273
289
|
)
|
|
290
|
+
_VERSION_TAG_RE = re.compile(r"<version>\s*([^<]+?)\s*</version>", re.IGNORECASE)
|
|
291
|
+
_PROP_REF_RE = re.compile(r"^\$\{([^}]+)\}$")
|
|
274
292
|
_HIB_GRADLE_VERSION_RE = re.compile(
|
|
275
293
|
r"org\.hibernate(?:\.orm)?:hibernate-(?:core|orm):" + _HIB_VER,
|
|
276
294
|
re.IGNORECASE,
|
|
@@ -290,37 +308,88 @@ def _ver_key(full: str) -> tuple[int, int]:
|
|
|
290
308
|
return (major, minor)
|
|
291
309
|
|
|
292
310
|
|
|
311
|
+
def _lookup_maven_property(texts: list[str], name: str) -> Optional[str]:
|
|
312
|
+
"""Resolve a single Maven property by exact tag name across build files."""
|
|
313
|
+
pat = re.compile(
|
|
314
|
+
r"<" + re.escape(name) + r">\s*([^<]+?)\s*</" + re.escape(name) + r">",
|
|
315
|
+
re.IGNORECASE,
|
|
316
|
+
)
|
|
317
|
+
for t in texts:
|
|
318
|
+
m = pat.search(t)
|
|
319
|
+
if m:
|
|
320
|
+
return m.group(1).strip()
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _resolve_version_expr(texts: list[str], expr: str, depth: int = 0) -> Optional[str]:
|
|
325
|
+
"""Resolve a <version> value that may be a literal or a ${property} chain."""
|
|
326
|
+
expr = expr.strip()
|
|
327
|
+
ref = _PROP_REF_RE.match(expr)
|
|
328
|
+
if ref:
|
|
329
|
+
if depth > 5:
|
|
330
|
+
return None
|
|
331
|
+
val = _lookup_maven_property(texts, ref.group(1))
|
|
332
|
+
if val is None:
|
|
333
|
+
return None
|
|
334
|
+
return _resolve_version_expr(texts, val, depth + 1)
|
|
335
|
+
return expr if _HIB_VER_ANCHORED.match(expr) else None
|
|
336
|
+
|
|
337
|
+
|
|
293
338
|
def _resolve_hibernate_version(root: Path) -> tuple[Optional[str], Optional[int], str]:
|
|
294
339
|
"""Return (full_version_string, major, confidence) for the effective Hibernate.
|
|
295
340
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
341
|
+
Resolution order (BUG #1 — never trust a same-named-but-different artifact):
|
|
342
|
+
1. The <version> anchored to the hibernate-core / hibernate-orm dependency
|
|
343
|
+
(literal, or a ${property} resolved to that specific property), and the
|
|
344
|
+
Gradle org.hibernate(.orm):hibernate-(core|orm) coordinate. HIGH conf.
|
|
345
|
+
2. Fallback: a <hibernate*version> property that is NOT a sibling-artifact
|
|
346
|
+
version (Search/Validator/Envers/…). HIGH conf.
|
|
347
|
+
3. None resolvable → degrade to hypothesis (confidence "none").
|
|
348
|
+
The newest qualifying version wins in a multi-module build.
|
|
299
349
|
"""
|
|
300
350
|
import os
|
|
301
|
-
|
|
351
|
+
texts: list[str] = []
|
|
302
352
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
303
353
|
dirnames[:] = [d for d in dirnames if d not in _SKIP_BUILD_SCAN_DIRS]
|
|
304
354
|
for fname in filenames:
|
|
305
355
|
if fname not in _BUILD_FILE_NAMES:
|
|
306
356
|
continue
|
|
307
357
|
try:
|
|
308
|
-
|
|
358
|
+
texts.append(
|
|
359
|
+
(Path(dirpath) / fname).read_text(encoding="utf-8", errors="replace")
|
|
360
|
+
)
|
|
309
361
|
except OSError:
|
|
310
362
|
continue
|
|
311
|
-
|
|
312
|
-
for m in _HIB_PROP_RE.finditer(text):
|
|
313
|
-
fulls.append(m.group(2))
|
|
314
|
-
for m in _HIB_COORD_VERSION_RE.finditer(text):
|
|
315
|
-
fulls.append(m.group(1))
|
|
316
|
-
for m in _HIB_GRADLE_VERSION_RE.finditer(text):
|
|
317
|
-
fulls.append(m.group(1))
|
|
318
|
-
for full in fulls:
|
|
319
|
-
if best_full is None or _ver_key(full) > _ver_key(best_full):
|
|
320
|
-
best_full = full
|
|
321
|
-
if best_full is None:
|
|
363
|
+
if not texts:
|
|
322
364
|
return None, None, "none"
|
|
323
|
-
|
|
365
|
+
|
|
366
|
+
# 1. Anchored to the hibernate-core / hibernate-orm coordinate.
|
|
367
|
+
anchored: list[str] = []
|
|
368
|
+
for text in texts:
|
|
369
|
+
for dep in _HIB_CORE_DEP_RE.finditer(text):
|
|
370
|
+
vm = _VERSION_TAG_RE.search(dep.group("body"))
|
|
371
|
+
if vm:
|
|
372
|
+
resolved = _resolve_version_expr(texts, vm.group(1))
|
|
373
|
+
if resolved:
|
|
374
|
+
anchored.append(resolved)
|
|
375
|
+
for m in _HIB_GRADLE_VERSION_RE.finditer(text):
|
|
376
|
+
anchored.append(m.group(1))
|
|
377
|
+
if anchored:
|
|
378
|
+
best = max(anchored, key=_ver_key)
|
|
379
|
+
return best, _ver_key(best)[0], "high"
|
|
380
|
+
|
|
381
|
+
# 2. Fallback: a Hibernate *property* that is not a sibling-artifact version.
|
|
382
|
+
prop_cands: list[str] = []
|
|
383
|
+
for text in texts:
|
|
384
|
+
for m in _HIB_PROP_RE.finditer(text):
|
|
385
|
+
if _HIB_NON_CORE_PROP_RE.search(m.group(1)):
|
|
386
|
+
continue
|
|
387
|
+
prop_cands.append(m.group(2))
|
|
388
|
+
if prop_cands:
|
|
389
|
+
best = max(prop_cands, key=_ver_key)
|
|
390
|
+
return best, _ver_key(best)[0], "high"
|
|
391
|
+
|
|
392
|
+
return None, None, "none"
|
|
324
393
|
|
|
325
394
|
# Escalation markers — dynamic / reflection-based persistence construction.
|
|
326
395
|
_ABSTRACTION_CLASS_RE = re.compile(
|
|
@@ -495,7 +564,13 @@ class HibernateStratification:
|
|
|
495
564
|
"classification": self.classification,
|
|
496
565
|
"classification_label": self.classification_label,
|
|
497
566
|
"stratified": True,
|
|
498
|
-
"hibernate_readiness"
|
|
567
|
+
# BUG #1 (v1.70.0): renamed from "hibernate_readiness" to avoid a
|
|
568
|
+
# same-name contradiction with the document-level migrate-check field.
|
|
569
|
+
# This is the RAW 5→6 rewrite-zone readiness (independent of whether the
|
|
570
|
+
# axis applies). The authoritative, applicability-gated migration score
|
|
571
|
+
# lives at the document root as "hibernate_readiness"; consult that one
|
|
572
|
+
# for decisions. They diverge by design when the axis is N/A.
|
|
573
|
+
"rewrite_zone_readiness": self.readiness,
|
|
499
574
|
"risk_matrix": [r.to_dict() for r in self.risk_matrix],
|
|
500
575
|
"module_exposure_map": self.module_exposure,
|
|
501
576
|
"incompatible_patterns": self.incompatible_patterns,
|
|
@@ -200,6 +200,27 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
200
200
|
has_mail_import = bool(_MAIL_IMPORT_RE.search(text))
|
|
201
201
|
naming_factory = _classify_naming_factory(text)
|
|
202
202
|
|
|
203
|
+
# BUG #3 (v1.70.0): "HttpClient" is a simple name that collides with
|
|
204
|
+
# user-defined classes (e.g. org.openmrs.util.HttpClient, a thin wrapper over
|
|
205
|
+
# java.net.HttpURLConnection — a completely different API from the JDK 11+
|
|
206
|
+
# java.net.http.HttpClient). Resolve the JDK client by its FULLY-QUALIFIED
|
|
207
|
+
# import, never by the bare class name. When the file imports/declares a
|
|
208
|
+
# different HttpClient (or none can be resolved), degrade to a low-confidence
|
|
209
|
+
# "custom-http-wrapper" rather than asserting a JDK client that isn't there.
|
|
210
|
+
import_fqns = set(
|
|
211
|
+
re.findall(r"^\s*import\s+(?:static\s+)?([\w.]+)\s*;", text, re.MULTILINE)
|
|
212
|
+
)
|
|
213
|
+
http_jdk_imported = (
|
|
214
|
+
"java.net.http.HttpClient" in import_fqns or "java.net.http.*" in import_fqns
|
|
215
|
+
)
|
|
216
|
+
declares_own_httpclient = bool(
|
|
217
|
+
re.search(r"\b(?:class|interface|enum)\s+HttpClient\b", text)
|
|
218
|
+
)
|
|
219
|
+
http_other_import = any(
|
|
220
|
+
fqn.endswith(".HttpClient") and not fqn.startswith("java.net.http.")
|
|
221
|
+
for fqn in import_fqns
|
|
222
|
+
)
|
|
223
|
+
|
|
203
224
|
# Token clients — per line, skipping imports/package/comment noise.
|
|
204
225
|
# First pass records the declaration site and any variable name bound to
|
|
205
226
|
# the client, so a later call site (where the URL literal usually lives)
|
|
@@ -241,6 +262,27 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
241
262
|
kind, client, confidence = (
|
|
242
263
|
"naming-directory-unknown", "jndi-dircontext", "low",
|
|
243
264
|
)
|
|
265
|
+
# BUG #3: resolve the ambiguous bare "HttpClient" by its import, and
|
|
266
|
+
# suppress pure type-declaration sites (field / parameter / return
|
|
267
|
+
# type) — only a construction or static call is a real network site.
|
|
268
|
+
if client == "jdk-httpclient":
|
|
269
|
+
if not (http_jdk_imported and not declares_own_httpclient):
|
|
270
|
+
# Not the JDK client (own class, third-party, or unresolvable).
|
|
271
|
+
client, confidence = "custom-http-wrapper", "low"
|
|
272
|
+
if declares_own_httpclient or http_other_import:
|
|
273
|
+
confidence = "low"
|
|
274
|
+
is_construction = bool(
|
|
275
|
+
re.search(r"\bnew\s+HttpClient\b", line)
|
|
276
|
+
) or bool(re.search(r"\bHttpClient\s*\.", line))
|
|
277
|
+
if not is_construction:
|
|
278
|
+
# Type-declaration only (e.g. `HttpClient field;`,
|
|
279
|
+
# `void setX(HttpClient c)`): track the var for the URL
|
|
280
|
+
# second pass but do NOT emit it as an invocation site.
|
|
281
|
+
tok = m.group(0)
|
|
282
|
+
decl = re.search(re.escape(tok) + r"\s+(\w+)\b", line)
|
|
283
|
+
if decl:
|
|
284
|
+
var_to_client[decl.group(1)] = (kind, client)
|
|
285
|
+
continue
|
|
244
286
|
_add(kind, client, _extract_target(line), rel, lineno, confidence)
|
|
245
287
|
tok = m.group(0)
|
|
246
288
|
# `Type name` (field/local decl) and `name = new Type(` forms.
|
|
@@ -1287,6 +1287,7 @@ class MigrationReport:
|
|
|
1287
1287
|
readiness_aggregate: dict = field(default_factory=dict)
|
|
1288
1288
|
blocking_count: int = 0
|
|
1289
1289
|
estimated_effort_days: float = 0.0
|
|
1290
|
+
effort_breakdown: dict = field(default_factory=dict)
|
|
1290
1291
|
# Tri-state: True = Boot 2 confirmed, False = Boot 3+ confirmed,
|
|
1291
1292
|
# None = could not determine. Absence of evidence is never reported as True.
|
|
1292
1293
|
spring_boot_2_detected: Optional[bool] = None
|
|
@@ -1499,13 +1500,32 @@ class MigrationReport:
|
|
|
1499
1500
|
|
|
1500
1501
|
# BUG #5: effort over MAIN findings only — N/A axes (Hibernate-6 phantom,
|
|
1501
1502
|
# test fixtures) no longer pad the estimate.
|
|
1502
|
-
|
|
1503
|
+
_file_effort = (
|
|
1503
1504
|
len(critical_files) * 0.5
|
|
1504
1505
|
+ len(high_files) * 0.25
|
|
1505
1506
|
+ len(medium_files) * 0.1
|
|
1506
|
-
+ len(low_files) * 0.05
|
|
1507
|
-
1,
|
|
1507
|
+
+ len(low_files) * 0.05
|
|
1508
1508
|
)
|
|
1509
|
+
# BUG #1 (v1.70.0): when the Hibernate 5→6 axis APPLIES, fold its measured
|
|
1510
|
+
# rewrite effort (risk_matrix → total_effort_range_days) into the headline
|
|
1511
|
+
# estimate. Previously a Hibernate-5 project whose ${hibernateVersion} was
|
|
1512
|
+
# misread as 6 set migration_applicable=False, which silently DROPPED this
|
|
1513
|
+
# 28.9–95.6 person-day range from estimated_effort_days, under-reporting the
|
|
1514
|
+
# real cost 1.5–2.6×. We add the range midpoint and expose the breakdown.
|
|
1515
|
+
_hib_effort = 0.0
|
|
1516
|
+
if _hibernate_applies and hib is not None:
|
|
1517
|
+
_r = hib.total_effort_range_days or {}
|
|
1518
|
+
_lo, _hi = _r.get("low"), _r.get("high")
|
|
1519
|
+
if isinstance(_lo, (int, float)) and isinstance(_hi, (int, float)):
|
|
1520
|
+
_hib_effort = (float(_lo) + float(_hi)) / 2.0
|
|
1521
|
+
self.estimated_effort_days = round(_file_effort + _hib_effort, 1)
|
|
1522
|
+
self.effort_breakdown = {
|
|
1523
|
+
"findings_effort_days": round(_file_effort, 1),
|
|
1524
|
+
"hibernate_rewrite_effort_days": round(_hib_effort, 1),
|
|
1525
|
+
"hibernate_rewrite_range": (
|
|
1526
|
+
hib.total_effort_range_days if (_hibernate_applies and hib is not None) else None
|
|
1527
|
+
),
|
|
1528
|
+
}
|
|
1509
1529
|
|
|
1510
1530
|
# BUG #6 / #8: hygiene + non-blocking buckets, surfaced separately.
|
|
1511
1531
|
self.hygiene_findings = sum(
|
|
@@ -1561,6 +1581,7 @@ class MigrationReport:
|
|
|
1561
1581
|
"headline_blocker": self.headline_blocker,
|
|
1562
1582
|
"blocking_count": self.blocking_count,
|
|
1563
1583
|
"estimated_effort_days": self.estimated_effort_days,
|
|
1584
|
+
"effort_breakdown": self.effort_breakdown,
|
|
1564
1585
|
"hygiene_findings": self.hygiene_findings,
|
|
1565
1586
|
"non_blocking": self.non_blocking,
|
|
1566
1587
|
"spring_present": self.spring_present,
|
|
@@ -121,6 +121,8 @@ class ImpactChainResult:
|
|
|
121
121
|
resolution: str = "not_found" # "exact" | "class_expanded" | "partial" | "not_found"
|
|
122
122
|
direct_callers: list[str] = field(default_factory=list)
|
|
123
123
|
indirect_callers: list[str] = field(default_factory=list)
|
|
124
|
+
# BUG #2: count of own-class members dropped from callers (members, not callers).
|
|
125
|
+
self_referential_excluded: int = 0
|
|
124
126
|
implementations: list[str] = field(default_factory=list) # in-repo subtypes of queried interface/base
|
|
125
127
|
endpoints_affected: list[AffectedEndpoint] = field(default_factory=list)
|
|
126
128
|
transaction_boundary: Optional[dict] = None # TransactionBoundary.to_dict() or None
|
|
@@ -144,6 +146,7 @@ class ImpactChainResult:
|
|
|
144
146
|
"resolution": self.resolution,
|
|
145
147
|
"direct_callers": self.direct_callers,
|
|
146
148
|
"indirect_callers": self.indirect_callers,
|
|
149
|
+
"self_referential_excluded": self.self_referential_excluded,
|
|
147
150
|
"implementations": self.implementations,
|
|
148
151
|
"endpoints_affected": [ep.to_dict() for ep in self.endpoints_affected],
|
|
149
152
|
"transaction_boundary": self.transaction_boundary,
|
|
@@ -471,6 +474,15 @@ def _bfs_callers(
|
|
|
471
474
|
indirect: list[str] = []
|
|
472
475
|
was_truncated = False
|
|
473
476
|
|
|
477
|
+
# BUG #2 (v1.70.0): a class's OWN members are not "callers" of that class — they
|
|
478
|
+
# are members. When the seed is a class node, _edges_for folds in every method
|
|
479
|
+
# key of that class, so internal method→method calls (e.g.
|
|
480
|
+
# ConceptServiceImpl#purgeConcept → ConceptServiceImpl#saveConcept) were leaking
|
|
481
|
+
# into direct_callers and inflating the blast radius ~12×. Exclude any caller
|
|
482
|
+
# whose owning class is one of the seed classes.
|
|
483
|
+
seed_classes: set[str] = {normalize_owner_fqn(s) for s in seed_fqns}
|
|
484
|
+
self_excluded: int = 0
|
|
485
|
+
|
|
474
486
|
# BUG-004: index class FQN → list of method-level keys in reverse_graph.
|
|
475
487
|
# Callers of Foo#doWork are stored under reverse_graph["Foo#doWork"], never
|
|
476
488
|
# under reverse_graph["Foo"]. Without this index, BFS silently terminates
|
|
@@ -506,7 +518,9 @@ def _bfs_callers(
|
|
|
506
518
|
for seed in seed_fqns:
|
|
507
519
|
for etype, fqn_list in _edges_for(seed):
|
|
508
520
|
if etype not in _SKIP_EDGE_TYPES:
|
|
509
|
-
unique_direct_callers.update(
|
|
521
|
+
unique_direct_callers.update(
|
|
522
|
+
c for c in fqn_list if normalize_owner_fqn(c) not in seed_classes
|
|
523
|
+
)
|
|
510
524
|
|
|
511
525
|
effective_depth = 1 if len(unique_direct_callers) > _BFS_CALLER_CAP else max_depth
|
|
512
526
|
if effective_depth < max_depth:
|
|
@@ -516,8 +530,14 @@ def _bfs_callers(
|
|
|
516
530
|
queue: list[tuple[str, int]] = [(s, 0) for s in seed_fqns]
|
|
517
531
|
|
|
518
532
|
def _add_caller(caller: str, depth: int) -> None:
|
|
533
|
+
nonlocal self_excluded
|
|
519
534
|
if caller in visited:
|
|
520
535
|
return
|
|
536
|
+
# BUG #2: a member of a seed class is not an external caller — drop it.
|
|
537
|
+
if normalize_owner_fqn(caller) in seed_classes:
|
|
538
|
+
visited.add(caller)
|
|
539
|
+
self_excluded += 1
|
|
540
|
+
return
|
|
521
541
|
visited.add(caller)
|
|
522
542
|
if depth == 0:
|
|
523
543
|
direct.append(caller)
|
|
@@ -542,7 +562,7 @@ def _bfs_callers(
|
|
|
542
562
|
else:
|
|
543
563
|
_add_caller(caller, depth)
|
|
544
564
|
|
|
545
|
-
return direct, indirect, was_truncated
|
|
565
|
+
return direct, indirect, was_truncated, self_excluded
|
|
546
566
|
|
|
547
567
|
|
|
548
568
|
# ---------------------------------------------------------------------------
|
|
@@ -888,9 +908,15 @@ class ImpactOrchestrator:
|
|
|
888
908
|
)
|
|
889
909
|
|
|
890
910
|
# ── 2. BFS through reverse graph ─────────────────────────────────
|
|
891
|
-
direct_callers, indirect_callers, truncated = _bfs_callers(
|
|
911
|
+
direct_callers, indirect_callers, truncated, self_excluded = _bfs_callers(
|
|
892
912
|
seed_fqns, cir.reverse_graph, depth, impl_graph=impl_graph
|
|
893
913
|
)
|
|
914
|
+
if self_excluded:
|
|
915
|
+
warnings.append(
|
|
916
|
+
f"Self-referential exclusion (BUG #2): {self_excluded} member(s) of the "
|
|
917
|
+
f"analyzed class were dropped from callers — a class's own methods are "
|
|
918
|
+
f"members, not external callers (they do not count toward blast radius)."
|
|
919
|
+
)
|
|
894
920
|
if truncated:
|
|
895
921
|
warnings.append(
|
|
896
922
|
"Hub-class guard active: symbol has > 500 direct callers — "
|
|
@@ -1083,6 +1109,7 @@ class ImpactOrchestrator:
|
|
|
1083
1109
|
resolution=resolution,
|
|
1084
1110
|
direct_callers=direct_callers,
|
|
1085
1111
|
indirect_callers=indirect_callers,
|
|
1112
|
+
self_referential_excluded=self_excluded,
|
|
1086
1113
|
implementations=sorted(subtype_classes_added),
|
|
1087
1114
|
endpoints_affected=endpoints_affected,
|
|
1088
1115
|
transaction_boundary=tx_boundary,
|
|
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
|