sourcecode 1.66.0__py3-none-any.whl → 1.68.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.66.0"
3
+ __version__ = "1.68.0"
sourcecode/cli.py CHANGED
@@ -4896,7 +4896,8 @@ def migrate_check_cmd(
4896
4896
  output, output_path, copy,
4897
4897
  success_msg=(
4898
4898
  f"Migration check written to {output_path} "
4899
- f"(score: {report.readiness_score}/100, {_total} findings)"
4899
+ f"(score: {report.readiness_score if report.readiness_score is not None else 'N/A'}"
4900
+ f"{'/100' if report.readiness_score is not None else ''}, {_total} findings)"
4900
4901
  ),
4901
4902
  )
4902
4903
 
@@ -5524,6 +5525,91 @@ def fix_bug_cmd(
5524
5525
  )
5525
5526
 
5526
5527
 
5528
+ # Method signatures that are almost always FRAMEWORK ENTRY POINTS, not dead code —
5529
+ # invoked by a dispatcher (reflection / XML / SPI), invisible to a static Java
5530
+ # call-graph. Generalizes beyond any one framework.
5531
+ _DYNAMIC_ENTRY_SIGNATURE_RE = __import__("re").compile(
5532
+ r"\(\s*DispatchContext\b" # OFBiz Service Engine service
5533
+ r"|HttpServletRequest\s+\w+\s*,\s*HttpServletResponse" # OFBiz event / servlet handler
5534
+ r"|@(?:Scheduled|PostConstruct|PreDestroy|EventListener|Bean|"
5535
+ r"RequestMapping|GetMapping|PostMapping|Path|GET|POST|Provider|"
5536
+ r"ApplicationScoped|Singleton)\b", # annotation-dispatched entry
5537
+ )
5538
+ # Config file extensions where a framework wires classes by name (XML/props/yaml).
5539
+ _CONFIG_REF_EXTS: frozenset = frozenset({".xml", ".properties", ".yml", ".yaml", ".groovy"})
5540
+ _CONFIG_SCAN_MAX_FILES: int = 12000
5541
+ _CONFIG_SCAN_MAX_BYTES: int = 256 * 1024
5542
+
5543
+
5544
+ def _partition_static_unreferenced(nodes: list[dict], root: Path) -> tuple[list[dict], list[dict]]:
5545
+ """Split zero-degree classes into (truly_unreferenced, framework_dispatched).
5546
+
5547
+ A class with no static callers is NOT necessarily dead: frameworks invoke
5548
+ classes via reflection, XML/SPI config, or annotations that a static call-graph
5549
+ cannot see (e.g. Apache OFBiz Service Engine services, JAX-RS resources,
5550
+ ServiceLoader providers, scheduled beans). We exclude a candidate when EITHER:
5551
+ 1. its source declares a dynamic-entry method signature, OR
5552
+ 2. its simple name / FQN is referenced from a non-Java config file.
5553
+ Whatever survives is reported as *statically_unreferenced* — never a confident
5554
+ "dead zone".
5555
+ """
5556
+ import os
5557
+ if not nodes:
5558
+ return [], []
5559
+ by_simple: dict[str, list[dict]] = {}
5560
+ for n in nodes:
5561
+ simple = (n.get("fqn") or "").rsplit(".", 1)[-1]
5562
+ if simple:
5563
+ by_simple.setdefault(simple, []).append(n)
5564
+
5565
+ dispatched_fqns: set[str] = set()
5566
+
5567
+ # 1. Source-signature allowlist (bounded — candidate set is small).
5568
+ for n in nodes:
5569
+ src = n.get("source_file")
5570
+ if not src:
5571
+ continue
5572
+ try:
5573
+ txt = (root / src).read_text(encoding="utf-8", errors="replace")
5574
+ except OSError:
5575
+ continue
5576
+ if _DYNAMIC_ENTRY_SIGNATURE_RE.search(txt):
5577
+ dispatched_fqns.add(n["fqn"])
5578
+
5579
+ # 2. Config-reference scan — find candidate names wired from XML/props/yaml.
5580
+ unresolved_simple = {s for s, ns in by_simple.items()
5581
+ if any(x["fqn"] not in dispatched_fqns for x in ns)}
5582
+ if unresolved_simple:
5583
+ files_scanned = 0
5584
+ for dirpath, dirnames, filenames in os.walk(root):
5585
+ dirnames[:] = [d for d in dirnames
5586
+ if d not in {".git", "build", "out", "target", "node_modules", ".gradle"}]
5587
+ for fname in filenames:
5588
+ ext = os.path.splitext(fname)[1].lower()
5589
+ if ext not in _CONFIG_REF_EXTS:
5590
+ continue
5591
+ if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
5592
+ break
5593
+ fpath = os.path.join(dirpath, fname)
5594
+ try:
5595
+ with open(fpath, "r", encoding="utf-8", errors="replace") as fh:
5596
+ text = fh.read(_CONFIG_SCAN_MAX_BYTES)
5597
+ except OSError:
5598
+ continue
5599
+ files_scanned += 1
5600
+ for simple in list(unresolved_simple):
5601
+ if simple in text:
5602
+ for x in by_simple.get(simple, []):
5603
+ dispatched_fqns.add(x["fqn"])
5604
+ unresolved_simple.discard(simple)
5605
+ if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
5606
+ break
5607
+
5608
+ unreferenced = [n for n in nodes if n["fqn"] not in dispatched_fqns]
5609
+ dispatched = [n for n in nodes if n["fqn"] in dispatched_fqns]
5610
+ return unreferenced, dispatched
5611
+
5612
+
5527
5613
  @app.command("modernize")
