sourcecode 1.71.0__py3-none-any.whl → 1.72.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.71.0"
3
+ __version__ = "1.72.0"
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import hashlib
18
18
  import json
19
+ import re
19
20
  from dataclasses import dataclass, field
20
21
  from pathlib import Path
21
22
  from typing import Any, Optional
@@ -37,6 +38,11 @@ IR_SCHEMA_VERSION = "1.0.0"
37
38
  # Edge types excluded from reverse graph (mirrors repository_ir._REVERSE_EXCLUDE)
38
39
  _REVERSE_EXCLUDE: frozenset[str] = frozenset({"annotated_with", "mapped_to"})
39
40
 
41
+ # A route path that looks like a Java FQN is framework dynamic-admin routing, not a
42
+ # real REST endpoint (mirrors the filter in repository_ir.extract_java_endpoints so
43
+ # the CIR endpoint count reconciles with the `endpoints` command). See BUG #7.
44
+ _FQN_PATH_RE: re.Pattern = re.compile(r"/(org|com|net|io|edu)\.[a-z][a-z0-9]*\.[a-zA-Z]")
45
+
40
46
 
41
47
  # ---------------------------------------------------------------------------
42
48
  # CanonicalSecurity
@@ -324,9 +330,17 @@ def ir_dict_to_canonical(
324
330
  # Build canonical endpoints from route_surface — stable ordering
325
331
  route_surface = ir.get("route_surface") or []
326
332
  # Deduplicate by endpoint id (route_surface can have duplicates from multi-prefix)
333
+ # BUG #7 (Broadleaf field test): the `endpoints` command (extract_java_endpoints)
334
+ # drops routes whose path looks like a Java FQN — framework dynamic-admin routing
335
+ # (Broadleaf @AdminSection registers entity class FQNs as URL segments) is not a
336
+ # real REST surface. The CIR endpoint list feeds `spring-audit` (endpoints_analyzed),
337
+ # `impact` and `validation`; it must apply the SAME filter so its count reconciles
338
+ # with `endpoints` instead of being silently inflated by those pseudo-routes.
327
339
  _seen_ids: set[str] = set()
328
340
  raw_endpoints: list[CanonicalEndpoint] = []
329
341
  for r in route_surface:
342
+ if _FQN_PATH_RE.search(r.get("path", "") or ""):
343
+ continue
330
344
  ep = _route_to_canonical_endpoint(r)
331
345
  if ep.id not in _seen_ids:
332
346
  _seen_ids.add(ep.id)
sourcecode/cli.py CHANGED
@@ -5764,11 +5764,63 @@ def modernize_cmd(
5764
5764
  key=lambda h: (-h["hotspot_score"], h["fqn"]),
5765
5765
  )[:15]
5766
5766
 
5767
- # Cross-module tangles: subsystems with high member count
5768
- tangle_modules = sorted(
5769
- [s for s in subsystems if len(s.get("members") or []) >= 5],
5770
- key=lambda s: -len(s.get("members") or []),
5771
- )[:10]
5767
+ # Cross-module tangles: actual inter-subsystem coupling measured from the
5768
+ # dependency graph, NOT a re-labelled subsystem list. For every structural
5769
+ # edge whose endpoints live in two different subsystems we tally a directed
5770
+ # (from_subsystem → to_subsystem) coupling count; pairs coupled in BOTH
5771
+ # directions are the real tangles (bidirectional/cyclic module coupling).
5772
+ _graph_edges: list = (ir.get("graph") or {}).get("edges") or []
5773
+ # Structural coupling edge types. annotated_with / mapped_to / event edges are
5774
+ # excluded (metadata + separate topology, not module-decomposition coupling).
5775
+ _TANGLE_EDGE_TYPES = frozenset({
5776
+ "imports", "injects", "extends", "implements",
5777
+ "references", "calls", "instantiates", "returns",
5778
+ })
5779
+ _fqn_to_pkg: dict[str, str] = {}
5780
+ _pkg_to_label: dict[str, str] = {}
5781
+ for _s in subsystems:
5782
+ _pp = _s.get("package_prefix") or _s.get("pkg") or ""
5783
+ if not _pp:
5784
+ continue
5785
+ _pkg_to_label[_pp] = _s.get("label") or _s.get("name") or _pp
5786
+ for _m in (_s.get("members") or []):
5787
+ _fqn_to_pkg[_m] = _pp
5788
+
5789
+ from collections import Counter as _Counter
5790
+ _pair_counts: "_Counter[tuple[str, str]]" = _Counter()
5791
+ _pair_example: dict[tuple[str, str], str] = {}
5792
+ for _e in _graph_edges:
5793
+ if _e.get("type") not in _TANGLE_EDGE_TYPES:
5794
+ continue
5795
+ _a = _fqn_to_pkg.get(_e.get("from"))
5796
+ _b = _fqn_to_pkg.get(_e.get("to"))
5797
+ if not _a or not _b or _a == _b:
5798
+ continue
5799
+ _pair_counts[(_a, _b)] += 1
5800
+ _pair_example.setdefault((_a, _b), f"{_e.get('from')} → {_e.get('to')}")
5801
+
5802
+ _cross_module_tangles = []
5803
+ for (_a, _b), _cnt in sorted(
5804
+ _pair_counts.items(), key=lambda kv: (-kv[1], kv[0])
5805
+ ):
5806
+ _reverse = _pair_counts.get((_b, _a), 0)
5807
+ _cross_module_tangles.append({
5808
+ "from_subsystem": _pkg_to_label.get(_a, _a),
5809
+ "to_subsystem": _pkg_to_label.get(_b, _b),
5810
+ "from_package": _a,
5811
+ "to_package": _b,
5812
+ "edge_count": _cnt,
5813
+ "reverse_edge_count": _reverse,
5814
+ # A mutual (cyclic) dependency is the actual "tangle" — both modules
5815
+ # reference each other, so neither can be extracted independently.
5816
+ "mutual": _reverse > 0,
5817
+ "example": _pair_example.get((_a, _b), ""),
5818
+ })
5819
+ # Surface mutual tangles first (highest decomposition risk), then by strength.
5820
+ _cross_module_tangles.sort(
5821
+ key=lambda t: (not t["mutual"], -t["edge_count"], t["from_package"])
5822
+ )
5823
+ _cross_module_tangles = _cross_module_tangles[:15]
5772
5824
 
5773
5825
  _summary = {
5774
5826
  "total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
@@ -5837,6 +5889,17 @@ def modernize_cmd(
5837
5889
  {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
5838
5890
  for n in coupling_nodes
5839
5891
  ],
5892
+ # BUG #6 (Broadleaf field test): make the in_degree metric self-describing
5893
+ # so it is not confused with `explain`'s caller count. They differ by design.
5894
+ "high_coupling_nodes_note": (
5895
+ "in_degree = raw count of incoming graph edges across ALL edge types "
5896
+ "(imports, injects, extends/implements, references, annotations), "
5897
+ "counted at symbol level. This is deliberately larger than and NOT "
5898
+ "equal to `sourcecode explain`'s caller count, which reports DISTINCT "
5899
+ "dependent classes (DI dependents + reverse-call-graph callers, "
5900
+ "deduplicated to class level). Use in_degree for blast-radius ranking, "
5901
+ "explain's caller list for the concrete dependents to inspect."
5902
+ ),
5840
5903
  "statically_unreferenced": [
5841
5904
  {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
5842
5905
  for n in dead_zones
@@ -5853,14 +5916,15 @@ def modernize_cmd(
5853
5916
  ],
5854
5917
  "subsystem_summary": _subsystem_summary,
5855
5918
  "subsystem_summary_note": _subsystem_summary_note,
5856
- "cross_module_tangles": [
5857
- {
5858
- "label": s.get("label") or s.get("name") or "",
5859
- "class_count": _class_count(s.get("members") or []),
5860
- "member_count": len(s.get("members") or []),
5861
- }
5862
- for s in tangle_modules
5863
- ],
5919
+ "cross_module_tangles": _cross_module_tangles,
5920
+ "cross_module_tangles_note": (
5921
+ "Directed inter-subsystem coupling measured from structural graph "
5922
+ "edges (imports/injects/extends/implements/references/calls). "
5923
+ "edge_count = edges from_package→to_package; mutual=true marks a "
5924
+ "bidirectional (cyclic) dependency — the actual tangle that blocks "
5925
+ "independent module extraction. Empty means no cross-subsystem "
5926
+ "structural coupling was detected."
5927
+ ),
5864
5928
  # BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
5865
5929
  "recommendation": (
5866
5930
  (
sourcecode/explain.py CHANGED
@@ -298,10 +298,15 @@ def _structural_purpose(
298
298
  head = f"Likely {role} (no DI annotations found)" if role else \
299
299
  "Likely a structurally significant class (no DI annotations found)"
300
300
  signals: list[str] = []
301
+ # BUG #6 (Broadleaf field test): this counts DISTINCT dependent classes (DI
302
+ # dependents + reverse-call-graph callers, deduped to class level), which is a
303
+ # different metric from modernize's node.in_degree (raw incoming-edge count
304
+ # across ALL edge types, symbol-level). Name it explicitly so the two numbers
305
+ # are not read as a contradiction when cross-referenced.
301
306
  if in_degree >= 3:
302
- signals.append(f"high in-degree ({in_degree})")
307
+ signals.append(f"high fan-in ({in_degree} distinct caller classes)")
303
308
  elif in_degree:
304
- signals.append(f"in-degree {in_degree}")
309
+ signals.append(f"{in_degree} distinct caller class{'es' if in_degree != 1 else ''}")
305
310
  if lifecycle:
306
311
  signals.append(f"lifecycle methods detected ({'/'.join(lifecycle)})")
307
312
  suffix = f" — {', '.join(signals)}" if signals else ""
@@ -83,6 +83,17 @@ _TOKEN_CLIENTS: "tuple[tuple[str, str, str], ...]" = (
83
83
  ("InitialLdapContext", "ldap", "jndi-ldap"),
84
84
  ("InitialDirContext", "ldap", "jndi-ldap"),
85
85
  ("LdapContext", "ldap", "jndi-ldap"),
86
+ # Spring Security LDAP authentication (BUG #5, Broadleaf field test): the
87
+ # idiomatic auth integration is a UserDetails mapper that extends
88
+ # LdapUserDetailsMapper and consumes DirContextOperations — neither uses
89
+ # LdapTemplate/ContextSource directly, so the connection-type tokens above
90
+ # miss it entirely. These types come from spring-security-ldap / spring-ldap
91
+ # and are unambiguous LDAP integration points.
92
+ ("LdapUserDetailsMapper", "ldap", "spring-security-ldap"),
93
+ ("DirContextOperations", "ldap", "spring-ldap"),
94
+ ("LdapContextSource", "ldap", "spring-ldap"),
95
+ ("DefaultSpringSecurityContextSource", "ldap", "spring-security-ldap"),
96
+ ("BindAuthenticator", "ldap", "spring-security-ldap"),
86
97
  # Mail / SMTP (JavaMail / Jakarta Mail)
87
98
  ("JavaMailSender", "smtp", "spring-mail"),
88
99
  ("MimeMessage", "smtp", "javamail"),
@@ -645,6 +645,18 @@ def _classify_code_context(finding: "MigrationFinding") -> str:
645
645
  if is_test_or_fixture_path(path):
646
646
  return "test"
647
647
  norm = path.replace("\\", "/").lower()
648
+ # BUG #4 (Broadleaf field test): a Spring XML config file can live under
649
+ # src/main/resources yet be test-only scaffolding loaded exclusively by
650
+ # test @ContextConfiguration (e.g. bl-applicationContext-test-security.xml).
651
+ # is_test_or_fixture_path deliberately ignores the filename, so a `test`
652
+ # marker in the config file's own name is not seen. For XML config, a `test`
653
+ # token in the base filename is a strong, low-FP test-scope signal —
654
+ # production context files are not named `-test-`. Keep it out of
655
+ # blocking_count so test scaffolding never inflates the headline blocker.
656
+ if norm.endswith(".xml"):
657
+ base = norm.rsplit("/", 1)[-1]
658
+ if re.search(r"(?:^|[-_.])test(?:[-_.]|$)", base):
659
+ return "test"
648
660
  if any(frag in norm for frag in _GENERATED_PATH_FRAGMENTS):
649
661
  return "generated"
650
662
  # javax.annotation.Generated is the marker emitted into autogenerated code.
@@ -936,6 +948,42 @@ def _find_non_java_files(
936
948
  return xml_files, build_files
937
949
 
938
950
 
951
+ def _xml_explanation_for(rule: "_XmlRule", matches: list) -> str:
952
+ """Return an explanation reflecting the sub-pattern that actually matched.
953
+
954
+ BUG #4 (Broadleaf field test): MIG-031's pattern is an OR of two independent
955
+ triggers — old-style `<http auto-config="true">` and a versioned legacy schema
956
+ (spring-security-[2-5].x.xsd). The static template asserted BOTH, so a file
957
+ that only used auto-config (its schema line already being the unversioned
958
+ spring-security.xsd) got a factually wrong explanation claiming a versioned
959
+ schema. Emit only the condition(s) present in this file.
960
+ """
961
+ if rule.id != "MIG-031":
962
+ return rule.explanation
963
+ matched_text = " ".join(m.group(0).lower() for m in matches)
964
+ has_autoconfig = "auto-config" in matched_text
965
+ has_versioned_schema = bool(re.search(r"spring-security-[2345]\.", matched_text))
966
+ triggers: list[str] = []
967
+ if has_autoconfig:
968
+ triggers.append(
969
+ "uses the old-style <http auto-config='true'> shortcut, which was "
970
+ "removed in Spring Security 6"
971
+ )
972
+ if has_versioned_schema:
973
+ triggers.append(
974
+ "references a versioned legacy schema (spring-security-[2-5].x.xsd) "
975
+ "whose namespace attributes changed in Spring Security 6"
976
+ )
977
+ if not triggers: # defensive — pattern matched but neither branch classified
978
+ return rule.explanation
979
+ return (
980
+ "XML-based Spring Security configuration in this file "
981
+ + " and ".join(triggers)
982
+ + ". Spring Security 6 (Spring Boot 3) requires migrating this to "
983
+ "Java-based @Configuration with a SecurityFilterChain @Bean."
984
+ )
985
+
986
+
939
987
  def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
940
988
  """Apply XML rules to raw XML text. Returns one finding per matched rule."""
941
989
  findings: list[MigrationFinding] = []
@@ -947,6 +995,7 @@ def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
947
995
  continue
948
996
  first_line = text[: matches[0].start()].count("\n") + 1
949
997
  snippets = [m.group(0)[:120].strip() for m in matches[:5]]
998
+ explanation = _xml_explanation_for(rule, matches)
950
999
  findings.append(
951
1000
  MigrationFinding(
952
1001
  id=MigrationFinding.make_id(rule.id, rel_path),
@@ -956,7 +1005,7 @@ def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
956
1005
  source_file=rel_path,
957
1006
  first_line=first_line,
958
1007
  imports_found=snippets,
959
- explanation=rule.explanation,
1008
+ explanation=explanation,
960
1009
  fix_hint=rule.fix_hint,
961
1010
  migration_target=rule.migration_target,
962
1011
  openrewrite_recipe=rule.openrewrite_recipe,
@@ -387,6 +387,59 @@ def _strip_java_comments(source: str) -> str:
387
387
  return source
388
388
 
389
389
 
390
+ # Declaration keyword OR a brace — matched in source order to track nesting when
391
+ # building minimal symbols for pre-scan-skipped files (see _minimal_class_symbols).
392
+ _MIN_DECL_OR_BRACE_RE = re.compile(r'\b(?:class|interface|enum)\s+([A-Z]\w*)|([{}])')
393
+
394
+
395
+ def _minimal_class_symbols(
396
+ source: str, package: str, rel_path: str
397
+ ) -> "list[SymbolRecord]":
398
+ """Nesting-aware class/interface/enum symbols for pre-scan-skipped files.
399
+
400
+ Tracks brace depth so a NESTED type gets a fully-qualified `pkg.Outer.Inner`
401
+ FQN (matching _extract_symbols), never a package-only `pkg.Inner`.
402
+
403
+ BUG #2 (Broadleaf field test): the previous fast path emitted every declared
404
+ type as `f"{pkg}.{name}"`, dropping the enclosing class. Two distinct nested
405
+ types with the same simple name under different outer classes in one package
406
+ (Broadleaf has 25 `GroupName` nested classes) therefore collapsed onto ONE
407
+ colliding FQN, and — since pre-scan-skipped files build no relations — those
408
+ zero-degree phantom nodes were reported as false `statically_unreferenced`
409
+ dead code. Qualifying by the enclosing class keeps them distinct and correct.
410
+ """
411
+ decl_source = _STRING_LITERAL_RE.sub('', _strip_java_comments(source))
412
+ syms: "list[SymbolRecord]" = []
413
+ stack: "list[tuple[str, int]]" = [] # (fqn, brace-depth at which body opened)
414
+ depth = 0
415
+ pending_fqn: "Optional[str]" = None
416
+ for m in _MIN_DECL_OR_BRACE_RE.finditer(decl_source):
417
+ name = m.group(1)
418
+ if name is not None:
419
+ enclosing = stack[-1][0] if stack else None
420
+ if enclosing:
421
+ pending_fqn = f"{enclosing}.{name}"
422
+ else:
423
+ pending_fqn = f"{package}.{name}" if package else name
424
+ continue
425
+ brace = m.group(2)
426
+ if brace == '{':
427
+ depth += 1
428
+ if pending_fqn is not None:
429
+ stack.append((pending_fqn, depth))
430
+ syms.append(SymbolRecord(
431
+ symbol=pending_fqn, type="class", confidence="medium",
432
+ declaring_file=rel_path,
433
+ ))
434
+ pending_fqn = None
435
+ else: # '}'
436
+ while stack and stack[-1][1] == depth:
437
+ stack.pop()
438
+ if depth > 0:
439
+ depth -= 1
440
+ return syms
441
+
442
+
390
443
  def _parse_annotation_line(line: str) -> tuple[str, str]:
391
444
  """Parse annotation name and args from a line starting with '@'.
392
445
 
@@ -1127,6 +1180,234 @@ def _build_same_package_map(symbols: list[SymbolRecord]) -> dict[str, dict[str,
1127
1180
  return result
1128
1181
 
1129
1182
 
1183
+ # A type-qualified member reference inside an annotation argument, e.g. the
1184
+ # `GroupName.General` / `FieldOrder.NAME` in
1185
+ # `@AdminPresentation(group = GroupName.General, order = FieldOrder.NAME)`, or the
1186
+ # fully-qualified nested form `SystemPropertyAdminPresentation.GroupOrder.General`.
1187
+ # group(0) is the whole dotted chain (UpperCamel head + `.member`… tail); the
1188
+ # resolver walks the chain through known nested types so a reference to
1189
+ # `Outer.Nested.CONST` credits the NESTED holder, not just the outer type.
1190
+ _ANN_REF_TOKEN_RE = re.compile(r'\b[A-Z]\w*(?:\.\w+)+')
1191
+ # Combined token scanner: an annotation-with-args head, a type declaration, or a
1192
+ # brace — matched in source order so annotation references can be attributed to
1193
+ # the class whose body encloses them.
1194
+ _ANN_OR_DECL_OR_BRACE_RE = re.compile(
1195
+ r'@([A-Z]\w*)\s*\('
1196
+ r'|\b(?:class|interface|enum)\s+([A-Z]\w*)'
1197
+ r'|([{}])'
1198
+ )
1199
+
1200
+
1201
+ def _extract_annotation_type_refs(
1202
+ source: str, package: str
1203
+ ) -> "list[tuple[str, str]]":
1204
+ """Return (enclosing_class_fqn, referenced_type_simple_name) pairs.
1205
+
1206
+ Walks the comment/string-stripped source tracking the nesting stack so each
1207
+ type reference found inside an annotation argument list is attributed to the
1208
+ class whose body encloses the annotated member. Member-level annotations sit
1209
+ inside the class body (stack has the class); this is the common case for
1210
+ `@AdminPresentation(group = GroupName.General)` on entity fields.
1211
+ """
1212
+ text = _STRING_LITERAL_RE.sub('""', _strip_java_comments(source))
1213
+ refs: "list[tuple[str, str]]" = []
1214
+ stack: "list[tuple[str, int]]" = []
1215
+ depth = 0
1216
+ pending: "Optional[str]" = None
1217
+ # Type refs found in a CLASS-LEVEL annotation (which precedes the class body,
1218
+ # so the stack does not yet contain the owning class) are held here and
1219
+ # attributed to the class when its body opens — e.g. an interface annotated
1220
+ # `@AdminTabPresentation(name = TabName.General)` that references its OWN
1221
+ # nested constant holders.
1222
+ pending_class_refs: "list[str]" = []
1223
+ n = len(text)
1224
+ idx = 0
1225
+ while idx < n:
1226
+ m = _ANN_OR_DECL_OR_BRACE_RE.search(text, idx)
1227
+ if not m:
1228
+ break
1229
+ ann, cname, brace = m.group(1), m.group(2), m.group(3)
1230
+ if ann is not None:
1231
+ # Balance the annotation's parens and scan its argument text.
1232
+ popen = m.end() - 1
1233
+ d = 0
1234
+ j = popen
1235
+ while j < n:
1236
+ c = text[j]
1237
+ if c == '(':
1238
+ d += 1
1239
+ elif c == ')':
1240
+ d -= 1
1241
+ if d == 0:
1242
+ break
1243
+ j += 1
1244
+ arg_text = text[popen + 1:j]
1245
+ cur_class = stack[-1][0] if stack else ""
1246
+ for tm in _ANN_REF_TOKEN_RE.finditer(arg_text):
1247
+ if cur_class:
1248
+ refs.append((cur_class, tm.group(0)))
1249
+ else:
1250
+ pending_class_refs.append(tm.group(0))
1251
+ idx = j + 1
1252
+ continue
1253
+ if cname is not None:
1254
+ enc = stack[-1][0] if stack else None
1255
+ pending = f"{enc}.{cname}" if enc else (
1256
+ f"{package}.{cname}" if package else cname
1257
+ )
1258
+ idx = m.end()
1259
+ continue
1260
+ if brace == '{':
1261
+ depth += 1
1262
+ if pending is not None:
1263
+ stack.append((pending, depth))
1264
+ # A class-level annotation on this class referenced these types.
1265
+ for head in pending_class_refs:
1266
+ refs.append((pending, head))
1267
+ pending_class_refs = []
1268
+ pending = None
1269
+ else: # '}'
1270
+ while stack and stack[-1][1] == depth:
1271
+ stack.pop()
1272
+ if depth > 0:
1273
+ depth -= 1
1274
+ idx = m.end()
1275
+ return refs
1276
+
1277
+
1278
+ def _annotation_reference_edges(
1279
+ ref_sources: "list[tuple[str, list[str], list[tuple[str, str]]]]",
1280
+ all_symbols: "list[SymbolRecord]",
1281
+ relations: "list[RelationEdge]",
1282
+ same_pkg_map: "dict[str, dict[str, str]]",
1283
+ ) -> "list[RelationEdge]":
1284
+ """Emit `references` edges for static constants read inside annotation args.
1285
+
1286
+ BUG #2 (Broadleaf field test): `@AdminPresentation(group = GroupName.General)`
1287
+ is a real reference to the constant-holder nested class `GroupName`, but the
1288
+ static call-graph never counted annotation-argument reads as edges. The
1289
+ referenced nested classes therefore had zero in-degree and were reported as
1290
+ false `statically_unreferenced` dead code (~95% of that list on Broadleaf).
1291
+
1292
+ Resolution for a bare head type T referenced from class C:
1293
+ import / same-package / wildcard → direct FQN, else
1294
+ nested-type lookup: a unique repo nested type named T, or — when the simple
1295
+ name is ambiguous (Broadleaf has 25 `GroupName`s) — the one declared by C or
1296
+ one of C's (transitive) supertypes. Unresolvable / ambiguous refs are
1297
+ dropped (honest: no guessed edge).
1298
+ """
1299
+ class_fqns: set[str] = {
1300
+ s.symbol for s in all_symbols
1301
+ if s.type in ("class", "interface") and "#" not in s.symbol
1302
+ }
1303
+ # enclosing_of[nested_fqn] = outer_fqn; nested_by_simple[simple] = {fqns}.
1304
+ enclosing_of: dict[str, str] = {}
1305
+ nested_by_simple: dict[str, set[str]] = {}
1306
+ for fqn in class_fqns:
1307
+ prefix = fqn
1308
+ while True:
1309
+ cut = prefix.rfind(".")
1310
+ if cut < 0:
1311
+ break
1312
+ prefix = prefix[:cut]
1313
+ if prefix in class_fqns:
1314
+ enclosing_of[fqn] = prefix
1315
+ nested_by_simple.setdefault(fqn[len(prefix) + 1:], set()).add(fqn)
1316
+ break
1317
+
1318
+ direct_super: dict[str, set[str]] = {}
1319
+ for e in relations:
1320
+ if e.type in ("extends", "implements"):
1321
+ direct_super.setdefault(e.from_symbol, set()).add(e.to_symbol)
1322
+
1323
+ def _super_closure(fqn: str) -> set[str]:
1324
+ seen: set[str] = set()
1325
+ stack = [fqn]
1326
+ while stack:
1327
+ cur = stack.pop()
1328
+ for sup in direct_super.get(cur, ()):
1329
+ if sup not in seen:
1330
+ seen.add(sup)
1331
+ stack.append(sup)
1332
+ return seen
1333
+
1334
+ edges: list[RelationEdge] = []
1335
+ seen_edges: set[tuple[str, str]] = set()
1336
+ for package, raw_imports, ref_pairs in ref_sources:
1337
+ if not ref_pairs:
1338
+ continue
1339
+ import_map: dict[str, str] = {}
1340
+ wildcard_pkgs: list[str] = []
1341
+ for fq in raw_imports:
1342
+ parts = fq.split(".")
1343
+ if parts[-1] == "*":
1344
+ wildcard_pkgs.append(fq[:-2])
1345
+ else:
1346
+ import_map[parts[-1]] = fq
1347
+ pkg_types = same_pkg_map.get(package, {})
1348
+
1349
+ def _resolve_head(head: str, cur_class: str) -> "Optional[str]":
1350
+ fq = import_map.get(head) or pkg_types.get(head)
1351
+ if fq:
1352
+ return fq
1353
+ for wp in wildcard_pkgs:
1354
+ cand = same_pkg_map.get(wp, {}).get(head)
1355
+ if cand:
1356
+ return cand
1357
+ # cur_class itself may BE the head (self-qualified nested ref).
1358
+ if cur_class.rsplit(".", 1)[-1] == head:
1359
+ return cur_class
1360
+ cands = nested_by_simple.get(head)
1361
+ if not cands:
1362
+ return None
1363
+ if len(cands) == 1:
1364
+ return next(iter(cands))
1365
+ allowed = {cur_class} | _super_closure(cur_class)
1366
+ matched = [c for c in cands if enclosing_of.get(c) in allowed]
1367
+ if len(matched) == 1:
1368
+ return matched[0]
1369
+ self_nested = f"{cur_class}.{head}"
1370
+ if self_nested in cands:
1371
+ return self_nested
1372
+ return None
1373
+
1374
+ def _resolve_chain(chain: str, cur_class: str) -> "Optional[str]":
1375
+ parts = chain.split(".")
1376
+ fqn = _resolve_head(parts[0], cur_class)
1377
+ if not fqn:
1378
+ return None
1379
+ # Walk the remaining segments through KNOWN nested types so a
1380
+ # `Outer.Nested.CONST` reference credits the nested holder actually read,
1381
+ # not merely the outer type. Stop at the first non-type segment (the
1382
+ # constant/field), which is the deepest resolvable class.
1383
+ for seg in parts[1:]:
1384
+ nxt = f"{fqn}.{seg}"
1385
+ if nxt in class_fqns:
1386
+ fqn = nxt
1387
+ else:
1388
+ break
1389
+ return fqn
1390
+
1391
+ for cur_class, chain in ref_pairs:
1392
+ if cur_class not in class_fqns:
1393
+ continue
1394
+ target = _resolve_chain(chain, cur_class)
1395
+ if not target or target == cur_class or target not in class_fqns:
1396
+ continue
1397
+ key = (cur_class, target)
1398
+ if key in seen_edges:
1399
+ continue
1400
+ seen_edges.add(key)
1401
+ edges.append(RelationEdge(
1402
+ from_symbol=cur_class,
1403
+ to_symbol=target,
1404
+ type="references",
1405
+ confidence="high",
1406
+ evidence={"type": "annotation_value", "value": chain},
1407
+ ))
1408
+ return edges
1409
+
1410
+
1130
1411
  # Reserved words that read like calls (`if (`, `for (`, …). Can never be sibling
1131
1412
  # method names (Java reserves them), but guard the intra-class scan anyway.
1132
1413
  _CALL_KEYWORDS: frozenset[str] = frozenset({
@@ -3374,6 +3655,9 @@ def build_repo_ir(
3374
3655
  )
3375
3656
 
3376
3657
  _per_file: list[tuple[str, str, str, list[str], list[SymbolRecord]]] = []
3658
+ # (package, imports, annotation-ref-pairs) for the reference-graph post-pass,
3659
+ # collected from BOTH fully-parsed and pre-scan-skipped files (BUG #2).
3660
+ _ann_ref_sources: list[tuple[str, list[str], list[tuple[str, str]]]] = []
3377
3661
  for rel_path in sorted(file_paths):
3378
3662
  abs_path = root / rel_path
3379
3663
  try:
@@ -3400,23 +3684,31 @@ def build_repo_ir(
3400
3684
  # literals before scanning so only real declarations are captured. The name
3401
3685
  # must also start uppercase ([A-Z]) — Java type convention, matching the
3402
3686
  # precision of the full _CLASS_DECL_RE used on annotated files.
3403
- _decl_source = _STRING_LITERAL_RE.sub('', _strip_java_comments(source))
3404
- _min_syms: list[SymbolRecord] = []
3405
- for _cm in re.finditer(r'\b(?:class|interface|enum)\s+([A-Z]\w*)', _decl_source):
3406
- _cls_name = _cm.group(1)
3407
- _fqn = f"{_pkg}.{_cls_name}" if _pkg else _cls_name
3408
- _min_syms.append(SymbolRecord(
3409
- symbol=_fqn, type="class", confidence="medium",
3410
- declaring_file=rel_path,
3411
- ))
3687
+ _min_syms = _minimal_class_symbols(source, _pkg, rel_path)
3412
3688
  all_symbols.extend(_min_syms)
3413
- # No relations needed for non-annotated files
3689
+ # No relations needed for non-annotated files. BUG #2: but a pre-scan-
3690
+ # skipped file can still hold annotation-argument constant references —
3691
+ # e.g. a presentation interface whose class-level @AdminTabPresentation
3692
+ # references its own nested TabName/TabOrder holders. Capture those refs
3693
+ # (cheaply, no full parse) so the reference-graph post-pass can resolve
3694
+ # them and the holders are not flagged as false dead code.
3695
+ if "@" in source:
3696
+ _skipped_refs = _extract_annotation_type_refs(source, _pkg)
3697
+ if _skipped_refs:
3698
+ _imports = re.findall(
3699
+ r'^\s*import\s+(?:static\s+)?([\w.]+)\s*;', source, re.MULTILINE
3700
+ )
3701
+ _ann_ref_sources.append((_pkg, _imports, _skipped_refs))
3414
3702
  continue
3415
3703
  package, symbols, raw_imports = _extract_symbols(
3416
3704
  source, rel_path, extra_capture=_extra_capture
3417
3705
  )
3418
3706
  all_symbols.extend(symbols)
3419
3707
  _per_file.append((rel_path, source, package, raw_imports, symbols))
3708
+ if "@" in source:
3709
+ _parsed_refs = _extract_annotation_type_refs(source, package)
3710
+ if _parsed_refs:
3711
+ _ann_ref_sources.append((package, raw_imports, _parsed_refs))
3420
3712
 
3421
3713
  # Build {package: {simple_name: FQN}} from every class/interface found.
3422
3714
  _same_pkg_map: dict[str, dict[str, str]] = _build_same_package_map(all_symbols)
@@ -3454,6 +3746,15 @@ def build_repo_ir(
3454
3746
 
3455
3747
  all_relations.extend(relations)
3456
3748
 
3749
+ # BUG #2 (Broadleaf): annotation-argument static-constant reads
3750
+ # (@Anno(attr = SomeType.CONST)) are real references — emit them as edges so
3751
+ # constant-holder types are not reported as zero-caller dead code. Runs as a
3752
+ # global post-pass because bare inherited-nested references (e.g. `GroupName`
3753
+ # from an implemented interface) need the repo-wide supertype/nested index.
3754
+ all_relations.extend(
3755
+ _annotation_reference_edges(_ann_ref_sources, all_symbols, all_relations, _same_pkg_map)
3756
+ )
3757
+
3457
3758
  spring_summary = _build_spring_summary(all_symbols)
3458
3759
 
3459
3760
  # Deduplicate relations
@@ -491,6 +491,17 @@ def run_security_audit(
491
491
  limitations=_sec_limitations,
492
492
  metadata={
493
493
  "endpoints_analyzed": len(cir.endpoints),
494
+ "endpoints_analyzed_note": (
495
+ "Count of canonical endpoints analyzed for security: Spring-MVC/JAX-RS "
496
+ "annotated handlers plus OpenAPI-spec-recovered routes. Built from the "
497
+ "SAME route surface as the `endpoints` command and with the SAME "
498
+ "exclusion of framework dynamic-admin FQN-shaped paths, but "
499
+ "DEDUPLICATED by (method, path, controller, handler) — so this value is "
500
+ "<= the `endpoints` command total, the small difference being "
501
+ "multi-prefix routes that collapse to one canonical endpoint here. It "
502
+ "does not include the non-Spring REST surface (`endpoints` reports that "
503
+ "separately under non_spring_rest_surface)."
504
+ ),
494
505
  "security_model": cir.metadata.get("security_model", "unknown"),
495
506
  "analysis_time_ms": elapsed_ms,
496
507
  },
@@ -56,13 +56,61 @@ _WRITE_METHOD_RE = re.compile(
56
56
  # _CATCH_SWALLOW_RE is retained for reference but replaced by brace-counting
57
57
  # extraction in _has_swallowed_exception to avoid false positives from nested
58
58
  # braces (nested if/try blocks inside catch terminating the match prematurely).
59
- _CATCH_HEADER_RE = re.compile(r'\bcatch\s*\([^)]+\)\s*\{')
59
+ _CATCH_HEADER_RE = re.compile(r'\bcatch\s*\(([^)]+)\)\s*\{')
60
60
  _LOG_IN_CATCH_RE = re.compile(r'\b(?:log|logger|LOG|System\.out|e\.print)\b')
61
61
  _RETHROW_IN_CATCH_RE = re.compile(r'\bthrow\b')
62
62
  # Non-trivial return (method call) inside a catch block indicates recovery, not
63
63
  # silent swallowing — e.g. `return findNextId(idType)` after creating a missing row.
64
64
  _RECOVERY_RETURN_RE = re.compile(r'\breturn\s+\w[\w.<>]*\s*\(')
65
65
 
66
+ # Exception types whose catch is a documented part of normal control flow, not a
67
+ # masked failure. Catching these is the standard JPA/Spring-Data idiom (e.g.
68
+ # Query.getSingleResult() throws NoResultException by contract when 0 rows match;
69
+ # a tolerant insert catches EntityExistsException on a race). Catching one of
70
+ # these must never trigger TX-005. (Broadleaf field test: 4/4 TX-005 findings
71
+ # were this idiom — 100% FP rate.)
72
+ _EXPECTED_CONTROL_FLOW_EXCEPTIONS = frozenset({
73
+ "NoResultException",
74
+ "NonUniqueResultException",
75
+ "EntityExistsException",
76
+ "EntityNotFoundException",
77
+ "EmptyResultDataAccessException",
78
+ "IncorrectResultSizeDataAccessException",
79
+ })
80
+ # A caught type is "expected" if any of its simple-name tokens is in the set above.
81
+ _CATCH_TYPE_TOKEN_RE = re.compile(r'[A-Za-z_][A-Za-z0-9_.]*')
82
+ # Lexical markers of a deliberate, developer-documented no-op inside a catch block.
83
+ # When the catch body's leading comment says the swallow is intentional, TX-005 is
84
+ # not reporting a bug — it is second-guessing an explicit decision.
85
+ _INTENTIONAL_SWALLOW_COMMENT_RE = re.compile(
86
+ r'(?://|/\*|\*)[^\n]*?\b('
87
+ r'intentional|intentionally|acceptable|expected|'
88
+ r'do\s+nothing|no[\s\-]?op|noop|ignore[ds]?|ignoring|'
89
+ r'on\s+purpose|safe\s+to\s+ignore|by\s+design|not\s+an?\s+error'
90
+ r')\b',
91
+ re.IGNORECASE,
92
+ )
93
+
94
+
95
+ def _caught_types(catch_header_inner: str) -> list[str]:
96
+ """Extract caught exception simple-name tokens from a catch header inner text.
97
+
98
+ `catch_header_inner` is the text inside the parens, e.g.
99
+ "NoResultException nre" or "IllegalStateException | IOException e".
100
+ Returns simple names only (last dotted segment), lowercase-preserving.
101
+ """
102
+ # Drop the trailing variable name; split multi-catch on '|'.
103
+ types: list[str] = []
104
+ for alt in catch_header_inner.split('|'):
105
+ toks = _CATCH_TYPE_TOKEN_RE.findall(alt)
106
+ # First token(s) are the type; the last bare identifier is the var name.
107
+ # A type token contains '.' or starts uppercase; the var name is lowercase.
108
+ for tok in toks:
109
+ simple = tok.rsplit('.', 1)[-1]
110
+ if simple and simple[0].isupper():
111
+ types.append(simple)
112
+ return types
113
+
66
114
 
67
115
  def _extract_method_body(source: str, method_name: str) -> str:
68
116
  """Extract the first method body matching method_name using brace counting.
@@ -597,7 +645,7 @@ class _TX005ExceptionSwallowing:
597
645
  depth -= 1
598
646
  i += 1
599
647
  body = source[brace_pos:i]
600
- for catch_block in self._extract_catch_blocks(body):
648
+ for caught_header, catch_block in self._extract_catch_blocks(body):
601
649
  if not _LOG_IN_CATCH_RE.search(catch_block):
602
650
  continue
603
651
  if _RETHROW_IN_CATCH_RE.search(catch_block):
@@ -605,18 +653,31 @@ class _TX005ExceptionSwallowing:
605
653
  # Non-trivial return (method call) indicates recovery, not swallowing.
606
654
  if _RECOVERY_RETURN_RE.search(catch_block):
607
655
  continue
656
+ # Catching an exception that is part of normal control flow (JPA
657
+ # getSingleResult / tolerant insert) is not error-masking — skip.
658
+ caught = _caught_types(caught_header)
659
+ if any(t in _EXPECTED_CONTROL_FLOW_EXCEPTIONS for t in caught):
660
+ continue
661
+ # A developer-documented deliberate no-op is an explicit decision,
662
+ # not a bug the tool should flag.
663
+ if _INTENTIONAL_SWALLOW_COMMENT_RE.search(catch_block):
664
+ continue
608
665
  return True
609
666
  return False
610
667
 
611
668
  @staticmethod
612
- def _extract_catch_blocks(body: str) -> list[str]:
613
- """Extract full catch block bodies from a method body using brace counting.
614
-
615
- Handles nested braces correctly unlike a simple [^}]* regex which
616
- terminates at the first nested '}' inside the catch block.
669
+ def _extract_catch_blocks(body: str) -> list[tuple[str, str]]:
670
+ """Extract (caught-type-header, catch-body) pairs from a method body.
671
+
672
+ Uses brace counting so nested braces inside the catch body do not
673
+ terminate the match prematurely (unlike a simple [^}]* regex). The
674
+ caught-type-header is the text inside the catch parens (e.g.
675
+ "NoResultException nre"), used to distinguish expected-control-flow
676
+ exceptions from genuinely swallowed failures.
617
677
  """
618
- blocks: list[str] = []
678
+ blocks: list[tuple[str, str]] = []
619
679
  for m in _CATCH_HEADER_RE.finditer(body):
680
+ header_inner = m.group(1)
620
681
  brace_pos = m.end() - 1
621
682
  depth = 1
622
683
  i = brace_pos + 1
@@ -627,7 +688,7 @@ class _TX005ExceptionSwallowing:
627
688
  elif c == '}':
628
689
  depth -= 1
629
690
  i += 1
630
- blocks.append(body[brace_pos:i])
691
+ blocks.append((header_inner, body[brace_pos:i]))
631
692
  return blocks
632
693
 
633
694
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.71.0
3
+ Version: 1.72.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,13 +1,13 @@
1
- sourcecode/__init__.py,sha256=y7H6BpC2bXN93kgVpSIJ6MrHneDY6yl0VNPo_qOu4Ts,103
1
+ sourcecode/__init__.py,sha256=m6miAE4bWPKWjY-CMgVBctXpyTJ_4N6fgxWDzFMCCTk,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=r_xf-SWXwUm3nVQMCSXHJ3M8zKsQP7Ze8Nqf6TVLRq8,45998
4
4
  sourcecode/architecture_summary.py,sha256=UaxmUU77oTlWxttMU3c6PxHrU-Q6q5N31TeU_NWWTdU,24297
5
5
  sourcecode/ast_extractor.py,sha256=sa6CmLpn-k5G3_Hzxn8hAlZ5-TS-EVzXDD0Gvxd2jzs,50613
6
6
  sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
7
- sourcecode/canonical_ir.py,sha256=DEwucOPJguLsVtg5cV8mWXNi112l5jmBhv73KGGebVk,24849
7
+ sourcecode/canonical_ir.py,sha256=IftvTlokcSLbZYHjXQu55FnS_X1LU9bXGsGxm6ujPtw,25793
8
8
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
9
9
  sourcecode/classifier.py,sha256=YTTCoRdcLEFRVcql9Ow1dE7eYQj0jq2rgx32bRDnb1k,13852
10
- sourcecode/cli.py,sha256=VqAiGRauTO-s8yBJh2JMldha3VKx0jTXC0tOvRpWQkI,283297
10
+ sourcecode/cli.py,sha256=ZWl9QHCe8kCt5bBWtkyZayy78p9dR8cXW4H-W0j76M8,286811
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
@@ -20,7 +20,7 @@ sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24
20
20
  sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
21
21
  sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
22
22
  sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
23
- sourcecode/explain.py,sha256=GbcruAyzlmseV3o2rjeyxGQxToCfSYHJjKG3N05NVbQ,19897
23
+ sourcecode/explain.py,sha256=2Og-r74yUm38wAF6qD28Gt0jxi1Ygdx33hwp6-NH0xs,20348
24
24
  sourcecode/file_chunker.py,sha256=3vkM3mDQ5eE_yTPvUgjyjpGFBIjkW6_mrBmIbrylnA8,16444
25
25
  sourcecode/file_classifier.py,sha256=A0fEABqtfVu1MfoaxnPAvGpZgneGgVXlJDhT74NYXxE,15314
26
26
  sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,28781
@@ -29,12 +29,12 @@ 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=h0leIhlWvSjYq3F99LxvLIDLrJ-xPYxWAREG4LkqZ-4,61190
32
- sourcecode/integration_detector.py,sha256=HlVdiVki7i62t3pJ0lRjTQQ7WZqPDcPLTqBGAfjFjVQ,17883
32
+ sourcecode/integration_detector.py,sha256=69TGGtBCzWz0TpjyVhnOh58pFMzSIeqbs_gVQhm98Yk,18622
33
33
  sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
34
34
  sourcecode/license.py,sha256=wckiLuiwaE35KMCStUf1gYzleJuFe6qsSyxUQJvit3s,23500
35
35
  sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
36
36
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
37
- sourcecode/migrate_check.py,sha256=VyA-2n5J1Cqf1XxhQ143pf5Y7IvGwolMM1F4OCLOOaU,102628
37
+ sourcecode/migrate_check.py,sha256=xWxI7qovIslRRw0lhf1qsKyMKbMHFV8XsWUEzfENh38,105138
38
38
  sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
39
39
  sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
40
40
  sourcecode/path_filters.py,sha256=VnaD9jxZVNzluackNSTCdIddwzAqIseuSCs_A-gpCDM,6898
@@ -47,7 +47,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
47
47
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
48
48
  sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
49
49
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
50
- sourcecode/repository_ir.py,sha256=tj3e_uxkVGTRWfzkf2YoGO-oGakSO4DmWrUQow0dFGk,231201
50
+ sourcecode/repository_ir.py,sha256=iw_TVIpWT204AvTvm60iaDUCSZKDvY7s2OkLODOQPnk,244042
51
51
  sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
52
52
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
53
53
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
@@ -59,9 +59,9 @@ sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC
59
59
  sourcecode/spring_findings.py,sha256=G7Or2lKBUQbcTDqudLvSs9XvNg_YoAa-_lBOG_ULs8E,5457
60
60
  sourcecode/spring_impact.py,sha256=1Eu1vkdwTVsw92iBEJDe_i_FaSf4uGH9Rb0eSgIAAZc,57542
61
61
  sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
62
- sourcecode/spring_security_audit.py,sha256=XtPJ1SXlZJ8k6VYmaWuAp7Bbir4UmreAL7doIGQ5I7o,20595
62
+ sourcecode/spring_security_audit.py,sha256=xrdv3MaR_m2wblA6a8MRWM7attvZeoPXKsoRO_R2lY4,21401
63
63
  sourcecode/spring_semantic.py,sha256=O1nKSGVzlukuxLHQVuCPxc-XrcrMFxwlHA20_dmEGgM,13307
64
- sourcecode/spring_tx_analyzer.py,sha256=FdFcyqPp3aT9oJ-PKrnXcTA6s69wdvzG-NBm0GMGPTU,30717
64
+ sourcecode/spring_tx_analyzer.py,sha256=iH9mK0Tgvhgy_Ee2r13KA_RMf8HKN-pjCKQGuyCCBjM,33812
65
65
  sourcecode/summarizer.py,sha256=hV-kPFXXh15GnIvmLZnmVWTEbmr35C-yKVtPiR25U2g,21802
66
66
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
67
67
  sourcecode/validation_surface.py,sha256=2Ojfzhw9R4XpemAFqb9RXf4FKyI3DYjGo4Dp0AHZMQo,22600
@@ -104,8 +104,8 @@ sourcecode/telemetry/consent.py,sha256=H5z2Wu63pZqbaKucRPoJQJ0zCo4cke9ZlBrJC-MaW
104
104
  sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
105
105
  sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
106
106
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
107
- sourcecode-1.71.0.dist-info/METADATA,sha256=w75cRYv8E561ZW0S1KTAry6BealPGHv20TwuZgW-NGM,48339
108
- sourcecode-1.71.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
109
- sourcecode-1.71.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
110
- sourcecode-1.71.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
111
- sourcecode-1.71.0.dist-info/RECORD,,
107
+ sourcecode-1.72.0.dist-info/METADATA,sha256=U1s6MWAt8ek9ne5Rp7fr8i7TEfeyK_zmz4v51pEg-Eo,48339
108
+ sourcecode-1.72.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
109
+ sourcecode-1.72.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
110
+ sourcecode-1.72.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
111
+ sourcecode-1.72.0.dist-info/RECORD,,