sourcecode 1.62.0__py3-none-any.whl → 1.63.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.62.0"
3
+ __version__ = "1.63.0"
sourcecode/cli.py CHANGED
@@ -4841,9 +4841,12 @@ def migrate_check_cmd(
4841
4841
  \b
4842
4842
  Hibernate 5→6 stratification (in the 'hibernate' output section):
4843
4843
  4 independent layers (JPA annotations / Criteria / HQL / SPI), a per-layer
4844
- risk matrix, a module exposure map, critical call-chain detection, and an
4845
- UPGRADE vs REWRITE verdict (dynamic Criteria, custom SPI, reflection-built
4846
- queries, or concatenated query strings force the REWRITE classification).
4844
+ risk matrix with effort ranges, a module exposure map, critical call-chain
4845
+ detection, golden-SQL hotspots, a hibernate_readiness score, and an UPGRADE
4846
+ vs REWRITE verdict (dynamic Criteria, custom SPI, reflection-built queries,
4847
+ or concatenated query strings force the REWRITE classification).
4848
+ Emits actionable rewrite_targets[] (call-site line ranges → target_api +
4849
+ migration_kind) so a migration agent can consume the output directly.
4847
4850
 
4848
4851
  \b
4849
4852
  Examples:
@@ -10,25 +10,35 @@ independent migration domains and classifies each one on its own risk axis:
10
10
  3. HQL / String-based Queries — MEDIUM, escalates to HIGH on concatenation
11
11
  4. Hibernate SPI / Internal — CRITICAL BLOCKER (UserType, Interceptor, SPI)
12
12
 
13
- It also produces:
13
+ Beyond the diagnostic summary it emits **actionable, machine-readable rewrite
14
+ targets** at call-site granularity (`rewrite_targets[]`) so a migration agent can
15
+ consume the output directly instead of re-parsing the repository. It also produces:
14
16
  - a module-level Hibernate exposure map,
15
- - critical call-chain detection (DynamicEntityDao, BasicPersistenceModule, ),
16
- - stop-condition logic that decides UPGRADE ZONE vs REWRITE ZONE,
17
- - separation of observable code risk from inferred runtime risk.
17
+ - per-layer manual/assisted/mechanical sub-counts (static vs dynamic Criteria),
18
+ - honest effort ranges (low/high/confidence) + an auditable effort model,
19
+ - a hibernate_readiness score (0-100),
20
+ - critical call-chain detection and golden-SQL instrumentation hotspots,
21
+ - separation of observable code risk from inferred runtime risk,
22
+ - an UPGRADE-vs-REWRITE verdict via stop-condition logic.
18
23
 
19
24
  Entry point: analyze_hibernate(file_paths, root) → HibernateStratification
20
25
 
21
- This is purely additive: it does not aggregate Hibernate into a single score, and
22
- it does not assume that a Spring Boot upgrade resolves ORM risk.
26
+ This is purely additive and does not aggregate Hibernate into a single score.
23
27
  """
24
28
  from __future__ import annotations
25
29
 
30
+ import bisect
31
+ import hashlib
26
32
  import re
27
33
  from dataclasses import dataclass, field
28
34
  from pathlib import Path
29
35
  from typing import Optional
30
36
 
31
37
 
38
+ # Sub-schema version for the `hibernate` output section. Bump on shape changes.
39
+ HIBERNATE_SCHEMA_VERSION = "2.0"
40
+
41
+
32
42
  # ---------------------------------------------------------------------------
33
43
  # Layer identifiers
34
44
  # ---------------------------------------------------------------------------
@@ -44,6 +54,9 @@ _LAYER_TITLES: dict[str, str] = {
44
54
  LAYER_HQL: "HQL / String-based Queries",
45
55
  LAYER_SPI: "Hibernate SPI / Internal API",
46
56
  }
57
+ _LAYER_CODE: dict[str, str] = {
58
+ LAYER_JPA: "JPA", LAYER_CRITERIA: "CRIT", LAYER_HQL: "HQL", LAYER_SPI: "SPI",
59
+ }
47
60
 
48
61
  # Classification verdicts
49
62
  CLASS_NONE = "none"
@@ -60,6 +73,19 @@ _CLASS_LABELS: dict[str, str] = {
60
73
 
61
74
  _SEVERITY_RANK: dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3}
62
75
 
76
+ # Migration kinds (ascending automatability)
77
+ KIND_MANUAL = "manual_rewrite"
78
+ KIND_ASSISTED = "assisted"
79
+ KIND_MECHANICAL = "mechanical"
80
+ KIND_REVIEW = "review"
81
+
82
+ # Effort range multipliers by confidence band: (low_mult, high_mult).
83
+ _CONF_MULT: dict[str, tuple[float, float]] = {
84
+ "high": (0.9, 1.2),
85
+ "medium": (0.7, 1.5),
86
+ "low": (0.5, 2.0),
87
+ }
88
+
63
89
 
64
90
  # ---------------------------------------------------------------------------
65
91
  # Detection patterns
@@ -73,7 +99,7 @@ _JPA_ANNOTATION_RE = re.compile(
73
99
  )
74
100
  # Hibernate / JPA annotations deprecated or reworked in Hibernate 6 (escalate).
75
101
  _JPA_DEPRECATED_RE = re.compile(
76
- r"@TypeDef\b|@TypeDefs\b|"
102
+ r"@TypeDefs?\b|"
77
103
  r"org\.hibernate\.annotations\.Type\b|"
78
104
  r"@Type\s*\(\s*type\s*=|"
79
105
  r"@GenericGenerator\b|"
@@ -83,8 +109,7 @@ _JPA_DEPRECATED_RE = re.compile(
83
109
  # Layer 2 — JPA Criteria API + legacy Hibernate Criteria.
84
110
  _CRITERIA_JPA_RE = re.compile(
85
111
  r"\bCriteriaBuilder\b|\bCriteriaQuery\b|\bCriteriaUpdate\b|"
86
- r"\bCriteriaDelete\b|\bRoot\s*<|\bPredicate\b|\.criteria\b|"
87
- r"persistence\.criteria"
112
+ r"\bCriteriaDelete\b|\bRoot\s*<|\bPredicate\b|persistence\.criteria"
88
113
  )
89
114
  _CRITERIA_LEGACY_RE = re.compile(
90
115
  r"org\.hibernate\.Criteria\b|\.createCriteria\s*\(|\bDetachedCriteria\b|"
@@ -133,21 +158,65 @@ _REFLECTION_RE = re.compile(
133
158
  r"\.getMetamodel\s*\("
134
159
  )
135
160
 
161
+ # Lightweight symbol indexing.
162
+ _CLASS_DECL_RE = re.compile(r"\b(?:class|interface|enum)\s+(\w+)")
163
+ _METHOD_DECL_RE = re.compile(r"\b(\w+)\s*\([^;{)]*\)\s*\{")
164
+ _NON_METHOD_NAMES = frozenset({
165
+ "if", "for", "while", "switch", "catch", "synchronized", "try", "return",
166
+ "new", "do", "else", "case", "super", "this",
167
+ })
168
+
136
169
 
137
170
  # ---------------------------------------------------------------------------
138
171
  # Data model
139
172
  # ---------------------------------------------------------------------------
140
173
 
174
+ @dataclass
175
+ class RewriteTarget:
176
+ id: str
177
+ layer: str
178
+ source_file: str
179
+ line_start: int
180
+ line_end: int
181
+ current_pattern: str
182
+ current_snippet: str
183
+ target_api: str
184
+ migration_kind: str
185
+ auto_migratable: bool
186
+ blocking_reason: str
187
+ symbol: str
188
+ module: str
189
+ dynamic: bool = False
190
+
191
+ def to_dict(self) -> dict:
192
+ return {
193
+ "id": self.id,
194
+ "layer": self.layer,
195
+ "source_file": self.source_file,
196
+ "line_start": self.line_start,
197
+ "line_end": self.line_end,
198
+ "current_pattern": self.current_pattern,
199
+ "current_snippet": self.current_snippet,
200
+ "target_api": self.target_api,
201
+ "migration_kind": self.migration_kind,
202
+ "auto_migratable": self.auto_migratable,
203
+ "blocking_reason": self.blocking_reason,
204
+ "symbol": self.symbol,
205
+ "module": self.module,
206
+ "dynamic": self.dynamic,
207
+ }
208
+
209
+
141
210
  @dataclass
142
211
  class HibernateFinding:
143
212
  layer: str
144
213
  severity: str
145
- pattern: str # short human label of what matched
214
+ pattern: str
146
215
  source_file: str
147
216
  first_line: int
148
217
  module: str
149
218
  occurrences: int = 1
150
- dynamic: bool = False # escalated via reflection / abstraction DAO context
219
+ dynamic: bool = False
151
220
 
152
221
  def to_dict(self) -> dict:
153
222
  return {
@@ -167,20 +236,44 @@ class HibernateLayerRisk:
167
236
  layer: str
168
237
  risk: str
169
238
  reason: str
170
- estimated_effort: str
239
+ estimated_effort: str # legacy string "~75.8d" (compat)
171
240
  file_count: int
172
241
  occurrence_count: int
242
+ effort_range: dict = field(default_factory=dict) # {low, high, confidence}
243
+ # Migration-kind breakdown (derived from rewrite_targets of this layer).
244
+ manual_count: int = 0
245
+ assisted_count: int = 0
246
+ mechanical_count: int = 0
247
+ review_count: int = 0
248
+ # Criteria-specific.
249
+ static_count: Optional[int] = None
250
+ dynamic_count: Optional[int] = None
251
+ # SPI-specific.
252
+ userType_rewrite_count: Optional[int] = None
253
+ userType_resolvable_count: Optional[int] = None
173
254
 
174
255
  def to_dict(self) -> dict:
175
- return {
256
+ d: dict = {
176
257
  "layer": self.layer,
177
258
  "layer_title": _LAYER_TITLES.get(self.layer, self.layer),
178
259
  "risk": self.risk,
179
260
  "reason": self.reason,
180
261
  "estimated_effort": self.estimated_effort,
262
+ "effort_range": self.effort_range,
181
263
  "file_count": self.file_count,
182
264
  "occurrence_count": self.occurrence_count,
265
+ "manual_count": self.manual_count,
266
+ "assisted_count": self.assisted_count,
267
+ "mechanical_count": self.mechanical_count,
268
+ "review_count": self.review_count,
183
269
  }
270
+ if self.static_count is not None:
271
+ d["static_count"] = self.static_count
272
+ d["dynamic_count"] = self.dynamic_count
273
+ if self.userType_rewrite_count is not None:
274
+ d["userType_rewrite_count"] = self.userType_rewrite_count
275
+ d["userType_resolvable_count"] = self.userType_resolvable_count
276
+ return d
184
277
 
185
278
 
186
279
  @dataclass
@@ -188,26 +281,37 @@ class HibernateStratification:
188
281
  detected: bool = False
189
282
  classification: str = CLASS_NONE
190
283
  classification_label: str = _CLASS_LABELS[CLASS_NONE]
284
+ readiness: int = 100
191
285
  risk_matrix: list[HibernateLayerRisk] = field(default_factory=list)
192
286
  module_exposure: dict[str, dict] = field(default_factory=dict)
193
287
  incompatible_patterns: list[str] = field(default_factory=list)
194
288
  critical_call_chains: list[dict] = field(default_factory=list)
289
+ rewrite_targets: list[RewriteTarget] = field(default_factory=list)
290
+ golden_sql_hotspots: list[dict] = field(default_factory=list)
195
291
  stop_conditions_triggered: list[str] = field(default_factory=list)
196
292
  observable_risk: list[str] = field(default_factory=list)
197
293
  inferred_runtime_risk: list[str] = field(default_factory=list)
294
+ total_effort_range_days: dict = field(default_factory=dict)
295
+ effort_model: dict = field(default_factory=dict)
198
296
  findings: list[HibernateFinding] = field(default_factory=list)
199
297
 
200
298
  def to_dict(self) -> dict:
201
299
  return {
300
+ "schema_version": HIBERNATE_SCHEMA_VERSION,
202
301
  "detected": self.detected,
203
302
  "classification": self.classification,
204
303
  "classification_label": self.classification_label,
205
304
  "stratified": True,
305
+ "hibernate_readiness": self.readiness,
206
306
  "risk_matrix": [r.to_dict() for r in self.risk_matrix],
207
307
  "module_exposure_map": self.module_exposure,
208
308
  "incompatible_patterns": self.incompatible_patterns,
209
309
  "critical_call_chains": self.critical_call_chains,
310
+ "rewrite_targets": [t.to_dict() for t in self.rewrite_targets],
311
+ "golden_sql_hotspots": self.golden_sql_hotspots,
210
312
  "stop_conditions_triggered": self.stop_conditions_triggered,
313
+ "total_effort_range_days": self.total_effort_range_days,
314
+ "effort_model": self.effort_model,
211
315
  "risk_separation": {
212
316
  "observable_code_risk": self.observable_risk,
213
317
  "inferred_runtime_risk": self.inferred_runtime_risk,
@@ -222,14 +326,22 @@ class HibernateStratification:
222
326
  lines: list[str] = [
223
327
  "── Hibernate 5.x → 6.x Migration Stratification ──",
224
328
  f"Classification: {self.classification_label}",
329
+ f"Hibernate readiness: {self.readiness}/100 "
330
+ f"Rewrite targets: {len(self.rewrite_targets)} "
331
+ f"Effort: {self.total_effort_range_days.get('low', '?')}–"
332
+ f"{self.total_effort_range_days.get('high', '?')}d "
333
+ f"({self.total_effort_range_days.get('confidence', '?')} confidence)",
225
334
  "",
226
335
  "Hibernate Migration Impact Matrix:",
227
- f" {'Layer':<30} {'Risk':<9} Effort Reason",
336
+ f" {'Layer':<30} {'Risk':<9} {'Effort':<13} Breakdown",
228
337
  ]
229
338
  for r in self.risk_matrix:
339
+ bd = f"manual={r.manual_count} assisted={r.assisted_count} mech={r.mechanical_count}"
340
+ if r.static_count is not None:
341
+ bd += f" (static={r.static_count}/dyn={r.dynamic_count})"
230
342
  lines.append(
231
343
  f" {_LAYER_TITLES.get(r.layer, r.layer):<30} "
232
- f"{r.risk.upper():<9} {r.estimated_effort:<13} {r.reason}"
344
+ f"{r.risk.upper():<9} {r.estimated_effort:<13} {bd}"
233
345
  )
234
346
 
235
347
  lines.append("")
@@ -249,8 +361,7 @@ class HibernateStratification:
249
361
  tag_str = f" [{', '.join(tags)}]" if tags else ""
250
362
  layers = ", ".join(sorted(info.get("layers", {}).keys()))
251
363
  lines.append(
252
- f" {mod:<40} {info.get('max_risk', 'low').upper():<9} "
253
- f"{layers}{tag_str}"
364
+ f" {mod:<40} {info.get('max_risk', 'low').upper():<9} {layers}{tag_str}"
254
365
  )
255
366
  else:
256
367
  lines.append(" (no module attribution)")
@@ -263,6 +374,24 @@ class HibernateStratification:
263
374
  f" {cc['class']} ({cc['source_file']}:{cc['first_line']}) — {cc['reason']}"
264
375
  )
265
376
 
377
+ if self.golden_sql_hotspots:
378
+ lines.append("")
379
+ lines.append("Golden-SQL Instrumentation Hotspots (where to pin behaviour tests):")
380
+ for h in self.golden_sql_hotspots:
381
+ lines.append(
382
+ f" {h['symbol']:<35} {h['dynamic_query_count']:>4} dyn-queries "
383
+ f"({h['source_file']})"
384
+ )
385
+
386
+ if self.rewrite_targets:
387
+ lines.append("")
388
+ lines.append(f"Rewrite Targets (first 15 of {len(self.rewrite_targets)}):")
389
+ for t in self.rewrite_targets[:15]:
390
+ lines.append(
391
+ f" [{t.migration_kind}] {t.source_file}:{t.line_start} "
392
+ f"{t.symbol} → {t.target_api}"
393
+ )
394
+
266
395
  if self.incompatible_patterns:
267
396
  lines.append("")
268
397
  lines.append("Incompatible / High-risk Patterns Found:")
@@ -307,21 +436,150 @@ def _module_of(rel_path: str) -> str:
307
436
  return "root"
308
437
 
309
438
 
310
- def _first_line(source: str, match: re.Match) -> int:
311
- return source[: match.start()].count("\n") + 1
439
+ def _line_index(source: str) -> list[int]:
440
+ """Offsets of every newline — for O(log n) offset→line lookups via bisect."""
441
+ return [i for i, c in enumerate(source) if c == "\n"]
442
+
443
+
444
+ def _line_at(nl: list[int], pos: int) -> int:
445
+ return bisect.bisect_right(nl, pos) + 1
446
+
447
+
448
+ def _index_symbols(source: str, nl: list[int]) -> tuple[list[tuple[int, str]], list[tuple[int, str]]]:
449
+ classes = [(_line_at(nl, m.start()), m.group(1)) for m in _CLASS_DECL_RE.finditer(source)]
450
+ methods = [
451
+ (_line_at(nl, m.start()), m.group(1))
452
+ for m in _METHOD_DECL_RE.finditer(source)
453
+ if m.group(1) not in _NON_METHOD_NAMES
454
+ ]
455
+ return classes, methods
456
+
457
+
458
+ def _nearest(decls: list[tuple[int, str]], line: int) -> Optional[str]:
459
+ best: Optional[str] = None
460
+ for dl, name in decls:
461
+ if dl <= line:
462
+ best = name
463
+ else:
464
+ break
465
+ return best
466
+
467
+
468
+ def _symbol_at(line: int, classes: list[tuple[int, str]], methods: list[tuple[int, str]]) -> str:
469
+ cls = _nearest(classes, line)
470
+ meth = _nearest(methods, line)
471
+ if cls and meth:
472
+ return f"{cls}#{meth}"
473
+ return cls or meth or ""
474
+
475
+
476
+ def _snippet(lines: list[str], ls: int, le: int) -> str:
477
+ seg = " ".join(l.strip() for l in lines[ls - 1: le])
478
+ seg = re.sub(r"\s+", " ", seg).strip()
479
+ return (seg[:157] + "...") if len(seg) > 160 else seg
312
480
 
313
481
 
314
482
  def _count(pattern: re.Pattern, source: str) -> int:
315
483
  return sum(1 for _ in pattern.finditer(source))
316
484
 
317
485
 
486
+ def _make_id(layer: str, source_file: str, line: int, pattern: str) -> str:
487
+ h = hashlib.sha256(f"{source_file}:{line}:{pattern}".encode()).hexdigest()[:10]
488
+ return f"HB6-{_LAYER_CODE.get(layer, 'GEN')}-{h}"
489
+
490
+
318
491
  # ---------------------------------------------------------------------------
319
- # Effort heuristics
492
+ # Target-mapping rules (current pattern → Hibernate 6 destination)
320
493
  # ---------------------------------------------------------------------------
321
494
 
322
- def _effort_bucket(file_count: int, base_days_per_file: float, floor: float = 0.5) -> str:
323
- days = round(max(floor, file_count * base_days_per_file), 1) if file_count else 0.0
324
- return f"~{days}d"
495
+ def _criteria_target(legacy: bool, dynamic: bool) -> tuple[str, str, bool, str]:
496
+ if legacy:
497
+ return ("jakarta.persistence.criteria.CriteriaBuilder",
498
+ KIND_MANUAL, False,
499
+ "org.hibernate.Criteria / Restrictions / Projections removed in Hibernate 6")
500
+ if dynamic:
501
+ return ("jakarta.persistence.criteria.CriteriaBuilder (dynamic builder rewrite)",
502
+ KIND_MANUAL, False,
503
+ "dynamic Criteria construction via reflection / abstraction DAO")
504
+ return ("jakarta.persistence.criteria (validate Hibernate 6 semantics)",
505
+ KIND_ASSISTED, False,
506
+ "JPA Criteria metamodel/semantics changed in Hibernate 6")
507
+
508
+
509
+ def _jpa_deprecated_target(text: str) -> tuple[str, str, bool, str]:
510
+ if "@TypeDef" in text:
511
+ return ("@Type(value=...) (Hibernate 6 — @TypeDef removed)",
512
+ KIND_ASSISTED, False, "@TypeDef / @TypeDefs removed in Hibernate 6")
513
+ if "@GenericGenerator" in text:
514
+ return ("@GenericGenerator (Hibernate 6 — strategy/parameters reworked)",
515
+ KIND_ASSISTED, False, "@GenericGenerator reworked in Hibernate 6")
516
+ # @Type(type="...") or org.hibernate.annotations.Type
517
+ return ("@Type(value=...) / @JdbcTypeCode (Hibernate 6)",
518
+ KIND_MECHANICAL, True,
519
+ "@Type(type=) attribute renamed/replaced in Hibernate 6 (1:1 mapping)")
520
+
521
+
522
+ def _hql_target(concat: bool) -> tuple[str, str, bool, str]:
523
+ if concat:
524
+ return ("HQL (parameterize + validate Hibernate 6 parser)",
525
+ KIND_ASSISTED, False, "runtime-resolved query string (concatenation)")
526
+ return ("HQL / native query (validate against Hibernate 6 parser)",
527
+ KIND_REVIEW, False, "")
528
+
529
+
530
+ def _spi_target(text: str) -> tuple[str, str, bool, str]:
531
+ t = text
532
+ if "UserCollectionType" in t:
533
+ return ("org.hibernate.usertype.UserCollectionType (redesigned in H6)",
534
+ KIND_MANUAL, False, "UserCollectionType interface redesigned in Hibernate 6")
535
+ if "CompositeUserType" in t:
536
+ return ("org.hibernate.usertype.CompositeUserType (redesigned in H6)",
537
+ KIND_MANUAL, False, "CompositeUserType redesigned in Hibernate 6")
538
+ if "UserType" in t:
539
+ return ("org.hibernate.usertype.UserType (new method signatures in H6)",
540
+ KIND_MANUAL, False, "UserType interface redesigned in Hibernate 6")
541
+ if "EmptyInterceptor" in t:
542
+ return ("org.hibernate.Interceptor (EmptyInterceptor removed in H6)",
543
+ KIND_MANUAL, False, "EmptyInterceptor removed in Hibernate 6")
544
+ if "Interceptor" in t:
545
+ return ("org.hibernate.Interceptor (default methods in H6)",
546
+ KIND_ASSISTED, False, "Interceptor gained default methods in Hibernate 6")
547
+ if "EventListener" in t:
548
+ return ("Hibernate 6 event SPI",
549
+ KIND_MANUAL, False, "Hibernate event-listener SPI changed in Hibernate 6")
550
+ if "PropertyAccessStrategy" in t:
551
+ return ("org.hibernate.property.access.spi (changed in H6)",
552
+ KIND_MANUAL, False, "PropertyAccess SPI changed in Hibernate 6")
553
+ if "NamingStrategy" in t:
554
+ return ("Hibernate 6 naming-strategy SPI",
555
+ KIND_ASSISTED, False, "Naming strategy SPI adjusted in Hibernate 6")
556
+ if "SessionFactoryImpl" in t or "SessionImplementor" in t or "SharedSessionContractImplementor" in t:
557
+ return ("Hibernate 6 session SPI (SharedSessionContractImplementor)",
558
+ KIND_MANUAL, False, "internal session SPI changed in Hibernate 6")
559
+ return ("Hibernate 6 internal SPI (no stable public equivalent)",
560
+ KIND_MANUAL, False, "internal Hibernate SPI — no direct Hibernate 6 equivalent")
561
+
562
+
563
+ # Point-estimate person-days per affected file, per layer/branch — the audit-able
564
+ # basis for effort_model. Ranges derive from these via _CONF_MULT.
565
+ _EFFORT_RATES: dict[str, float] = {
566
+ "jpa_standard": 0.02,
567
+ "jpa_deprecated": 0.25,
568
+ "criteria_static": 0.4,
569
+ "criteria_dynamic": 0.75,
570
+ "hql_static": 0.15,
571
+ "hql_concat": 0.3,
572
+ "spi": 1.0,
573
+ }
574
+
575
+
576
+ def _effort_point(file_count: int, rate: float, floor: float = 0.5) -> float:
577
+ return round(max(floor, file_count * rate), 1) if file_count else 0.0
578
+
579
+
580
+ def _effort_range(point: float, confidence: str) -> dict:
581
+ lo, hi = _CONF_MULT[confidence]
582
+ return {"low": round(point * lo, 1), "high": round(point * hi, 1), "confidence": confidence}
325
583
 
326
584
 
327
585
  # ---------------------------------------------------------------------------
@@ -329,11 +587,12 @@ def _effort_bucket(file_count: int, base_days_per_file: float, floor: float = 0.
329
587
  # ---------------------------------------------------------------------------
330
588
 
331
589
  def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratification:
332
- """Scan Java sources for stratified Hibernate 5→6 migration risk."""
590
+ """Scan Java sources for stratified Hibernate 5→6 migration risk + rewrite targets."""
333
591
  findings: list[HibernateFinding] = []
334
- # module -> aggregation accumulator
592
+ targets: list[RewriteTarget] = []
335
593
  modules: dict[str, dict] = {}
336
594
  call_chains: list[dict] = []
595
+ hotspots: list[dict] = []
337
596
 
338
597
  layer_files: dict[str, set[str]] = {
339
598
  LAYER_JPA: set(), LAYER_CRITERIA: set(), LAYER_HQL: set(), LAYER_SPI: set()
@@ -346,10 +605,9 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
346
605
  spi_global = False
347
606
  reflection_query_global = False
348
607
  concat_query_global = False
349
-
350
608
  incompatible: set[str] = set()
351
609
 
352
- for rel_path in file_paths:
610
+ for rel_path in sorted(file_paths):
353
611
  if not rel_path.endswith(".java"):
354
612
  continue
355
613
  abs_path = root / rel_path
@@ -358,22 +616,27 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
358
616
  except OSError:
359
617
  continue
360
618
 
361
- # Per-file escalation context.
619
+ nl = _line_index(source)
620
+ src_lines = source.split("\n")
621
+ classes, methods = _index_symbols(source, nl)
622
+ module = _module_of(rel_path)
362
623
  has_abstraction = bool(_ABSTRACTION_CLASS_RE.search(source))
363
624
  has_reflection = bool(_REFLECTION_RE.search(source))
364
- module = _module_of(rel_path)
625
+
365
626
  mod = modules.setdefault(
366
627
  module,
367
628
  {"layers": {}, "max_risk": "low", "criteria_dynamic": False,
368
629
  "has_spi": False, "has_reflection": False, "files": set()},
369
630
  )
631
+ if has_reflection:
632
+ mod["has_reflection"] = True
370
633
 
371
- def _record(layer: str, severity: str, pattern_label: str,
372
- match: re.Match, occ: int, dynamic: bool = False) -> None:
634
+ def _emit_finding(layer: str, severity: str, label: str, line: int,
635
+ occ: int, dynamic: bool = False) -> None:
373
636
  findings.append(HibernateFinding(
374
- layer=layer, severity=severity, pattern=pattern_label,
375
- source_file=rel_path, first_line=_first_line(source, match),
376
- module=module, occurrences=occ, dynamic=dynamic,
637
+ layer=layer, severity=severity, pattern=label,
638
+ source_file=rel_path, first_line=line, module=module,
639
+ occurrences=occ, dynamic=dynamic,
377
640
  ))
378
641
  layer_files[layer].add(rel_path)
379
642
  layer_occ[layer] += occ
@@ -385,71 +648,114 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
385
648
  mod["max_risk"] = severity
386
649
  mod["files"].add(rel_path)
387
650
 
388
- if has_reflection:
389
- mod["has_reflection"] = True
651
+ def _emit_target(layer: str, pattern: str, ls: int, le: int,
652
+ target_api: str, kind: str, auto: bool, reason: str,
653
+ dynamic: bool = False) -> None:
654
+ targets.append(RewriteTarget(
655
+ id=_make_id(layer, rel_path, ls, pattern),
656
+ layer=layer, source_file=rel_path, line_start=ls, line_end=le,
657
+ current_pattern=pattern, current_snippet=_snippet(src_lines, ls, le),
658
+ target_api=target_api, migration_kind=kind, auto_migratable=auto,
659
+ blocking_reason=reason, symbol=_symbol_at(ls, classes, methods),
660
+ module=module, dynamic=dynamic,
661
+ ))
390
662
 
391
663
  # ── Layer 1: JPA annotations ───────────────────────────────────────
392
- m = _JPA_ANNOTATION_RE.search(source)
393
- if m:
394
- _record(LAYER_JPA, "low", "JPA mapping annotations",
395
- m, _count(_JPA_ANNOTATION_RE, source))
396
- dm = _JPA_DEPRECATED_RE.search(source)
397
- if dm:
664
+ ann_n = _count(_JPA_ANNOTATION_RE, source)
665
+ if ann_n:
666
+ am = _JPA_ANNOTATION_RE.search(source)
667
+ _emit_finding(LAYER_JPA, "low", "JPA mapping annotations",
668
+ _line_at(nl, am.start()), ann_n)
669
+ # Deprecated annotations → escalate + emit rewrite targets (per occurrence).
670
+ dep_matches = list(_JPA_DEPRECATED_RE.finditer(source))
671
+ if dep_matches:
398
672
  jpa_deprecated_seen = True
399
- _record(LAYER_JPA, "high", "Deprecated Hibernate annotation (@Type/@TypeDef/@GenericGenerator)",
400
- dm, _count(_JPA_DEPRECATED_RE, source))
401
- incompatible.add("Deprecated Hibernate annotations (@Type(type=)/@TypeDef) — reworked in Hibernate 6")
673
+ first = dep_matches[0]
674
+ _emit_finding(LAYER_JPA, "high",
675
+ "Deprecated Hibernate annotation (@Type/@TypeDef/@GenericGenerator)",
676
+ _line_at(nl, first.start()), len(dep_matches))
677
+ incompatible.add("Deprecated Hibernate annotations (@Type(type=)/@TypeDef) — "
678
+ "reworked in Hibernate 6")
679
+ for m in dep_matches:
680
+ ls = _line_at(nl, m.start())
681
+ api, kind, auto, reason = _jpa_deprecated_target(m.group(0))
682
+ _emit_target(LAYER_JPA, "deprecated-annotation", ls, ls,
683
+ api, kind, auto, reason)
402
684
 
403
685
  # ── Layer 2: Criteria API ──────────────────────────────────────────
404
- crit_m = _CRITERIA_JPA_RE.search(source)
405
- legacy_m = _CRITERIA_LEGACY_RE.search(source)
406
- crit_hit = crit_m or legacy_m
407
- if crit_hit:
408
- dynamic = has_abstraction or has_reflection
686
+ dynamic = has_abstraction or has_reflection
687
+ crit_matches: list[tuple[re.Match, bool]] = (
688
+ [(m, False) for m in _CRITERIA_JPA_RE.finditer(source)]
689
+ + [(m, True) for m in _CRITERIA_LEGACY_RE.finditer(source)]
690
+ )
691
+ if crit_matches:
409
692
  severity = "critical" if dynamic else "high"
410
- occ = _count(_CRITERIA_JPA_RE, source) + _count(_CRITERIA_LEGACY_RE, source)
693
+ first_line = min(_line_at(nl, m.start()) for m, _ in crit_matches)
411
694
  label = ("Dynamic Criteria construction (reflection/abstraction DAO)"
412
695
  if dynamic else "Criteria API usage")
413
- _record(LAYER_CRITERIA, severity, label, crit_hit, occ, dynamic=dynamic)
696
+ _emit_finding(LAYER_CRITERIA, severity, label, first_line,
697
+ len(crit_matches), dynamic=dynamic)
414
698
  if dynamic:
415
699
  criteria_dynamic_global = True
416
700
  mod["criteria_dynamic"] = True
417
701
  incompatible.add("Criteria built dynamically via reflection/abstraction layer "
418
702
  "(Hibernate 6 incompatible patterns likely)")
419
- if legacy_m:
420
- incompatible.add("Legacy org.hibernate.Criteria / Restrictions / Projections "
421
- "(removed in Hibernate 6 must move to JPA CriteriaBuilder)")
703
+ for m, legacy in crit_matches:
704
+ ls = _line_at(nl, m.start())
705
+ api, kind, auto, reason = _criteria_target(legacy, dynamic)
706
+ _emit_target(LAYER_CRITERIA,
707
+ "legacy-criteria" if legacy else "jpa-criteria",
708
+ ls, ls, api, kind, auto, reason, dynamic=dynamic)
709
+ if legacy:
710
+ incompatible.add("Legacy org.hibernate.Criteria / Restrictions / Projections "
711
+ "(removed in Hibernate 6 — move to JPA CriteriaBuilder)")
422
712
 
423
713
  # ── Layer 3: HQL / string queries ──────────────────────────────────
424
- hql_m = _HQL_RE.search(source)
425
- if hql_m:
426
- concat_m = _HQL_CONCAT_RE.search(source)
427
- if concat_m:
714
+ hql_matches = list(_HQL_RE.finditer(source))
715
+ if hql_matches:
716
+ file_has_concat = False
717
+ first_line = _line_at(nl, hql_matches[0].start())
718
+ for m in hql_matches:
719
+ ls = _line_at(nl, m.start())
720
+ window = source[m.start(): m.start() + 400]
721
+ is_concat = bool(_HQL_CONCAT_RE.match("." + window) or _HQL_CONCAT_RE.search(window))
722
+ le = _line_at(nl, m.start() + (window.find(";") if ";" in window else 0))
723
+ le = max(ls, le)
724
+ api, kind, auto, reason = _hql_target(is_concat)
725
+ _emit_target(LAYER_HQL, "concat-query" if is_concat else "static-query",
726
+ ls, le if is_concat else ls, api, kind, auto, reason)
727
+ if is_concat:
728
+ file_has_concat = True
729
+ sev = "high" if file_has_concat else "medium"
730
+ label = ("String-concatenated HQL/native query (dynamic SQL shape)"
731
+ if file_has_concat else "HQL / native string query")
732
+ _emit_finding(LAYER_HQL, sev, label, first_line, len(hql_matches))
733
+ if file_has_concat:
428
734
  concat_query_global = True
429
- _record(LAYER_HQL, "high",
430
- "String-concatenated HQL/native query (dynamic SQL shape)",
431
- concat_m, _count(_HQL_RE, source))
432
735
  incompatible.add("String-concatenated HQL/native queries — HIGH risk under "
433
- "Hibernate 6 ORM parser changes")
434
- else:
435
- _record(LAYER_HQL, "medium", "HQL / native string query",
436
- hql_m, _count(_HQL_RE, source))
736
+ "Hibernate 6 HQL parser changes")
437
737
 
438
738
  # ── Layer 4: Hibernate SPI / internal ──────────────────────────────
439
- spi_m = _SPI_RE.search(source)
440
- if spi_m:
739
+ spi_matches = list(_SPI_RE.finditer(source))
740
+ if spi_matches:
441
741
  spi_global = True
442
742
  mod["has_spi"] = True
443
- _record(LAYER_SPI, "critical", "Hibernate SPI / internal API (UserType/Interceptor/SPI)",
444
- spi_m, _count(_SPI_RE, source))
743
+ first = spi_matches[0]
744
+ _emit_finding(LAYER_SPI, "critical",
745
+ "Hibernate SPI / internal API (UserType/Interceptor/SPI)",
746
+ _line_at(nl, first.start()), len(spi_matches))
445
747
  incompatible.add("Custom Hibernate SPI (UserType/CompositeUserType/Interceptor/"
446
748
  "EventListener) — primary Hibernate 5→6 breakage source")
749
+ for m in spi_matches:
750
+ ls = _line_at(nl, m.start())
751
+ api, kind, auto, reason = _spi_target(m.group(0))
752
+ _emit_target(LAYER_SPI, "hibernate-spi", ls, ls, api, kind, auto, reason)
447
753
 
448
754
  # ── Critical call-chain detection ──────────────────────────────────
449
- if has_abstraction and (crit_hit or has_reflection):
755
+ if has_abstraction and (crit_matches or has_reflection):
450
756
  am = _ABSTRACTION_CLASS_RE.search(source)
451
757
  reason_bits = []
452
- if crit_hit:
758
+ if crit_matches:
453
759
  reason_bits.append("Criteria construction")
454
760
  if has_reflection:
455
761
  reason_bits.append("reflection-based entity metadata")
@@ -457,13 +763,30 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
457
763
  call_chains.append({
458
764
  "class": am.group(0),
459
765
  "source_file": rel_path,
460
- "first_line": _first_line(source, am),
766
+ "first_line": _line_at(nl, am.start()),
461
767
  "reason": "dynamic query generation: " + " + ".join(reason_bits),
462
768
  "module": module,
463
769
  })
770
+ # Golden-SQL hotspot: rank by dynamic query volume in this file.
771
+ dyn_q = len(crit_matches) + _count(_REFLECTION_RE, source)
772
+ hotspots.append({
773
+ "symbol": _symbol_at(_line_at(nl, am.start()), classes, methods) or am.group(0),
774
+ "source_file": rel_path,
775
+ "module": module,
776
+ "dynamic_query_count": dyn_q,
777
+ "reflection": has_reflection,
778
+ "reason": "concentrates runtime-generated queries — pin golden-SQL tests here",
779
+ })
464
780
 
465
781
  strat = HibernateStratification()
466
782
  strat.findings = findings
783
+ strat.rewrite_targets = sorted(
784
+ targets, key=lambda t: (t.source_file, t.line_start, t.layer, t.id)
785
+ )
786
+ strat.critical_call_chains = sorted(call_chains, key=lambda c: (c["source_file"], c["first_line"]))
787
+ strat.golden_sql_hotspots = sorted(
788
+ hotspots, key=lambda h: (-h["dynamic_query_count"], h["source_file"])
789
+ )[:25]
467
790
 
468
791
  if not findings:
469
792
  strat.detected = False
@@ -472,62 +795,98 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
472
795
  return strat
473
796
 
474
797
  strat.detected = True
475
- strat.critical_call_chains = call_chains
476
798
  strat.incompatible_patterns = sorted(incompatible)
477
799
 
478
- # ── Build per-layer risk matrix ────────────────────────────────────────
800
+ # Per-layer target kind tallies.
801
+ def _kind_counts(layer: str) -> dict[str, int]:
802
+ c = {KIND_MANUAL: 0, KIND_ASSISTED: 0, KIND_MECHANICAL: 0, KIND_REVIEW: 0}
803
+ for t in targets:
804
+ if t.layer == layer:
805
+ c[t.migration_kind] = c.get(t.migration_kind, 0) + 1
806
+ return c
807
+
479
808
  matrix: list[HibernateLayerRisk] = []
809
+ point_days: dict[str, float] = {}
480
810
 
481
811
  # Layer 1
482
812
  if layer_files[LAYER_JPA]:
813
+ kc = _kind_counts(LAYER_JPA)
483
814
  if jpa_deprecated_seen:
484
- risk, reason = "high", ("Deprecated Hibernate annotations present "
485
- "(@Type(type=)/@TypeDef/@GenericGenerator reworked in H6)")
486
- effort = _effort_bucket(len(layer_files[LAYER_JPA]), 0.25)
815
+ risk, conf = "high", "medium"
816
+ reason = ("Deprecated Hibernate annotations present "
817
+ "(@Type(type=)/@TypeDef/@GenericGenerator reworked in H6)")
818
+ point = _effort_point(len(layer_files[LAYER_JPA]), _EFFORT_RATES["jpa_deprecated"])
487
819
  else:
488
- risk, reason = "low", ("Standard JPA mapping only — namespace handled by "
489
- "jakarta migration, no Hibernate-6 annotation breakage")
490
- effort = _effort_bucket(len(layer_files[LAYER_JPA]), 0.02, floor=0.2)
820
+ risk, conf = "low", "high"
821
+ reason = ("Standard JPA mapping only namespace handled by jakarta "
822
+ "migration, no Hibernate-6 annotation breakage")
823
+ point = _effort_point(len(layer_files[LAYER_JPA]), _EFFORT_RATES["jpa_standard"], floor=0.2)
824
+ point_days[LAYER_JPA] = point
491
825
  matrix.append(HibernateLayerRisk(
492
- LAYER_JPA, risk, reason, effort,
493
- len(layer_files[LAYER_JPA]), layer_occ[LAYER_JPA]))
826
+ LAYER_JPA, risk, reason, f"~{point}d", len(layer_files[LAYER_JPA]),
827
+ layer_occ[LAYER_JPA], _effort_range(point, conf),
828
+ manual_count=kc[KIND_MANUAL], assisted_count=kc[KIND_ASSISTED],
829
+ mechanical_count=kc[KIND_MECHANICAL], review_count=kc[KIND_REVIEW]))
494
830
 
495
831
  # Layer 2
496
832
  if layer_files[LAYER_CRITERIA]:
833
+ kc = _kind_counts(LAYER_CRITERIA)
834
+ crit_targets = [t for t in targets if t.layer == LAYER_CRITERIA]
835
+ dyn_n = sum(1 for t in crit_targets if t.dynamic)
836
+ stat_n = len(crit_targets) - dyn_n
497
837
  if criteria_dynamic_global:
498
- risk, reason = "critical", ("Criteria constructed dynamically via reflection / "
499
- "abstraction DAOs Hibernate 6 rewrite zone")
500
- effort = _effort_bucket(len(layer_files[LAYER_CRITERIA]), 0.75)
838
+ risk, conf = "critical", "low"
839
+ reason = ("Criteria constructed dynamically via reflection / abstraction DAOs "
840
+ "— Hibernate 6 rewrite zone")
841
+ point = _effort_point(len(layer_files[LAYER_CRITERIA]), _EFFORT_RATES["criteria_dynamic"])
501
842
  else:
502
- risk, reason = "high", ("Criteria API in use — JPA Criteria semantics changed; "
503
- "legacy org.hibernate.Criteria removed in H6")
504
- effort = _effort_bucket(len(layer_files[LAYER_CRITERIA]), 0.4)
843
+ risk, conf = "high", "medium"
844
+ reason = ("Criteria API in use — JPA Criteria semantics changed; "
845
+ "legacy org.hibernate.Criteria removed in H6")
846
+ point = _effort_point(len(layer_files[LAYER_CRITERIA]), _EFFORT_RATES["criteria_static"])
847
+ point_days[LAYER_CRITERIA] = point
505
848
  matrix.append(HibernateLayerRisk(
506
- LAYER_CRITERIA, risk, reason, effort,
507
- len(layer_files[LAYER_CRITERIA]), layer_occ[LAYER_CRITERIA]))
849
+ LAYER_CRITERIA, risk, reason, f"~{point}d", len(layer_files[LAYER_CRITERIA]),
850
+ layer_occ[LAYER_CRITERIA], _effort_range(point, conf),
851
+ manual_count=kc[KIND_MANUAL], assisted_count=kc[KIND_ASSISTED],
852
+ mechanical_count=kc[KIND_MECHANICAL], review_count=kc[KIND_REVIEW],
853
+ static_count=stat_n, dynamic_count=dyn_n))
508
854
 
509
855
  # Layer 3
510
856
  if layer_files[LAYER_HQL]:
857
+ kc = _kind_counts(LAYER_HQL)
511
858
  if concat_query_global:
512
- risk, reason = "high", ("String-concatenated / dynamic queries — HIGH risk under "
513
- "Hibernate 6 HQL parser changes")
514
- effort = _effort_bucket(len(layer_files[LAYER_HQL]), 0.3)
859
+ risk, conf = "high", "medium"
860
+ reason = ("String-concatenated / dynamic queries — HIGH risk under "
861
+ "Hibernate 6 HQL parser changes")
862
+ point = _effort_point(len(layer_files[LAYER_HQL]), _EFFORT_RATES["hql_concat"])
515
863
  else:
516
- risk, reason = "medium", ("Static HQL / native queries — revalidate against "
517
- "Hibernate 6 parser; mostly mechanical")
518
- effort = _effort_bucket(len(layer_files[LAYER_HQL]), 0.15)
864
+ risk, conf = "medium", "high"
865
+ reason = ("Static HQL / native queries — revalidate against Hibernate 6 "
866
+ "parser; mostly mechanical")
867
+ point = _effort_point(len(layer_files[LAYER_HQL]), _EFFORT_RATES["hql_static"])
868
+ point_days[LAYER_HQL] = point
519
869
  matrix.append(HibernateLayerRisk(
520
- LAYER_HQL, risk, reason, effort,
521
- len(layer_files[LAYER_HQL]), layer_occ[LAYER_HQL]))
870
+ LAYER_HQL, risk, reason, f"~{point}d", len(layer_files[LAYER_HQL]),
871
+ layer_occ[LAYER_HQL], _effort_range(point, conf),
872
+ manual_count=kc[KIND_MANUAL], assisted_count=kc[KIND_ASSISTED],
873
+ mechanical_count=kc[KIND_MECHANICAL], review_count=kc[KIND_REVIEW]))
522
874
 
523
875
  # Layer 4
524
876
  if layer_files[LAYER_SPI]:
525
- risk, reason = "critical", ("Custom Hibernate SPI/internal API (UserType, Interceptor, "
526
- "EventListener, engine.spi) — primary H5→H6 breakage")
527
- effort = _effort_bucket(len(layer_files[LAYER_SPI]), 1.0)
877
+ kc = _kind_counts(LAYER_SPI)
878
+ point = _effort_point(len(layer_files[LAYER_SPI]), _EFFORT_RATES["spi"])
879
+ point_days[LAYER_SPI] = point
528
880
  matrix.append(HibernateLayerRisk(
529
- LAYER_SPI, risk, reason, effort,
530
- len(layer_files[LAYER_SPI]), layer_occ[LAYER_SPI]))
881
+ LAYER_SPI, "critical",
882
+ "Custom Hibernate SPI/internal API (UserType, Interceptor, EventListener, "
883
+ "engine.spi) — primary H5→H6 breakage",
884
+ f"~{point}d", len(layer_files[LAYER_SPI]), layer_occ[LAYER_SPI],
885
+ _effort_range(point, "low"),
886
+ manual_count=kc[KIND_MANUAL], assisted_count=kc[KIND_ASSISTED],
887
+ mechanical_count=kc[KIND_MECHANICAL], review_count=kc[KIND_REVIEW],
888
+ userType_rewrite_count=kc[KIND_MANUAL],
889
+ userType_resolvable_count=kc[KIND_ASSISTED]))
531
890
 
532
891
  strat.risk_matrix = matrix
533
892
 
@@ -547,6 +906,31 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
547
906
  }
548
907
  strat.module_exposure = exposure
549
908
 
909
+ # ── Effort aggregation (honest: upper bound, layers may share files) ────
910
+ confidences = [r.effort_range.get("confidence", "low") for r in matrix]
911
+ worst_conf = "low" if "low" in confidences else ("medium" if "medium" in confidences else "high")
912
+ strat.total_effort_range_days = {
913
+ "low": round(sum(r.effort_range.get("low", 0) for r in matrix), 1),
914
+ "high": round(sum(r.effort_range.get("high", 0) for r in matrix), 1),
915
+ "confidence": worst_conf,
916
+ }
917
+ strat.effort_model = {
918
+ "unit": "person-days",
919
+ "point_days_per_file": dict(_EFFORT_RATES),
920
+ "range_multipliers_by_confidence": {k: list(v) for k, v in _CONF_MULT.items()},
921
+ "range_method": "low = point × low_mult, high = point × high_mult (per confidence band)",
922
+ "caveat": ("Per-layer efforts may share files; total_effort_range_days is an upper "
923
+ "bound and is NOT deduplicated across layers."),
924
+ }
925
+
926
+ # ── hibernate_readiness (0-100) ────────────────────────────────────────
927
+ crit_f = {f.source_file for f in findings if f.severity == "critical"}
928
+ high_f = {f.source_file for f in findings if f.severity == "high"}
929
+ med_f = {f.source_file for f in findings if f.severity == "medium"}
930
+ low_f = {f.source_file for f in findings if f.severity == "low"}
931
+ deduction = len(crit_f) * 15 + len(high_f) * 8 + len(med_f) * 3 + min(len(low_f), 15)
932
+ strat.readiness = max(0, 100 - deduction)
933
+
550
934
  # ── Stop-condition logic → upgrade vs rewrite classification ────────────
551
935
  stops: list[str] = []
552
936
  if criteria_dynamic_global:
@@ -565,8 +949,6 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
565
949
  strat.classification = CLASS_REWRITE
566
950
  elif layer_files[LAYER_CRITERIA] or concat_query_global or jpa_deprecated_seen:
567
951
  strat.classification = CLASS_UPGRADE_CARE
568
- elif layer_files[LAYER_HQL] or layer_files[LAYER_JPA]:
569
- strat.classification = CLASS_UPGRADE
570
952
  else:
571
953
  strat.classification = CLASS_UPGRADE
572
954
  strat.classification_label = _CLASS_LABELS[strat.classification]
@@ -582,7 +964,6 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
582
964
  observable.append(f"HQL/native query calls in {len(layer_files[LAYER_HQL])} file(s)")
583
965
  if layer_files[LAYER_SPI]:
584
966
  observable.append(f"Hibernate SPI/internal API in {len(layer_files[LAYER_SPI])} file(s)")
585
-
586
967
  if criteria_dynamic_global or reflection_query_global:
587
968
  inferred.append("Runtime-generated query shapes (reflection/metadata) — exact SQL "
588
969
  "cannot be statically enumerated; breakage surface is larger than "
sourcecode/mcp/server.py CHANGED
@@ -792,7 +792,11 @@ def get_migration_readiness(repo_path: str = ".", min_severity: str = "low") ->
792
792
 
793
793
  Maps to: sourcecode migrate-check <repo_path> --min-severity <min_severity>
794
794
  Returns: MigrationReport with schema_version, readiness_score (0–100; 100=ready to migrate),
795
- jakarta_readiness / boot3_readiness / jdk_modernization (per-dimension 0–100),
795
+ jakarta_readiness / boot3_readiness / jdk_modernization / hibernate_readiness
796
+ (per-dimension 0–100), headline_blocker (e.g. "hibernate_rewrite" or null),
797
+ hibernate (Hibernate 5→6 stratified model: 4-layer risk_matrix, rewrite_targets[]
798
+ with line-level call sites + target_api + migration_kind, module_exposure_map,
799
+ golden_sql_hotspots, total_effort_range_days, classification upgrade/rewrite),
796
800
  blocking_count, estimated_effort_days,
797
801
  spring_boot_2_detected (true|false|null — null=undetermined, never assumed true),
798
802
  spring_boot_version_detected,
@@ -1055,6 +1055,12 @@ class MigrationReport:
1055
1055
  jakarta_readiness: int = 100
1056
1056
  boot3_readiness: int = 100
1057
1057
  jdk_modernization: int = 100
1058
+ # 4th dimension: Hibernate 5→6 rewrite readiness (independent of jakarta/Boot3).
1059
+ # 100 when no Hibernate usage; sinks toward 0 in a rewrite zone.
1060
+ hibernate_readiness: int = 100
1061
+ # Names the dominant blocker class when one dimension dwarfs the headline
1062
+ # score (e.g. "hibernate_rewrite") so a reader of readiness_score is not misled.
1063
+ headline_blocker: Optional[str] = None
1058
1064
  blocking_count: int = 0
1059
1065
  estimated_effort_days: float = 0.0
1060
1066
  # Tri-state: True = Boot 2 confirmed, False = Boot 3+ confirmed,
@@ -1133,6 +1139,14 @@ class MigrationReport:
1133
1139
  self.boot3_readiness = _dimension_score(self.findings, _BOOT3_MIGRATION_TARGETS)
1134
1140
  self.jdk_modernization = _dimension_score(self.findings, None)
1135
1141
 
1142
+ # Hibernate is its own rewrite axis (orthogonal to jakarta/Boot3); it does
1143
+ # NOT sink the headline readiness_score, but is surfaced as a dimension and,
1144
+ # in a rewrite zone, as the headline_blocker so 62/100 is not read as "easy".
1145
+ if self.hibernate is not None and self.hibernate.detected:
1146
+ self.hibernate_readiness = self.hibernate.readiness
1147
+ if self.hibernate.classification == "rewrite_zone":
1148
+ self.headline_blocker = "hibernate_rewrite"
1149
+
1136
1150
  self.estimated_effort_days = round(
1137
1151
  len(critical_files) * 0.5
1138
1152
  + len(high_files) * 0.25
@@ -1160,6 +1174,8 @@ class MigrationReport:
1160
1174
  "jakarta_readiness": self.jakarta_readiness,
1161
1175
  "boot3_readiness": self.boot3_readiness,
1162
1176
  "jdk_modernization": self.jdk_modernization,
1177
+ "hibernate_readiness": self.hibernate_readiness,
1178
+ "headline_blocker": self.headline_blocker,
1163
1179
  "blocking_count": self.blocking_count,
1164
1180
  "estimated_effort_days": self.estimated_effort_days,
1165
1181
  "spring_boot_2_detected": self.spring_boot_2_detected,
@@ -1185,7 +1201,11 @@ class MigrationReport:
1185
1201
  f"Migration Readiness: {self.readiness_score}/100",
1186
1202
  f" jakarta: {self.jakarta_readiness}/100 "
1187
1203
  f"boot3: {self.boot3_readiness}/100 "
1188
- f"jdk-modernization: {self.jdk_modernization}/100",
1204
+ f"jdk-modernization: {self.jdk_modernization}/100 "
1205
+ f"hibernate: {self.hibernate_readiness}/100",
1206
+ *([f" ⚠ Headline blocker: {self.headline_blocker} "
1207
+ f"(readiness_score reflects jakarta/Boot3 only — Hibernate is a separate rewrite axis)"]
1208
+ if self.headline_blocker else []),
1189
1209
  f"Spring Boot 2 detected: {_boot}",
1190
1210
  f"Blocking issues: {self.blocking_count} "
1191
1211
  f"(critical: {self.summary.get('by_severity', {}).get('critical', 0)}, "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.62.0
3
+ Version: 1.63.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
@@ -657,6 +657,58 @@ Detects migration blockers across Java source files, Spring XML config files, an
657
657
 
658
658
  Each finding includes `severity`, `title`, `source_file`, `first_line`, `explanation`, `fix_hint`, `migration_target`, and `openrewrite_recipe` (when an automated recipe exists).
659
659
 
660
+ #### Hibernate 5.x → 6.x stratification (the `hibernate` output section)
661
+
662
+ A Hibernate major upgrade is **not** a single dependency bump for systems that use
663
+ dynamic persistence. `migrate-check` stratifies Hibernate exposure into four
664
+ independent migration domains — never one aggregated score — and emits **actionable,
665
+ machine-readable rewrite targets** so a migration agent can consume the output
666
+ directly instead of re-parsing the repo. Sub-`schema_version`: `2.0`.
667
+
668
+ **Four layers** (each on its own risk axis):
669
+
670
+ | Layer | Baseline | Escalates to |
671
+ |-------|----------|--------------|
672
+ | `jpa_annotations` | LOW (namespace handled by jakarta) | HIGH on deprecated `@Type(type=)` / `@TypeDef` / `@GenericGenerator` |
673
+ | `criteria_api` | HIGH (JPA Criteria semantics changed; legacy `org.hibernate.Criteria` removed) | **CRITICAL** when built via reflection / abstraction DAOs (`DynamicEntityDao`, `GenericDao`, `BasicPersistenceModule`) |
674
+ | `hql_string_queries` | MEDIUM (revalidate against H6 parser) | HIGH on string concatenation (SQL shape not statically inferable) |
675
+ | `hibernate_spi_internal` | CRITICAL blocker | `UserType`, `CompositeUserType`, `Interceptor`, `EventListener`, `org.hibernate.engine.spi` |
676
+
677
+ **Output keys (under `hibernate`):**
678
+
679
+ - `classification` — `upgrade_zone` / `upgrade_with_care` / `rewrite_zone`. Any of
680
+ {dynamic Criteria, custom SPI, reflection-built queries, concatenated query
681
+ strings} forces **`rewrite_zone`** ("HIGH RISK REWRITE ZONE, NOT UPGRADE ZONE").
682
+ - `risk_matrix[]` — per layer: `risk`, `reason`, `effort_range {low, high, confidence}`,
683
+ `file_count`, `occurrence_count`, migration-kind sub-counts (`manual_count`,
684
+ `assisted_count`, `mechanical_count`, `review_count`); Criteria adds
685
+ `static_count` vs `dynamic_count`; SPI adds `userType_rewrite_count` vs
686
+ `userType_resolvable_count`.
687
+ - `rewrite_targets[]` — one actionable target per call site: `id`, `layer`,
688
+ `source_file`, `line_start`/`line_end`, `current_pattern`, `current_snippet`,
689
+ `target_api` (the Hibernate-6 destination), `migration_kind`
690
+ (`manual_rewrite` / `assisted` / `mechanical` / `review`), `auto_migratable`,
691
+ `blocking_reason`, `symbol` (enclosing `Class#method`), `module`, `dynamic`.
692
+ - `module_exposure_map` — per Maven/Gradle module: `max_risk`, layers present, and
693
+ `dynamic-criteria` / `custom-SPI` / `reflection` tags.
694
+ - `critical_call_chains[]` — dynamic query-generation paths (reflection-based DAOs).
695
+ - `golden_sql_hotspots[]` — classes/methods ranked by dynamic-query volume — where
696
+ to pin golden-SQL behaviour tests before migrating.
697
+ - `total_effort_range_days` + `effort_model` — aggregate range plus the auditable
698
+ formula (and the caveat that layers may share files, so the total is an upper bound).
699
+ - `stop_conditions_triggered[]`, `risk_separation` (observable vs inferred runtime risk).
700
+
701
+ The report also exposes **`hibernate_readiness`** (0–100) as a fourth readiness
702
+ dimension alongside `jakarta_readiness` / `boot3_readiness` / `jdk_modernization`.
703
+ Hibernate is an orthogonal rewrite axis, so it does not sink the headline
704
+ `readiness_score`; instead, in a rewrite zone the top-level `headline_blocker` is set
705
+ to `"hibernate_rewrite"` so a reader of the headline score is not misled.
706
+
707
+ ```bash
708
+ # inspect only the Hibernate rewrite targets
709
+ sourcecode migrate-check . --format json | jq '.hibernate.rewrite_targets[]'
710
+ ```
711
+
660
712
  ### `rename-class` — Java class rename
661
713
 
662
714
  ```bash
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=M2ctLJu2Bc_7rapOGaPAxxYG50XM5jz5dbisyWgh-sE,103
1
+ sourcecode/__init__.py,sha256=7g_-jxyy4w0b0JRdZtxAZ89mML035ZUubdqOo6hl_u0,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
@@ -7,7 +7,7 @@ sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
7
7
  sourcecode/canonical_ir.py,sha256=DEwucOPJguLsVtg5cV8mWXNi112l5jmBhv73KGGebVk,24849
8
8
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
9
9
  sourcecode/classifier.py,sha256=hKzg-nQ47htqqIUzSGvYxv15cXrA3KgICTwJmdqal0o,8095
10
- sourcecode/cli.py,sha256=bEArsMTzwzag8xk-d2AkQk2I3WiZmVyoILRu_PtYBY0,276304
10
+ sourcecode/cli.py,sha256=PwLLelj1ACLkWQBCEThD6vhn4sGMn5kEg-6gRX1IAb8,276536
11
11
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
12
12
  sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
13
13
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -28,12 +28,12 @@ sourcecode/format_contract.py,sha256=1cTNqwP8geA2hbQoBHUPgX3_vSh3l8guJT_jmgEnFF8
28
28
  sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
29
29
  sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
30
30
  sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,62403
31
- sourcecode/hibernate_strat.py,sha256=Q_IJpO2jZEDaFPyp0v9svlcB3LetOu56R8Mv7S2F3aU,26165
31
+ sourcecode/hibernate_strat.py,sha256=Xl2gs_uIpjjq4fHtuibi6wGwpeEC-iKYmDSNYC5M6yM,44143
32
32
  sourcecode/integration_detector.py,sha256=ZJqrGwvZ4ee2JTGhlazKk67aZi173HxkhNpl8Yntpd8,6503
33
33
  sourcecode/license.py,sha256=wckiLuiwaE35KMCStUf1gYzleJuFe6qsSyxUQJvit3s,23500
34
34
  sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
35
35
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
36
- sourcecode/migrate_check.py,sha256=2f7f2NCrBmIqh4MzAHthaZ4UQTyAgjIkRPV9Fdi4Nic,65166
36
+ sourcecode/migrate_check.py,sha256=Er4pr_f3_eUS4A5G87hxnPMcQee2hqV9-XaS_sdkdu4,66470
37
37
  sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
38
38
  sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
39
39
  sourcecode/path_filters.py,sha256=EN1RGZRvLq5EcPgpjYV_IyCKVlAQQn2bbpEisQ5LpGg,3780
@@ -91,7 +91,7 @@ sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,17
91
91
  sourcecode/mcp/orchestrator.py,sha256=kT7IssYNyQXVtDf2Q69qrMSTuyJlJ1Rhkp6_EHqc_38,36520
92
92
  sourcecode/mcp/registry.py,sha256=8ot8A9_ccPNUvHgFkUO8lIlbp4XT_UcHR101nc71Gjc,65840
93
93
  sourcecode/mcp/runner.py,sha256=-Dp2qPGRkfNTVen6bKh7WtzQqpcEtsrXoiuajvshlKk,2866
94
- sourcecode/mcp/server.py,sha256=AQN076joOHyKoU7HaISpzPIIN-5271DDn5DqbQsnTaI,63324
94
+ sourcecode/mcp/server.py,sha256=DV7aiUObBGekShSDc7Iu67ISdesByutto3JOFbcYBFk,63693
95
95
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
96
96
  sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
97
97
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
@@ -103,8 +103,8 @@ sourcecode/telemetry/consent.py,sha256=H5z2Wu63pZqbaKucRPoJQJ0zCo4cke9ZlBrJC-MaW
103
103
  sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
104
104
  sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
105
105
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
106
- sourcecode-1.62.0.dist-info/METADATA,sha256=pj1Wd3yqoYh6bYXMq-w9Z4ZF4bqAAAfyfMo4sWaMkzQ,38941
107
- sourcecode-1.62.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
108
- sourcecode-1.62.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
109
- sourcecode-1.62.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
110
- sourcecode-1.62.0.dist-info/RECORD,,
106
+ sourcecode-1.63.0.dist-info/METADATA,sha256=P3a_x2U773iEAxs6a79XDIL1XT4cyAGfY5Y7KKmUQ_I,42339
107
+ sourcecode-1.63.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
108
+ sourcecode-1.63.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
109
+ sourcecode-1.63.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
110
+ sourcecode-1.63.0.dist-info/RECORD,,