5528
5614
  def modernize_cmd(
5529
5615
  path: Path = typer.Argument(
@@ -5625,13 +5711,21 @@ def modernize_cmd(
5625
5711
  key=lambda n: (-n.get("in_degree", 0), n.get("fqn", "")),
5626
5712
  )[:20]
5627
5713
 
5628
- # Dead zones: symbols with zero in-degree AND zero out-degree (isolated)
5629
- dead_zones = sorted(
5714
+ # Statically-unreferenced zones: classes with zero in-degree AND zero out-degree
5715
+ # in the Java call-graph. These are NOT necessarily dead — framework dispatch
5716
+ # (reflection / XML / SPI / annotations) is invisible to a static graph — so we
5717
+ # partition out framework-dispatched entry points before reporting, and never
5718
+ # call the survivors "dead". (Defect 5: OFBiz Service-Engine services and event
5719
+ # handlers were false-positive "dead zones".)
5720
+ _zero_degree = sorted(
5630
5721
  [n for n in graph_nodes
5631
5722
  if n.get("in_degree", 0) == 0 and n.get("out_degree", 0) == 0
5632
5723
  and n.get("type") in ("class", "interface")],
5633
5724
  key=lambda n: n.get("fqn", ""),
5634
- )[:20]
5725
+ )
5726
+ dead_zones, framework_dispatched = _partition_static_unreferenced(_zero_degree, root)
5727
+ dead_zones = dead_zones[:20]
5728
+ framework_dispatched = framework_dispatched[:20]
5635
5729
 
5636
5730
  # Hotspot candidates: high in-degree service/repository/controller nodes,
5637
5731
  # ranked by composite score (in_degree × 2 + git_churn) for volatility signal.
@@ -5680,16 +5774,35 @@ def modernize_cmd(
5680
5774
  "total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
5681
5775
  "total_subsystems": len(subsystems),
5682
5776
  "high_coupling_nodes": len(coupling_nodes),
5683
- "dead_zone_candidates": len(dead_zones),
5777
+ "statically_unreferenced": len(dead_zones),
5778
+ "framework_dispatched": len(framework_dispatched),
5684
5779
  }
5780
+ # BUG #6 (v1.68.0): `member_count` counts ALL graph members in the subsystem —
5781
+ # classes, methods AND fields — so it runs ~5x higher than the class count and
5782
+ # is not comparable to the repo-level `total_classes` (which counts only
5783
+ # class/interface nodes). Surface an unambiguous `class_count` alongside it,
5784
+ # computed against the same class/interface node set as total_classes, so a
5785
+ # reader does not have to reverse-engineer the unit. `member_count` is retained
5786
+ # for backward compatibility but is now documented as the graph-member tally.
5787
+ _class_fqns = {n["fqn"] for n in graph_nodes if n.get("type") in ("class", "interface")}
5788
+
5789
+ def _class_count(members: list) -> int:
5790
+ return sum(1 for m in members if m in _class_fqns)
5791
+
5685
5792
  _subsystem_summary = [
5686
5793
  {
5687
5794
  "label": s.get("label") or s.get("name") or "",
5688
5795
  "package_prefix": s.get("package_prefix") or s.get("pkg") or "",
5796
+ "class_count": _class_count(s.get("members") or []),
5689
5797
  "member_count": len(s.get("members") or []),
5690
5798
  }
5691
5799
  for s in subsystems[:15]
5692
5800
  ]
5801
+ _subsystem_summary_note = (
5802
+ "class_count = class/interface declarations only (comparable to "
5803
+ "summary.total_classes). member_count = all graph members in the subsystem "
5804
+ "including methods and fields, so it is larger and not a class tally."
5805
+ )
5693
5806
 
5694
5807
  if (not _mod_is_pro) and _mod_large(str(root)):
5695
5808
  # Large monolith, free tier: structural discovery preview only — no dead
@@ -5706,6 +5819,7 @@ def modernize_cmd(
5706
5819
  ),
5707
5820
  "summary": _summary,
5708
5821
  "subsystem_summary": _subsystem_summary,
5822
+ "subsystem_summary_note": _subsystem_summary_note,
5709
5823
  "hotspot_candidates": hotspots[:3],
5710
5824
  "high_coupling_nodes": [
5711
5825
  {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
@@ -5723,14 +5837,26 @@ def modernize_cmd(
5723
5837
  {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
5724
5838
  for n in coupling_nodes
5725
5839
  ],
5726
- "dead_zone_candidates": [
5840
+ "statically_unreferenced": [
5727
5841
  {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
5728
5842
  for n in dead_zones
5729
5843
  ],
5844
+ "statically_unreferenced_note": (
5845
+ "Zero static callers in the Java call-graph. NOT confirmed dead: verify "
5846
+ "no framework dispatch (reflection, XML/SPI config, annotations) before "
5847
+ "removing. Classes detected as framework-dispatched are listed separately "
5848
+ "under framework_dispatched and excluded from this list."
5849
+ ),
5850
+ "framework_dispatched": [
5851
+ {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
5852
+ for n in framework_dispatched
5853
+ ],
5730
5854
  "subsystem_summary": _subsystem_summary,
5855
+ "subsystem_summary_note": _subsystem_summary_note,
5731
5856
  "cross_module_tangles": [
5732
5857
  {
5733
5858
  "label": s.get("label") or s.get("name") or "",
5859
+ "class_count": _class_count(s.get("members") or []),
5734
5860
  "member_count": len(s.get("members") or []),
5735
5861
  }
5736
5862
  for s in tangle_modules
@@ -5742,7 +5868,8 @@ def modernize_cmd(
5742
5868
  if hotspots else
5743
5869
  "high_coupling_nodes shows the most-referenced classes — start there. "
5744
5870
  )
5745
- + "Dead zones are safe to remove or refactor. "
5871
+ + "statically_unreferenced lists classes with no Java callers review "
5872
+ + "for framework dispatch (XML/reflection/SPI) before removing. "
5746
5873
  + "Cross-module tangles indicate coupling worth decomposing."
5747
5874
  ),
5748
5875
  }
@@ -28,7 +28,11 @@ _STACK_LABELS: dict[str, str] = {
28
28
 
29
29
  _TYPE_LABELS: dict[str, str] = {
30
30
  "api": "REST API",
31
- "web_mvc": "Spring MVC web app",
31
+ # Vendor-neutral: web_mvc is triggered by server-side template engines
32
+ # (Thymeleaf/FreeMarker), which are NOT exclusive to Spring (e.g. Apache OFBiz
33
+ # uses FreeMarker with its own framework). The detected frameworks list carries
34
+ # the actual stack; the type label must not assert "Spring".
35
+ "web_mvc": "Server-side MVC web app",
32
36
  "webapp": "Web app",
33
37
  "fullstack": "Full-stack app",
34
38
  "cli": "CLI tool",
@@ -1278,9 +1278,42 @@ class DependencyAnalyzer:
1278
1278
  return records, limitations
1279
1279
 
1280
1280
  def _strip_gradle_comments(self, content: str) -> str:
1281
- content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
1282
- content = re.sub(r"//[^\n]*", "", content)
1283
- return content
1281
+ """Strip // and /* */ comments WITHOUT touching string literals.
1282
+
1283
+ A naive regex treats `/*` inside a string as a block-comment open. Gradle
1284
+ build files routinely embed `/*` in Ant-style glob strings (e.g.
1285
+ '**/*.java') and `//` in URL strings — a regex stripper then eats the entire
1286
+ dependencies block up to the next literal `*/` (real defect on Apache OFBiz,
1287
+ where ~100 deps vanished). This scanner skips quoted strings (single, double,
1288
+ and triple-quoted, with backslash escapes) so only genuine comments are removed.
1289
+ """
1290
+ out: list[str] = []
1291
+ i, n = 0, len(content)
1292
+ quote: Optional[str] = None # active string delimiter
1293
+ while i < n:
1294
+ if quote is not None:
1295
+ # Inside a string literal — copy verbatim until the matching delimiter.
1296
+ if len(quote) == 1 and content[i] == "\\" and i + 1 < n:
1297
+ out.append(content[i:i + 2]); i += 2; continue
1298
+ if content.startswith(quote, i):
1299
+ out.append(quote); i += len(quote); quote = None; continue
1300
+ out.append(content[i]); i += 1; continue
1301
+ if content.startswith("//", i):
1302
+ j = content.find("\n", i)
1303
+ if j == -1:
1304
+ break
1305
+ i = j; continue
1306
+ if content.startswith("/*", i):
1307
+ j = content.find("*/", i + 2)
1308
+ if j == -1:
1309
+ break
1310
+ i = j + 2; continue
1311
+ for q in ('"""', "'''", '"', "'"):
1312
+ if content.startswith(q, i):
1313
+ quote = q; out.append(q); i += len(q); break
1314
+ else:
1315
+ out.append(content[i]); i += 1
1316
+ return "".join(out)
1284
1317
 
1285
1318
  def _analyze_gradle(self, root: Path) -> tuple[list[DependencyRecord], list[str]]:
1286
1319
  for filename in ("build.gradle", "build.gradle.kts"):
@@ -1307,7 +1340,20 @@ class DependencyAnalyzer:
1307
1340
  except OSError:
1308
1341
  pass
1309
1342
 
1310
- return self._dedupe(records), ["gradle: no compatible lockfile found; transitive dependencies unavailable"]
1343
+ deduped = self._dedupe(records)
1344
+ gradle_limitations = [
1345
+ "gradle: no compatible lockfile found; transitive dependencies unavailable"
1346
+ ]
1347
+ # Honest-zero guard: if the build file declares a dependencies block but
1348
+ # nothing was resolved, report a GAP rather than a confident "0 deps".
1349
+ if not deduped and re.search(r"\bdependencies\s*\{", content):
1350
+ jar_count = sum(1 for _ in root.glob("lib/**/*.jar"))
1351
+ gap = ("gradle: dependencies declared but none resolved by static "
1352
+ "parsing (non-standard declaration syntax)")
1353
+ if jar_count:
1354
+ gap += f"; {jar_count} jar(s) present under lib/ (not parsed)"
1355
+ gradle_limitations.append(gap)
1356
+ return deduped, gradle_limitations
1311
1357
  return [], []
1312
1358
 
1313
1359
  def _parse_gradle_properties(self, root: Path, content: str) -> dict[str, str]:
@@ -7,8 +7,14 @@ deterministic source-text matching (same approach as the JNDI datasource scan in
7
7
 
8
8
  Covered clients:
9
9
 
10
- * HTTP — ``RestTemplate``, ``WebClient``, ``@FeignClient`` (declarative)
11
- * LDAP — ``LdapTemplate``
10
+ * HTTP — ``RestTemplate``, ``WebClient``, ``@FeignClient`` (declarative),
11
+ JDK/Apache/OkHttp clients
12
+ * LDAP — ``LdapTemplate``, JNDI ``InitialLdapContext``/``LdapContext``
13
+ * DNS — JNDI ``DirContext`` configured with ``DnsContextFactory`` (BUG #2:
14
+ ``DirContext`` is protocol-agnostic and is classified by its
15
+ ``INITIAL_CONTEXT_FACTORY``, not assumed to be LDAP)
16
+ * SMTP — JavaMail / Jakarta Mail (BUG #1: gated on a mail import so the bare
17
+ word "Transport" in a log string is not a false positive)
12
18
  * JMS — ``JmsTemplate``, ActiveMQ connection factories
13
19
 
14
20
  Each hit is reported with a ``file:line`` evidence anchor and, when a literal URL
@@ -28,6 +34,24 @@ _URL_RE = re.compile(r'"((?:https?|ldaps?|tcp|amqp|jms|nio)://[^"]*)"')
28
34
  # First string literal on a line (fallback target, e.g. WebClient.create("x")).
29
35
  _STR_RE = re.compile(r'"([^"]+)"')
30
36
 
37
+ # BUG #1 (v1.68.0): a JavaMail/Jakarta-Mail import is required before an SMTP token
38
+ # (Transport, MimeMessage) is trusted. The bare word "Transport" is also a common
39
+ # English noun that appears in log strings ( logger.warn("Transport initialization
40
+ # failure") in non-mail code), so a mail-import gate plus a string-literal skip
41
+ # stops those false positives.
42
+ _MAIL_IMPORT_RE = re.compile(
43
+ r"^\s*import\s+(?:(?:javax|jakarta)\.mail|org\.springframework\.mail)\b",
44
+ re.MULTILINE,
45
+ )
46
+
47
+ # BUG #2 (v1.68.0): javax.naming.directory.{DirContext,InitialDirContext} is NOT
48
+ # LDAP-specific — the actual protocol is decided by the value bound to
49
+ # Context.INITIAL_CONTEXT_FACTORY. com.sun.jndi.dns.DnsContextFactory means DNS
50
+ # (SRV/A record lookups), com.sun.jndi.ldap.LdapCtxFactory means LDAP. Classify by
51
+ # the factory class present in the file instead of defaulting to LDAP.
52
+ _DNS_FACTORY_RE = re.compile(r"jndi\.dns|DnsContextFactory", re.IGNORECASE)
53
+ _LDAP_FACTORY_RE = re.compile(r"jndi\.ldap|LdapCtxFactory", re.IGNORECASE)
54
+
31
55
  # Declarative HTTP client. Attrs may span multiple lines, so matched on full text.
32
56
  _FEIGN_RE = re.compile(r"@FeignClient\s*\(([^)]*)\)", re.DOTALL)
33
57
  _ATTR_URL_RE = re.compile(r'url\s*=\s*"([^"]*)"')
@@ -82,6 +106,39 @@ def _extract_target(line: str) -> Optional[str]:
82
106
  return None
83
107
 
84
108
 
109
+ def _in_string_literal(line: str, idx: int) -> bool:
110
+ """True if char offset ``idx`` falls inside a double-quoted string on ``line``.
111
+
112
+ BUG #1 (v1.68.0): a Java type token never legitimately appears inside a string
113
+ literal, so a match there (e.g. the word "Transport" in a log message) is noise,
114
+ not a real client construct. Counts unescaped quotes before ``idx``.
115
+ """
116
+ quote_count = 0
117
+ i = 0
118
+ while i < idx and i < len(line):
119
+ ch = line[i]
120
+ if ch == "\\":
121
+ i += 2
122
+ continue
123
+ if ch == '"':
124
+ quote_count += 1
125
+ i += 1
126
+ return quote_count % 2 == 1
127
+
128
+
129
+ def _classify_naming_factory(text: str) -> Optional[str]:
130
+ """Classify a javax.naming.directory usage by its INITIAL_CONTEXT_FACTORY.
131
+
132
+ Returns ``"dns"``, ``"ldap"``, or ``None`` (factory not statically resolvable).
133
+ See BUG #2 — DirContext alone does not imply LDAP.
134
+ """
135
+ if _DNS_FACTORY_RE.search(text):
136
+ return "dns"
137
+ if _LDAP_FACTORY_RE.search(text):
138
+ return "ldap"
139
+ return None
140
+
141
+
85
142
  def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
86
143
  """Detect outbound integrations across ``file_paths`` (relative to ``root``).
87
144
 
@@ -93,18 +150,31 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
93
150
  seen: "set[tuple[str, str, Optional[str], str]]" = set()
94
151
  records: "list[dict]" = []
95
152
 
96
- def _add(kind: str, client: str, target: Optional[str], rel: str, line: int) -> None:
153
+ def _add(
154
+ kind: str,
155
+ client: str,
156
+ target: Optional[str],
157
+ rel: str,
158
+ line: int,
159
+ confidence: Optional[str] = None,
160
+ ) -> None:
97
161
  evidence = f"{rel}:{line}"
98
162
  key = (kind, client, target, evidence)
99
163
  if key in seen:
100
164
  return
101
165
  seen.add(key)
102
- records.append({
166
+ rec = {
103
167
  "kind": kind,
104
168
  "client": client,
105
169
  "target": target,
106
170
  "evidence": evidence,
107
- })
171
+ }
172
+ # Per-record confidence is emitted only when the classification is uncertain
173
+ # (e.g. a JNDI DirContext whose factory could not be resolved). Confident hits
174
+ # stay schema-clean without the field.
175
+ if confidence is not None:
176
+ rec["confidence"] = confidence
177
+ records.append(rec)
108
178
 
109
179
  for rel in file_paths:
110
180
  try:
@@ -126,6 +196,10 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
126
196
  )
127
197
  _add("http", "feign", target, rel, _line_of(text, m.start()))
128
198
 
199
+ # Per-file context used to disambiguate / gate uncertain tokens (BUG #1/#2).
200
+ has_mail_import = bool(_MAIL_IMPORT_RE.search(text))
201
+ naming_factory = _classify_naming_factory(text)
202
+
129
203
  # Token clients — per line, skipping imports/package/comment noise.
130
204
  # First pass records the declaration site and any variable name bound to
131
205
  # the client, so a later call site (where the URL literal usually lives)
@@ -146,7 +220,28 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
146
220
  m = token_re.search(line)
147
221
  if not m:
148
222
  continue
149
- _add(kind, client, _extract_target(line), rel, lineno)
223
+ # BUG #1: a token matched inside a string literal (log message, doc
224
+ # comment text) is never a real client construct — skip it.
225
+ if _in_string_literal(line, m.start()):
226
+ continue
227
+ confidence: Optional[str] = None
228
+ # BUG #1: SMTP tokens require a JavaMail/Jakarta-Mail import to be
229
+ # trusted — "Transport" / "MimeMessage" are too generic otherwise.
230
+ if kind == "smtp" and not has_mail_import:
231
+ continue
232
+ # BUG #2: javax.naming.directory.{Dir,Initial}Context is protocol-
233
+ # agnostic. Reclassify by the configured context factory; default to
234
+ # an explicit low-confidence "unknown" rather than assuming LDAP.
235
+ if client == "jndi-ldap" and "Dir" in token_re.pattern:
236
+ if naming_factory == "dns":
237
+ kind, client = "dns", "jndi-dns"
238
+ elif naming_factory == "ldap":
239
+ kind, client = "ldap", "jndi-ldap"
240
+ else:
241
+ kind, client, confidence = (
242
+ "naming-directory-unknown", "jndi-dircontext", "low",
243
+ )
244
+ _add(kind, client, _extract_target(line), rel, lineno, confidence)
150
245
  tok = m.group(0)
151
246
  # `Type name` (field/local decl) and `name = new Type(` forms.
152
247
  decl = re.search(re.escape(tok) + r"\s+(\w+)\b", line)
@@ -186,9 +281,13 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
186
281
  "confidence": confidence,
187
282
  "coverage_note": (
188
283
  "Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
189
- "+ JNDI), SMTP (JavaMail), and JMS client constructs by source-text "
190
- "matching. A count of 0 means no such construct was found, not that the "
191
- "system has no outbound integrations runtime/DI-wired clients are not "
192
- "statically visible."
284
+ "+ JNDI), DNS (JNDI DirContext w/ DnsContextFactory), SMTP (JavaMail, "
285
+ "import-gated), and JMS client constructs by source-text matching. JNDI "
286
+ "DirContext usage is classified by its INITIAL_CONTEXT_FACTORY (dns vs "
287
+ "ldap); when the factory is not statically resolvable the kind is "
288
+ "'naming-directory-unknown' with confidence='low', never assumed LDAP. A "
289
+ "count of 0 means no such construct was found, not that the system has no "
290
+ "outbound integrations — runtime/DI-wired clients are not statically "
291
+ "visible."
193
292
  ),
194
293
  }
@@ -774,7 +774,7 @@ def run_migrate_flow_impl(repo_path: str, min_severity: str = "low") -> dict[str
774
774
  if not _is_java_repo(repo_path):
775
775
  quality_warnings.append(
776
776
  "not_a_java_repo: migration analysis targets Spring Boot / Java repos. "
777
- "Result will be empty (readiness_score=100, no findings)."
777
+ "Result will be empty (readiness_score=null/N/A, no findings)."
778
778
  )
779
779
 
780
780
  report = _exec(["migrate-check", repo_path, "--min-severity", min_severity])
@@ -798,7 +798,7 @@ depth: BFS depth for indirect caller traversal (1–8, default: 4).
798
798
  Analyzes codebase for modernization opportunities: dead zones, hotspot scores, upgrade candidates.
799
799
 
800
800
  Maps to: sourcecode modernize <repo_path>
801
- Returns: hotspot_candidates (high fan-in + git churn), dead_zone_candidates (isolated classes),
801
+ Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (zero-caller classes — NOT confirmed dead; verify no framework dispatch) + framework_dispatched,
802
802
  high_coupling_nodes, subsystem_summary, cross_module_tangles, recommendation.
803
803
 
804
804
  Best for: refactor planning, identifying where to start, finding safe removal candidates.
@@ -1403,7 +1403,7 @@ Spring Boot 2→3 migration readiness: javax→jakarta namespace blockers. JAVA
1403
1403
  When to call: when asked about Spring Boot migration readiness, javax vs jakarta imports,
1404
1404
  or upgrading from Spring Boot 2.x to 3.x. Use BEFORE get_spring_audit when the goal
1405
1405
  is migration planning rather than ongoing Spring semantic audit.
1406
- Do NOT call on non-Java repositories — returns readiness_score=100 with no findings.
1406
+ Do NOT call on non-Java repositories — returns readiness_score=null (N/A) with no findings.
1407
1407
 
1408
1408
  Rules detected:
1409
1409
  MIG-001 critical — javax.persistence imports (JPA; will not compile after migration)
@@ -1415,7 +1415,7 @@ Rules detected:
1415
1415
  MIG-007 medium — javax.inject imports (DI annotations)
1416
1416
  MIG-008 medium — javax.ws.rs imports (JAX-RS API)
1417
1417
 
1418
- Returns: schema_version, readiness_score (0–100; 100=ready to migrate),
1418
+ Returns: schema_version, readiness_score (0–100, or null=N/A when no migration target applies; 100=ready to migrate),
1419
1419
  jakarta_readiness / boot3_readiness / jdk_modernization (per-dimension 0–100),
1420
1420
  blocking_count, estimated_effort_days, spring_boot_2_detected (true|false|null —
1421
1421
  null=undetermined, never assumed true), spring_boot_version_detected,
sourcecode/mcp/server.py CHANGED
@@ -517,7 +517,7 @@ def run_migrate_flow(repo_path: str = ".", min_severity: str = "low") -> dict:
517
517
  Primary high-value entry point for migration planning. Wraps migrate-check and
518
518
  lifts the headline numbers to the top level so the agent can plan a 2→3 upgrade
519
519
  without parsing the full report:
520
- - readiness_score (0–100; 100 = ready), blocking_count, estimated_effort_days
520
+ - readiness_score (0–100, or null=N/A when no migration target applies; 100 = ready), blocking_count, estimated_effort_days
521
521
  - by_severity and by_target breakdown (jakarta / spring_security_6 / java_11)
522
522
 
523
523
  Use this instead of calling get_migration_readiness and interpreting it by hand.
@@ -788,10 +788,10 @@ def get_migration_readiness(repo_path: str = ".", min_severity: str = "low") ->
788
788
  When to call: when asked about Spring Boot migration readiness, javax vs jakarta imports,
789
789
  or upgrading from Spring Boot 2.x to 3.x. Call this BEFORE get_spring_audit when
790
790
  the goal is migration planning — not ongoing audit.
791
- Do NOT call on non-Java repositories — returns readiness_score=100 with no findings.
791
+ Do NOT call on non-Java repositories — returns readiness_score=null (N/A) with no findings.
792
792
 
793
793
  Maps to: sourcecode migrate-check <repo_path> --min-severity <min_severity>
794
- Returns: MigrationReport with schema_version, readiness_score (0–100; 100=ready to migrate),
794
+ Returns: MigrationReport with schema_version, readiness_score (0–100, or null=N/A when no migration target applies; 100=ready to migrate),
795
795
  jakarta_readiness / boot3_readiness / jdk_modernization / hibernate_readiness
796
796
  (per-dimension 0–100), headline_blocker (e.g. "hibernate_rewrite" or null),
797
797
  hibernate (Hibernate 5→6 stratified model: 4-layer risk_matrix, rewrite_targets[]
@@ -1335,7 +1335,7 @@ def modernize_context(repo_path: str = ".", format: str = "json") -> dict:
1335
1335
  """Analyzes codebase for modernization opportunities: dead zones, hotspot scores, upgrade candidates.
1336
1336
 
1337
1337
  Maps to: sourcecode modernize <repo_path>
1338
- Returns: hotspot_candidates (high fan-in + git churn), dead_zone_candidates (isolated classes),
1338
+ Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (zero-caller classes — NOT confirmed dead; verify no framework dispatch) + framework_dispatched,
1339
1339
  high_coupling_nodes, subsystem_summary, cross_module_tangles, recommendation.
1340
1340
 
1341
1341
  Best for: refactor planning, identifying where to start, finding safe removal candidates.
@@ -608,29 +608,79 @@ def _classify_code_context(finding: "MigrationFinding") -> str:
608
608
  return "main"
609
609
 
610
610
 
611
- # BUG #2: Spring presence signal. The Boot3 dimension only applies to repos that
612
- # actually use Spring — a Quarkus/Micronaut/Helidon/Jakarta-pure repo has no
613
- # Spring Boot 2→3 axis. Detected from build coordinates or framework imports.
614
- _SPRING_BUILD_RE: re.Pattern = re.compile(
615
- r"org\.springframework(?:\.boot)?\b|spring-boot-starter", re.IGNORECASE
611
+ # BUG #1/#2: Spring presence must be SCOPE- and ARTIFACT-aware. The Boot 2→3 axis
612
+ # only applies to repos that use Spring AT RUNTIME — a Quarkus/Micronaut/Jakarta-pure
613
+ # repo (or one like Apache OFBiz whose ONLY Spring coordinate is spring-test, a TEST
614
+ # support library declared under a legacy `compile` block) has no Spring Boot axis.
615
+ # We therefore split Spring usage into runtime vs test-only and NEVER let a test
616
+ # artifact poison spring_present (which gates the boot3 readiness dimension).
617
+
618
+ # Test-only Spring artifacts: their presence — even when declared in a compile/
619
+ # implementation block — never implies Spring at runtime. The ARTIFACT itself is a
620
+ # test library, regardless of the declared scope.
621
+ _SPRING_TEST_ARTIFACT_RE: re.Pattern = re.compile(
622
+ r"\bspring-(?:test|boot-test(?:-autoconfigure)?|boot-starter-test|security-test)\b",
623
+ re.IGNORECASE,
616
624
  )
625
+ # Any `spring-<artifact>` coordinate token (matches both Gradle `org.springframework:
626
+ # spring-core:…` strings and Maven `<artifactId>spring-core</artifactId>` text).
627
+ _SPRING_ARTIFACT_TOKEN_RE: re.Pattern = re.compile(
628
+ r"\bspring-[a-z][a-z0-9]*(?:-[a-z0-9]+)*\b", re.IGNORECASE
629
+ )
630
+ # Spring Boot Gradle plugin / BOM group — an unambiguous runtime signal.
631
+ _SPRING_BOOT_PLUGIN_RE: re.Pattern = re.compile(
632
+ r"""(?:id\s*['"]\s*org\.springframework\.boot|"""
633
+ r"""org\.springframework\.boot\s*[:'"])""",
634
+ re.IGNORECASE,
635
+ )
636
+ # Import-side split: a Spring import in a TEST package (org.springframework.*.test
637
+ # or org.springframework.test / .boot.test) is a test-only signal; any other
638
+ # org.springframework import in MAIN sources is a runtime signal.
617
639
  _SPRING_IMPORT_RE: re.Pattern = re.compile(
618
- r"^[ \t]*import\s+org\.springframework\.", re.MULTILINE
640
+ r"^[ \t]*import\s+(?:static\s+)?org\.springframework\.([\w.]+)", re.MULTILINE
619
641
  )
620
642
 
621
643
 
622
- def _detect_spring_present(root: Path, spring_import_seen: bool) -> bool:
623
- """True when Spring is actually used (build coordinate or org.springframework import)."""
624
- if spring_import_seen:
625
- return True
644
+ def _build_text_spring_signals(text: str) -> tuple[bool, bool]:
645
+ """(runtime, any_spring) for one build-file's text artifact/scope aware.
646
+
647
+ A `spring-*` artifact token that is not a test library, or the Spring Boot
648
+ plugin/BOM group, counts as runtime. spring-test (and friends) alone count as
649
+ "spring present but test-only" — never runtime.
650
+ """
651
+ runtime = False
652
+ any_spring = False
653
+ if _SPRING_BOOT_PLUGIN_RE.search(text):
654
+ runtime = True
655
+ any_spring = True
656
+ for m in _SPRING_ARTIFACT_TOKEN_RE.finditer(text):
657
+ any_spring = True
658
+ if not _SPRING_TEST_ARTIFACT_RE.match(m.group(0)):
659
+ runtime = True
660
+ return runtime, any_spring
661
+
662
+
663
+ def _detect_spring_usage(
664
+ root: Path, runtime_import_seen: bool, test_import_seen: bool
665
+ ) -> tuple[bool, bool]:
666
+ """Return (runtime_present, test_only).
667
+
668
+ runtime_present — Spring is on the runtime/compile path (build coordinate or a
669
+ MAIN-source org.springframework import that is not a test pkg).
670
+ test_only — Spring appears ONLY as a test dependency (e.g. spring-test);
671
+ the Boot 2→3 migration axis is N/A.
672
+ """
673
+ runtime = runtime_import_seen
674
+ any_spring = runtime_import_seen or test_import_seen
626
675
  for abs_path, _rel in _find_build_files(root):
627
676
  try:
628
677
  text = abs_path.read_text(encoding="utf-8", errors="replace")
629
678
  except OSError:
630
679
  continue
631
- if _SPRING_BUILD_RE.search(text):
632
- return True
633
- return False
680
+ r, s = _build_text_spring_signals(text)
681
+ runtime = runtime or r
682
+ any_spring = any_spring or s
683
+ return runtime, (any_spring and not runtime)
634
684
 
635
685
  # G-1: cap on total readiness deduction from low-severity (advisory, non-blocking)
636
686
  # findings, so optional modernization cleanups cannot collapse the migration-readiness
@@ -1165,7 +1215,9 @@ class MigrationReport:
1165
1215
  repo_id: str = ""
1166
1216
  git_head: str = ""
1167
1217
 
1168
- readiness_score: int = 100
1218
+ # Optional[int]: None == N/A (no applicable migration dimension), never a
1219
+ # manufactured 100 on a repo with nothing to migrate.
1220
+ readiness_score: Optional[int] = 100
1169
1221
  # Per-dimension readiness (0-100). javax→jakarta namespace, full Boot 2→3
1170
1222
  # migration, and orthogonal JDK modernization are scored independently so
1171
1223
  # JDK debt (java.util.Date, reflection) does not sink a jakarta-ready repo.
@@ -1192,8 +1244,16 @@ class MigrationReport:
1192
1244
  # None = could not determine. Absence of evidence is never reported as True.
1193
1245
  spring_boot_2_detected: Optional[bool] = None
1194
1246
  spring_boot_version_detected: Optional[str] = None
1195
- # BUG #2: whether Spring is used at all. The Boot3 dimension is N/A without it.
1247
+ # BUG #2: whether Spring is used AT RUNTIME. The Boot3 dimension is N/A without it.
1196
1248
  spring_present: bool = True
1249
+ # BUG #1/#2: Spring appears ONLY as a test dependency (e.g. spring-test). Reported
1250
+ # so a consumer can see WHY boot3 is N/A despite an org.springframework coordinate.
1251
+ spring_test_only: bool = False
1252
+ # BUG #3/#4: the repo imports the jakarta.* namespace (already on Jakarta EE 9+).
1253
+ # Evidence that the namespace axis is RELEVANT (and complete) — distinguishes an
1254
+ # already-migrated Jakarta repo (jakarta applicable, 100) from a repo that was
1255
+ # never Java EE at all (jakarta N/A).
1256
+ jakarta_namespace_adopted: bool = False
1197
1257
  # BUG #6 / #8: findings that do NOT count toward blocking_count or readiness —
1198
1258
  # best-practice hygiene (java.util.Date…) and test/fixture/generated buckets,
1199
1259
  # surfaced separately so the headline reflects real product migration risk.
@@ -1261,6 +1321,16 @@ class MigrationReport:
1261
1321
  if not self.spring_present:
1262
1322
  self.boot3_readiness = 100
1263
1323
 
1324
+ # BUG #3/#4: the jakarta dimension is applicable ONLY with POSITIVE evidence the
1325
+ # namespace axis is relevant — either migratable javax.* code (a javax→jakarta
1326
+ # finding, axis in-progress) OR the jakarta.* namespace already adopted (axis
1327
+ # complete → 100). A repo with neither was never Java EE: jakarta is N/A, never a
1328
+ # manufactured 100 folded into the headline.
1329
+ _jakarta_applies = (
1330
+ any(f.migration_target in _JAKARTA_TARGETS for f in main_findings)
1331
+ or self.jakarta_namespace_adopted
1332
+ )
1333
+
1264
1334
  # ── BUG #2: Hibernate applicability — version-driven, never heuristic ────
1265
1335
  # The 5→6 axis is applicable ONLY when the repo is actually on Hibernate < 6.
1266
1336
  # resolved < 6 → applicable (rewrite score is a measurement)
@@ -1316,14 +1386,22 @@ class MigrationReport:
1316
1386
  # cannot sink a framework-complete repo. N/A dimensions carry score=None and
1317
1387
  # never enter the aggregate.
1318
1388
  self.applicable_dimensions = {
1319
- "jakarta": {"applicable": True, "score": self.jakarta_readiness,
1320
- "reason": "javax→jakarta namespace migration"},
1389
+ "jakarta": {
1390
+ "applicable": _jakarta_applies,
1391
+ "score": self.jakarta_readiness if _jakarta_applies else None,
1392
+ "reason": ("javax→jakarta namespace migration"
1393
+ if _jakarta_applies
1394
+ else "N/A — no migratable javax.* imports detected"),
1395
+ },
1321
1396
  "boot3": {
1322
1397
  "applicable": self.spring_present,
1323
1398
  "score": self.boot3_readiness if self.spring_present else None,
1324
1399
  "reason": ("Spring Boot 2→3 / Security 6 migration"
1325
1400
  if self.spring_present
1326
- else "N/A — no Spring usage detected (non-Spring stack)"),
1401
+ else ("N/A — Spring present only as a TEST dependency "
1402
+ "(spring-test); no runtime Spring"
1403
+ if self.spring_test_only
1404
+ else "N/A — no Spring usage detected (non-Spring stack)")),
1327
1405
  },
1328
1406
  "jdk_modernization": {"applicable": True, "score": self.jdk_modernization,
1329
1407
  "reason": "orthogonal JDK modernization debt (excluded from "
@@ -1347,14 +1425,22 @@ class MigrationReport:
1347
1425
  for name in _MIGRATION_DIMENSIONS
1348
1426
  if self.applicable_dimensions[name]["applicable"]
1349
1427
  }
1350
- self.readiness_score = min(agg_inputs.values()) if agg_inputs else 100
1428
+ # BUG #4: when NO migration dimension applies (non-Spring repo, no migratable
1429
+ # javax.*, no Hibernate 5), readiness is N/A (None) — NOT a manufactured 100.
1430
+ # Absence of a migration target is "not applicable", never "100% ready".
1431
+ self.readiness_score = min(agg_inputs.values()) if agg_inputs else None
1351
1432
  self.readiness_aggregate = {
1352
1433
  "method": "min",
1353
1434
  "inputs": agg_inputs,
1354
1435
  "excluded": ["jdk_modernization"],
1436
+ "applicable": bool(agg_inputs),
1355
1437
  "note": ("readiness_score = min over applicable migration dimensions "
1356
1438
  "(jakarta / boot3 / hibernate). jdk_modernization is an orthogonal "
1357
- "upkeep axis and is intentionally excluded."),
1439
+ "upkeep axis and is intentionally excluded."
1440
+ if agg_inputs else
1441
+ "N/A — no migration target detected (no migratable javax.*, no "
1442
+ "runtime Spring, no Hibernate 5). JDK-modernization findings, if "
1443
+ "any, are reported on their own axis."),
1358
1444
  }
1359
1445
  # Internal consistency guard — the headline cannot diverge from the dimensions
1360
1446
  # it claims to summarize (catches a future scorer change that breaks the model).
@@ -1431,6 +1517,7 @@ class MigrationReport:
1431
1517
  "hygiene_findings": self.hygiene_findings,
1432
1518
  "non_blocking": self.non_blocking,
1433
1519
  "spring_present": self.spring_present,
1520
+ "spring_test_only": self.spring_test_only,
1434
1521
  "spring_boot_2_detected": self.spring_boot_2_detected,
1435
1522
  "spring_boot_version_detected": self.spring_boot_version_detected,
1436
1523
  "summary": self.summary,
@@ -1464,8 +1551,10 @@ class MigrationReport:
1464
1551
  main_high = sum(1 for f in self.findings
1465
1552
  if f.code_context == "main" and f.severity == "high")
1466
1553
  nb = self.non_blocking.get("count", 0)
1554
+ _headline = (f"{self.readiness_score}/100" if self.readiness_score is not None
1555
+ else "N/A (no migration target detected)")
1467
1556
  lines: list[str] = [
1468
- f"Migration Readiness: {self.readiness_score}/100",
1557
+ f"Migration Readiness: {_headline}",
1469
1558
  f" {_dim('jakarta')} {_dim('boot3')} "
1470
1559
  f"{_dim('jdk_modernization')} {_dim('hibernate')}",
1471
1560
  *([f" ⚠ Headline blocker: {self.headline_blocker} "
@@ -1542,6 +1631,53 @@ def _refine_mig006(matched_imports: list[str]) -> tuple[str, str]:
1542
1631
  )
1543
1632
 
1544
1633
 
1634
+ # ---------------------------------------------------------------------------
1635
+ # BUG #3 (v1.68.0): framework-gated narrative.
1636
+ # MIG-* explanations are written assuming a Spring Boot 2→3 upgrade because that is
1637
+ # the overwhelmingly common Jakarta-migration context. On a repo where the tool has
1638
+ # already determined spring_present=False (plain Jakarta EE, JAX-RS/Jersey, Quarkus,
1639
+ # Guice, ...), the underlying javax→jakarta finding is still valid, but prose that
1640
+ # says "Spring Boot 3 requires ..." is factually wrong for that repo and misleads any
1641
+ # reader who trusts the narrative over the structured spring_present field.
1642
+ #
1643
+ # Treat the narrative as a pure function of the structured data: when spring_present
1644
+ # is False, rewrite Spring-specific framing into framework-neutral Jakarta-EE /
1645
+ # servlet-container framing. Ordered phrase map first (keeps nice sentences for the
1646
+ # common phrasings), then a catch-all so no "Spring Boot" string can survive.
1647
+ # ---------------------------------------------------------------------------
1648
+ _NON_SPRING_EXPLANATION_SUBS: tuple[tuple[str, str], ...] = (
1649
+ ("Spring Boot 3 bundles Jakarta Servlet 6.0.",
1650
+ "Modern servlet containers (Jetty 12+, Tomcat 11+) require Jakarta Servlet 6.0."),
1651
+ ("Spring Boot 3 requires Jakarta Servlet 5.0+",
1652
+ "Jakarta EE 9+ servlet containers require Jakarta Servlet 5.0+"),
1653
+ ("Spring Boot 3 uses Jakarta EE 9 which moved",
1654
+ "Jakarta EE 9 moved"),
1655
+ ("Spring Boot 3 uses Hibernate Validator 8.x which implements jakarta.validation.",
1656
+ "Jakarta EE 9+ uses Hibernate Validator 8.x which implements jakarta.validation."),
1657
+ ("Spring Boot 3 depends on Jakarta Transactions (jakarta.transaction).",
1658
+ "Jakarta EE 9+ uses Jakarta Transactions (jakarta.transaction)."),
1659
+ ("Spring Boot 3 requires Hibernate 6",
1660
+ "Jakarta Persistence (JPA 3.x) requires Hibernate 6"),
1661
+ ("Spring Boot 3 cache abstraction. Spring Boot 3 requires EhCache 3.x",
1662
+ "Jakarta-EE cache abstraction. The modern JCache provider requires EhCache 3.x"),
1663
+ # Catch-all: any remaining Spring-Boot framing → generic Jakarta EE 9+.
1664
+ ("Spring Boot 3", "Jakarta EE 9+"),
1665
+ ("Spring Boot 2", "the pre-Jakarta (javax) baseline"),
1666
+ )
1667
+
1668
+
1669
+ def _neutralize_non_spring_explanation(text: str) -> str:
1670
+ """Rewrite Spring-specific migration prose into framework-neutral Jakarta framing.
1671
+
1672
+ Applied only when the report's spring_present is False. Deterministic phrase
1673
+ substitution — no model, no guess. Guarantees the returned text contains no
1674
+ "Spring Boot" framing that the structured data does not support.
1675
+ """
1676
+ for needle, repl in _NON_SPRING_EXPLANATION_SUBS:
1677
+ text = text.replace(needle, repl)
1678
+ return text
1679
+
1680
+
1545
1681
  def _scan_file(
1546
1682
  source: str,
1547
1683
  rel_path: str,
@@ -1643,7 +1779,12 @@ def run_migrate_check(
1643
1779
  limitations: list[str] = []
1644
1780
  read_errors = 0
1645
1781
  jakarta_import_count = 0
1646
- spring_import_seen = False
1782
+ # BUG #1/#2: split Spring imports into runtime vs test. A test-tree file, or a
1783
+ # test-only Spring package (org.springframework.test / .boot.test), is a
1784
+ # test-only signal and must NOT mark the repo as runtime-Spring.
1785
+ spring_runtime_import_seen = False
1786
+ spring_test_import_seen = False
1787
+ from sourcecode.path_filters import is_test_path as _is_test_path
1647
1788
 
1648
1789
  # ── Java source scan ────────────────────────────────────────────────────
1649
1790
  for rel_path in file_paths:
@@ -1656,8 +1797,14 @@ def run_migrate_check(
1656
1797
 
1657
1798
  # Jakarta EE 9+ namespace adoption signal (vetoes a false Boot-2 verdict).
1658
1799
  jakarta_import_count += len(_JAKARTA_IMPORT_RE.findall(source))
1659
- if not spring_import_seen and _SPRING_IMPORT_RE.search(source):
1660
- spring_import_seen = True
1800
+ _in_test = _is_test_path(rel_path)
1801
+ for _m in _SPRING_IMPORT_RE.finditer(source):
1802
+ _sub = _m.group(1)
1803
+ _is_test_pkg = _sub.startswith(("test.", "boot.test.")) or _sub in ("test", "boot.test")
1804
+ if _in_test or _is_test_pkg:
1805
+ spring_test_import_seen = True
1806
+ else:
1807
+ spring_runtime_import_seen = True
1661
1808
 
1662
1809
  file_findings = _scan_file(source, rel_path, _ALL_RULES)
1663
1810
  filtered = [f for f in file_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
@@ -1713,13 +1860,24 @@ def run_migrate_check(
1713
1860
  limitations.extend(_STATIC_LIMITATIONS)
1714
1861
 
1715
1862
  spring_boot_2, spring_boot_version = _detect_spring_boot(root, jakarta_import_count)
1716
- spring_present = _detect_spring_present(root, spring_import_seen)
1863
+ spring_present, spring_test_only = _detect_spring_usage(
1864
+ root, spring_runtime_import_seen, spring_test_import_seen
1865
+ )
1717
1866
 
1718
1867
  # BUG #8: classify each finding's code context (main / test / generated) so the
1719
1868
  # report can keep test fixtures and autogenerated sources out of blocking_count.
1720
1869
  for f in all_findings:
1721
1870
  f.code_context = _classify_code_context(f)
1722
1871
 
1872
+ # BUG #3 (v1.68.0): on a non-Spring repo, rewrite Spring-Boot-specific framing in
1873
+ # every finding's explanation into framework-neutral Jakarta-EE framing. The
1874
+ # finding itself (javax→jakarta) stays; only the prose is corrected so it matches
1875
+ # the structured spring_present=False signal in the same report.
1876
+ if not spring_present:
1877
+ for f in all_findings:
1878
+ if f.explanation:
1879
+ f.explanation = _neutralize_non_spring_explanation(f.explanation)
1880
+
1723
1881
  # Hibernate 5→6 stratification (independent of min_severity — it is its own
1724
1882
  # risk model, not a severity-filtered finding stream).
1725
1883
  from sourcecode.hibernate_strat import analyze_hibernate
@@ -1729,6 +1887,8 @@ def run_migrate_check(
1729
1887
  spring_boot_2_detected=spring_boot_2,
1730
1888
  spring_boot_version_detected=spring_boot_version,
1731
1889
  spring_present=spring_present,
1890
+ spring_test_only=spring_test_only,
1891
+ jakarta_namespace_adopted=jakarta_import_count > 0,
1732
1892
  findings=all_findings,
1733
1893
  hibernate=hibernate_strat,
1734
1894
  limitations=limitations,
@@ -154,5 +154,36 @@ def is_vendor_path(path: str) -> bool:
154
154
  for part in dir_parts:
155
155
  if part in _LIB_SEGMENTS:
156
156
  return True
157
+ # Vendored web libraries shipped into a theme/webapp by file name, e.g.
158
+ # jquery-3.5.1.js, jquery-migrate-1.2.1.js, bootstrap.bundle.js. These carry
159
+ # third-party comments (jQuery's own "BUG:" notes) that must not be reported
160
+ # as the project's own code notes.
161
+ if _is_vendored_web_lib(filename) or any(part in _VENDOR_WEB_LIB_DIRS for part in dir_parts):
162
+ return True
163
+
164
+ return False
165
+
157
166
 
167
+ # Stems of common vendored JS/CSS libraries. A web-asset file whose name starts with
168
+ # one of these (optionally followed by a version/qualifier) is third-party, not the
169
+ # project's own source — wherever it physically sits in the tree.
170
+ _VENDOR_WEB_LIB_STEMS: tuple[str, ...] = (
171
+ "jquery", "bootstrap", "angular", "react", "vue", "svelte", "ember",
172
+ "lodash", "underscore", "moment", "d3", "three", "chart", "popper",
173
+ "modernizr", "select2", "datatables", "ckeditor", "tinymce", "fontawesome",
174
+ "font-awesome", "backbone", "knockout", "prototype", "scriptaculous",
175
+ "highcharts", "leaflet", "axios", "polyfill", "babel", "require", "requirejs",
176
+ "handlebars", "mustache", "dojo", "extjs", "swiper", "slick", "fullcalendar",
177
+ )
178
+ # Directory names that hold only vendored web assets.
179
+ _VENDOR_WEB_LIB_DIRS: frozenset[str] = frozenset({"jquery", "bootstrap", "webjars"})
180
+
181
+
182
+ def _is_vendored_web_lib(filename: str) -> bool:
183
+ base = filename.lower()
184
+ for stem in _VENDOR_WEB_LIB_STEMS:
185
+ s = stem.strip()
186
+ # match "jquery.js", "jquery-3.5.1.js", "jquery.min.js", "jquery-ui.js"
187
+ if base == f"{s}.js" or base == f"{s}.css" or base.startswith((f"{s}-", f"{s}.")):
188
+ return True
158
189
  return False
@@ -20,7 +20,7 @@ import subprocess
20
20
  from collections import deque
21
21
  from dataclasses import dataclass, field
22
22
  from pathlib import Path
23
- from typing import Any, Optional
23
+ from typing import Any, Iterable, Optional
24
24
 
25
25
  from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
26
26
  from sourcecode.path_filters import is_test_path as _is_test_path
@@ -4700,9 +4700,16 @@ def compute_blast_radius(
4700
4700
  )
4701
4701
  if is_mapper and fqn not in _seen_mapper_fqns:
4702
4702
  _seen_mapper_fqns.add(fqn)
4703
+ # BUG #5 (v1.68.0): distinguish a CONFIRMED persistence mapper (a
4704
+ # @Repository/DAO role or a MyBatis @Mapper interface) from a class that
4705
+ # only matched the name pattern (e.g. AzToRegionMapper — an AWS topology
4706
+ # mapper with no data layer). Only confirmed entries may be described as
4707
+ # "persistence" in the narrative.
4708
+ _is_persistence = role in _MAPPER_ROLES or symbol_kind == "mapper_interface"
4703
4709
  _mapper_entry: dict = {
4704
4710
  "fqn": fqn,
4705
4711
  "role": role or ("mapper" if symbol_kind == "mapper_interface" else "repository"),
4712
+ "mapper_kind": "persistence" if _is_persistence else "name_heuristic",
4706
4713
  "source_file": node_dict.get("source_file") or "",
4707
4714
  }
4708
4715
  if canonical != fqn:
@@ -4841,18 +4848,33 @@ def compute_blast_radius(
4841
4848
  _parts.append(f"{n_ep} endpoint{'s' if n_ep != 1 else ''} exposed")
4842
4849
  if n_txn:
4843
4850
  _parts.append(f"{n_txn} transactional boundary{'s' if n_txn != 1 else ''} touched")
4844
- if n_mappers:
4845
- _parts.append(f"{n_mappers} persistence path{'s' if n_mappers != 1 else ''} in blast cone")
4851
+ # BUG #5 (v1.68.0): only confirmed persistence mappers earn the word "persistence".
4852
+ # Name-heuristic matches (e.g. *Mapper utility classes with no data layer) are
4853
+ # surfaced as neutral "data-mapping class(es)" so the prose never invents a
4854
+ # persistence tier the repo does not have.
4855
+ _n_persist = sum(1 for m in mappers_affected if m.get("mapper_kind") == "persistence")
4856
+ _n_heur = n_mappers - _n_persist
4857
+ if _n_persist:
4858
+ _parts.append(f"{_n_persist} persistence path{'s' if _n_persist != 1 else ''} in blast cone")
4859
+ if _n_heur:
4860
+ _parts.append(f"{_n_heur} data-mapping class{'es' if _n_heur != 1 else ''} in blast cone")
4846
4861
  if n_sec:
4847
4862
  _parts.append(f"{n_sec} security-gated endpoint{'s' if n_sec != 1 else ''} affected")
4848
4863
  if n_modules > 1:
4849
4864
  _parts.append(f"impact crosses {n_modules} modules")
4850
4865
 
4866
+ # BUG #4 (v1.68.0): derive the DI-framework label ONCE from caller evidence and
4867
+ # reuse it for both the short explanation and the long via_interface_note, so the
4868
+ # two never disagree (the old code hardcoded "Spring/CDI" in one and "Spring/CDI/
4869
+ # Guice" in the other). When the actual framework is identifiable from the caller
4870
+ # FQNs (e.g. *.guice.* packages on a Guice repo), name it specifically instead of
4871
+ # a generic guess.
4872
+ _di_label = _detect_di_framework_label(direct_callers)
4851
4873
  if _iface_bridging:
4852
4874
  _iface_names = [b["interface"].split(".")[-1] for b in _iface_bridging]
4853
4875
  _parts.append(
4854
4876
  f"callers resolved via interface{'s' if len(_iface_names) > 1 else ''} "
4855
- f"({', '.join(_iface_names)}) — Spring/CDI DI pattern"
4877
+ f"({', '.join(_iface_names)}) — {_di_label} DI pattern"
4856
4878
  )
4857
4879
 
4858
4880
  # Transparency: hub-class BFS truncation must appear in explanation so the
@@ -4924,9 +4946,9 @@ def compute_blast_radius(
4924
4946
  if _iface_bridging:
4925
4947
  out["via_interface_resolution"] = _iface_bridging
4926
4948
  out["via_interface_note"] = (
4927
- "Target is a concrete class injected via interface(s) in DI frameworks "
4928
- "(Spring/CDI/Guice). direct_callers includes callers of the implemented "
4929
- "interface(s) — these are the real production dependents."
4949
+ "Target is a concrete class injected via interface(s) in a "
4950
+ f"{_di_label} DI framework. direct_callers includes callers of the "
4951
+ "implemented interface(s) — these are the real production dependents."
4930
4952
  )
4931
4953
  if _bfs_truncated:
4932
4954
  out["bfs_truncation_reason"] = "hub_class_depth_cap"
@@ -5106,3 +5128,31 @@ def _all_callers_from_rg(fqn: str, reverse_graph: dict[str, dict[str, list[str]]
5106
5128
  def _simple_name(fqn: str) -> str:
5107
5129
  """Extract the simple class name from a fully-qualified name."""
5108
5130
  return fqn.split(".")[-1].split("#")[0]
5131
+
5132
+
5133
+ def _detect_di_framework_label(caller_fqns: "Iterable[str]") -> str:
5134
+ """Name the DI framework(s) evidenced by the caller FQNs (BUG #4, v1.68.0).
5135
+
5136
+ Returns a "/"-joined label of the frameworks actually identifiable from the
5137
+ callers' package names (e.g. "Guice" when callers live under a ``*.guice.*``
5138
+ package, "Spring" under ``org.springframework``). When nothing is identifiable
5139
+ the label degrades to the generic "Spring/CDI/Guice" rather than asserting a
5140
+ framework the evidence does not support. The same label feeds both the short
5141
+ explanation and the long via_interface_note so they cannot disagree.
5142
+ """
5143
+ frameworks: set[str] = set()
5144
+ for fqn in caller_fqns:
5145
+ low = fqn.lower()
5146
+ if "guice" in low or "com.google.inject" in low:
5147
+ frameworks.add("Guice")
5148
+ if "springframework" in low or ".spring." in low:
5149
+ frameworks.add("Spring")
5150
+ if "jakarta.enterprise" in low or "javax.enterprise" in low or ".cdi." in low:
5151
+ frameworks.add("CDI")
5152
+ if "micronaut" in low:
5153
+ frameworks.add("Micronaut")
5154
+ if "dagger" in low:
5155
+ frameworks.add("Dagger")
5156
+ if frameworks:
5157
+ return "/".join(sorted(frameworks))
5158
+ return "Spring/CDI/Guice"
sourcecode/summarizer.py CHANGED
@@ -296,7 +296,7 @@ class ProjectSummarizer:
296
296
  _TYPE_LABELS: dict[str, str] = {
297
297
  "cli": "CLI",
298
298
  "api": "API",
299
- "web_mvc": "Spring MVC web app",
299
+ "web_mvc": "Server-side MVC web app",
300
300
  "webapp": "Web application",
301
301
  "library": "Library",
302
302
  "monorepo": "Monorepo",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.66.0
3
+ Version: 1.68.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
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=_rKEwisXr1IeFjMbYXGSfdRcelQmOHaAB0XR84P9tsk,103
1
+ sourcecode/__init__.py,sha256=gp3XLYiUZaatdehs90NAezSd-3vDxmo0g-OIVUqEBmg,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,15 +7,15 @@ 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=PwLLelj1ACLkWQBCEThD6vhn4sGMn5kEg-6gRX1IAb8,276536
10
+ sourcecode/cli.py,sha256=VqAiGRauTO-s8yBJh2JMldha3VKx0jTXC0tOvRpWQkI,283297
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
14
- sourcecode/context_summarizer.py,sha256=zlbm8ytdvJToohU108-dwBmEl52xl0gXpf6PZBOW_2A,6540
14
+ sourcecode/context_summarizer.py,sha256=cI2TZMvEhl0BEma12VtPaX6z03ZVBAetVzuK5GaCOvg,6852
15
15
  sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
16
16
  sourcecode/contract_pipeline.py,sha256=bNn9SYtwfI1CGKlYGxewexlp7_IWhpwSCae-BMuuVwQ,28895
17
17
  sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
18
- sourcecode/dependency_analyzer.py,sha256=qEnRiKFkleZJyLf_DyznJbWD1GJ881iG4RRDqH9oGQ4,61524
18
+ sourcecode/dependency_analyzer.py,sha256=fQCaWQ7_BNcVgZv8gweJCwJ1CGlXsz8nw_tqN3aHJNE,63952
19
19
  sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
20
20
  sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
21
21
  sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
@@ -29,14 +29,14 @@ 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
31
  sourcecode/hibernate_strat.py,sha256=ZyI0sikxh7IemU8uqn1Z73z-tHeFeCJuYhJnyQL8pzU,57765
32
- sourcecode/integration_detector.py,sha256=mQulsXN1P-4V_3ueh3xy_N9x3Aqlvm7GQ-3YGqlBEYM,8223
32
+ sourcecode/integration_detector.py,sha256=ygZwXHJXJ4QM6oiqKb5S6WIfElByDYdz9moyuFlZpYI,12997
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=daZLA7KaeoWoUPSMN8cYqHYD6HBflzc-z_N4bCGVPak,82993
36
+ sourcecode/migrate_check.py,sha256=Z0AZhWuZWfGuG3dsQugJn92FRpId5-SFAoyMc1RBZHk,91908
37
37
  sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
38
38
  sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
39
- sourcecode/path_filters.py,sha256=DMea0KIlmj2pzTkuy-3YYFF4ktP0q5zP9mH4v4uXh84,5237
39
+ sourcecode/path_filters.py,sha256=VnaD9jxZVNzluackNSTCdIddwzAqIseuSCs_A-gpCDM,6898
40
40
  sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
41
41
  sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
42
42
  sourcecode/prepare_context.py,sha256=PsRaKeABMh964niCS578WRgcccW9bajnm2yHS9zcKuM,222655
@@ -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=iwSE9JewlBkLuAFNDF5GgBPMKXPvf_hAxnLQrgeQX9Y,222027
49
+ sourcecode/repository_ir.py,sha256=Blo4rTCkY7EppwunxqlvI-1b9wU3EPLuHlZik3Ksz7c,224908
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
@@ -61,7 +61,7 @@ sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16
61
61
  sourcecode/spring_security_audit.py,sha256=XtPJ1SXlZJ8k6VYmaWuAp7Bbir4UmreAL7doIGQ5I7o,20595
62
62
  sourcecode/spring_semantic.py,sha256=O1nKSGVzlukuxLHQVuCPxc-XrcrMFxwlHA20_dmEGgM,13307
63
63
  sourcecode/spring_tx_analyzer.py,sha256=FdFcyqPp3aT9oJ-PKrnXcTA6s69wdvzG-NBm0GMGPTU,30717
64
- sourcecode/summarizer.py,sha256=zgdps7yS2IktAbWe7IWz0oUcr3QIuNPRGrsScbZ4R1g,21797
64
+ sourcecode/summarizer.py,sha256=hV-kPFXXh15GnIvmLZnmVWTEbmr35C-yKVtPiR25U2g,21802
65
65
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
66
66
  sourcecode/validation_surface.py,sha256=2Ojfzhw9R4XpemAFqb9RXf4FKyI3DYjGo4Dp0AHZMQo,22600
67
67
  sourcecode/version_check.py,sha256=CHp6ZxTIfo8kyHPCBgJA1uFC0xQCoXMuuOfrW8QTL8o,4942
@@ -88,10 +88,10 @@ sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5Tc
88
88
  sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
89
89
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
90
90
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
91
- sourcecode/mcp/orchestrator.py,sha256=kT7IssYNyQXVtDf2Q69qrMSTuyJlJ1Rhkp6_EHqc_38,36520
92
- sourcecode/mcp/registry.py,sha256=8ot8A9_ccPNUvHgFkUO8lIlbp4XT_UcHR101nc71Gjc,65840
91
+ sourcecode/mcp/orchestrator.py,sha256=diVoQgn24QmgPL3Ev8Sp6hsvh02OqY3MktHXOzrlodo,36525
92
+ sourcecode/mcp/registry.py,sha256=jLCUG7-m53Fbxqjzpt_OWu6NUp7b46S2ixTDixsTRyo,65975
93
93
  sourcecode/mcp/runner.py,sha256=-Dp2qPGRkfNTVen6bKh7WtzQqpcEtsrXoiuajvshlKk,2866
94
- sourcecode/mcp/server.py,sha256=DV7aiUObBGekShSDc7Iu67ISdesByutto3JOFbcYBFk,63693
94
+ sourcecode/mcp/server.py,sha256=1jwMR0OwJcLi1GoAKT300RSQAhfd_nYLrvABG7s0pUU,63874
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.66.0.dist-info/METADATA,sha256=PVY4maYclqHt2AKj_bcGmDzF7xmKhD0i7k2MrQepPKo,47341
107
- sourcecode-1.66.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
108
- sourcecode-1.66.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
109
- sourcecode-1.66.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
110
- sourcecode-1.66.0.dist-info/RECORD,,
106
+ sourcecode-1.68.0.dist-info/METADATA,sha256=_4nRHRSnFFBpmv9OJqSFf30pGcWo7XpOC0gyAJgoT4c,47341
107
+ sourcecode-1.68.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
108
+ sourcecode-1.68.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
109
+ sourcecode-1.68.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
110
+ sourcecode-1.68.0.dist-info/RECORD,,