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