sourcecode 1.31.9__py3-none-any.whl → 1.31.10__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.31.9"
3
+ __version__ = "1.31.10"
sourcecode/classifier.py CHANGED
@@ -20,6 +20,7 @@ _API_FRAMEWORKS = {
20
20
  "Rocket",
21
21
  "Spring Boot",
22
22
  "Quarkus",
23
+ "Jakarta EE", # JAX-RS / Jakarta REST — pure JAX-RS projects must not fall to "unknown"
23
24
  "Laravel",
24
25
  "Symfony",
25
26
  "Ktor",
@@ -128,8 +128,13 @@ def _infer_role(name: str, ecosystem: str, scope: str) -> str:
128
128
  return "runtime"
129
129
 
130
130
  if ecosystem == "java":
131
+ # Scope is authoritative: provided and dev scopes are not runtime, regardless of
132
+ # artifact name. Checking artifact patterns first would mis-classify spring-boot-test
133
+ # (scope=test) as "runtime", inflating false-positive fan-in signals.
131
134
  if scope == "provided":
132
135
  return "provided"
136
+ if is_dev:
137
+ return "devtool"
133
138
  artifact = n.split(":")[-1] if ":" in n else n
134
139
  if any(x in artifact for x in ("spring-boot", "spring-security")):
135
140
  return "runtime"
@@ -143,7 +148,7 @@ def _infer_role(name: str, ecosystem: str, scope: str) -> str:
143
148
  return "parsing"
144
149
  if any(x in artifact for x in ("jjwt", "nimbus-jose")):
145
150
  return "runtime"
146
- return "devtool" if is_dev else "runtime"
151
+ return "runtime"
147
152
 
148
153
  return "devtool" if is_dev else "runtime"
149
154
 
@@ -34,13 +34,16 @@ _HTTP_PATH_RE = re.compile(
34
34
  _REQUEST_METHOD_VERB_RE = re.compile(
35
35
  r'method\s*=\s*RequestMethod\.([A-Z]+)'
36
36
  )
37
- # @M3FiltroSeguridad custom security annotation
38
- _M3_FILTRO_RE = re.compile(r'@M3FiltroSeguridad\b')
39
- _M3_FILTRO_PARAMS_RE = re.compile(
40
- r'@M3FiltroSeguridad\s*\(\s*'
41
- r'(?:nombreRecurso\s*=\s*(?:"([^"]*)"|([\w.]+)))?' # group 1: string literal, group 2: constant ref
42
- r'(?:[^)]*nivelRequerido\s*=\s*(\d+))?' # group 3: nivel
43
- )
37
+ # Custom security annotation registry — extend here for project-specific annotations.
38
+ # Each entry: annotation_simple_name → compiled params regex.
39
+ # Groups: (1) resource string literal, (2) resource constant ref, (3) level integer.
40
+ _CUSTOM_SECURITY_ANNOTATIONS: dict[str, re.Pattern] = {
41
+ "M3FiltroSeguridad": re.compile(
42
+ r'@M3FiltroSeguridad\s*\(\s*'
43
+ r'(?:nombreRecurso\s*=\s*(?:"([^"]*)"|([\w.]+)))?'
44
+ r'(?:[^)]*nivelRequerido\s*=\s*(\d+))?'
45
+ ),
46
+ }
44
47
 
45
48
  # Security config detection
46
49
  _WEB_SECURITY_CONFIGURER_RE = re.compile(r'WebSecurityConfigurerAdapter\b')
@@ -436,7 +439,7 @@ class JavaDetector(AbstractDetector):
436
439
  ))
437
440
  if (not has_controller and not has_filter and not has_security
438
441
  and "ControllerAdvice" not in content
439
- and "M3FiltroSeguridad" not in content
442
+ and not any(ann in content for ann in _CUSTOM_SECURITY_ANNOTATIONS)
440
443
  and not has_jax_rs and not has_cdi and not has_spi):
441
444
  return []
442
445
 
@@ -449,11 +452,13 @@ class JavaDetector(AbstractDetector):
449
452
  elif verb_match:
450
453
  http_path = f"[{verb_match.group(1)}]"
451
454
  security_evidence = None
452
- m3_match = _M3_FILTRO_PARAMS_RE.search(content)
453
- if m3_match:
454
- nombre = m3_match.group(1) or m3_match.group(2) or ""
455
- nivel = m3_match.group(3) or ""
456
- security_evidence = f"@M3FiltroSeguridad(nombreRecurso={nombre!r}, nivelRequerido={nivel})"
455
+ for _ann_name, _ann_re in _CUSTOM_SECURITY_ANNOTATIONS.items():
456
+ _m = _ann_re.search(content)
457
+ if _m:
458
+ nombre = _m.group(1) or _m.group(2) or ""
459
+ nivel = _m.group(3) or ""
460
+ security_evidence = f"@{_ann_name}(nombreRecurso={nombre!r}, nivelRequerido={nivel})"
461
+ break
457
462
  return [EntryPoint(
458
463
  path=rel_path, stack="java", kind="rest_controller",
459
464
  source="annotation", confidence="high",
@@ -474,11 +479,13 @@ class JavaDetector(AbstractDetector):
474
479
  elif verb_match:
475
480
  http_path = f"[{verb_match.group(1)}]"
476
481
  security_evidence = None
477
- m3_match = _M3_FILTRO_PARAMS_RE.search(content)
478
- if m3_match:
479
- nombre = m3_match.group(1) or m3_match.group(2) or ""
480
- nivel = m3_match.group(3) or ""
481
- security_evidence = f"@M3FiltroSeguridad(nombreRecurso={nombre!r}, nivelRequerido={nivel})"
482
+ for _ann_name, _ann_re in _CUSTOM_SECURITY_ANNOTATIONS.items():
483
+ _m = _ann_re.search(content)
484
+ if _m:
485
+ nombre = _m.group(1) or _m.group(2) or ""
486
+ nivel = _m.group(3) or ""
487
+ security_evidence = f"@{_ann_name}(nombreRecurso={nombre!r}, nivelRequerido={nivel})"
488
+ break
482
489
  return [EntryPoint(
483
490
  path=rel_path, stack="java", kind="mvc_controller",
484
491
  source="annotation", confidence="medium",
@@ -514,7 +521,10 @@ class JavaDetector(AbstractDetector):
514
521
  )]
515
522
 
516
523
  # --- JAX-RS resource class ---
517
- if has_jax_rs and _JAX_RS_PATH_RE.search(content) and _JAX_RS_VERB_RE.search(content):
524
+ # Guard uses annotation PRESENCE ("@Path" in content), not _JAX_RS_PATH_RE, because
525
+ # the value-parsing regex fails for @Path(CONSTANT) and @Path(PREFIX + "/sub").
526
+ # _JAX_RS_PATH_RE is still used to extract the http_path display string when parseable.
527
+ if has_jax_rs and "@Path" in content and _JAX_RS_VERB_RE.search(content):
518
528
  path_m = _JAX_RS_PATH_RE.search(content)
519
529
  verb_m = _JAX_RS_VERB_RE.search(content)
520
530
  http_path: str | None = None
@@ -2103,8 +2103,15 @@ class TaskContextBuilder:
2103
2103
  _uncommitted = uncommitted_files or set()
2104
2104
  _max_churn = max(_hotspots.values(), default=1)
2105
2105
 
2106
+ # Per-file note counts — feeds code_note_count into RankingEngine for all tasks.
2107
+ # RankingEngine is the sole ranking source; no ad-hoc annotation boost outside it.
2108
+ _note_counts: dict[str, int] = {}
2109
+ for _n in (code_notes or []):
2110
+ _np = getattr(_n, "path", "")
2111
+ if _np:
2112
+ _note_counts[_np] = _note_counts.get(_np, 0) + 1
2113
+
2106
2114
  # Pre-compute fix-bug signals (used only when task_name == "fix-bug")
2107
- _annotated_files: set[str] = set()
2108
2115
  _dominant_stack = ""
2109
2116
  _recently_changed_stacks: set[str] = set()
2110
2117
  # Query-aware signals extracted from symptom (class names, exception types, tokens)
@@ -2113,10 +2120,6 @@ class TaskContextBuilder:
2113
2120
  _symptom_tokens: set[str] = set() # all lowercase tokens
2114
2121
 
2115
2122
  if task_name == "fix-bug":
2116
- _bug_kinds = {"FIXME", "BUG", "HACK", "XXX"}
2117
- for _n in (code_notes or []):
2118
- if getattr(_n, "kind", "").upper() in _bug_kinds:
2119
- _annotated_files.add(getattr(_n, "path", ""))
2120
2123
 
2121
2124
  def _file_stack(p: str) -> str:
2122
2125
  ext = Path(p).suffix.lower()
@@ -2167,13 +2170,15 @@ class TaskContextBuilder:
2167
2170
  if is_test and task_name != "generate-tests":
2168
2171
  continue
2169
2172
 
2170
- # Structural + git signals from unified engine (task-weighted)
2173
+ # Structural + git signals from unified engine (task-weighted).
2174
+ # code_note_count routes annotation density through RankingEngine — single source.
2171
2175
  fs = engine.score(
2172
2176
  path,
2173
2177
  is_entrypoint=(path in runtime_entry_set),
2174
2178
  git_churn=_hotspots.get(path, 0),
2175
2179
  max_churn=_max_churn,
2176
2180
  is_changed=(path in _uncommitted),
2181
+ code_note_count=_note_counts.get(path, 0),
2177
2182
  task=task_name,
2178
2183
  )
2179
2184
 
@@ -2264,9 +2269,6 @@ class TaskContextBuilder:
2264
2269
  if _recency > 0:
2265
2270
  content_boost += _recency
2266
2271
  _why_parts.append(f"recent commits (+{_recency:.2f})")
2267
- if path in _annotated_files:
2268
- content_boost += 0.20
2269
- _why_parts.append("FIXME/BUG annotation (+0.20)")
2270
2272
  _file_stk = _file_stack(path)
2271
2273
  if _dominant_stack and _file_stk == _dominant_stack:
2272
2274
  content_boost += 0.10
@@ -225,6 +225,14 @@ _SPRING_OTHER: frozenset[str] = frozenset({
225
225
 
226
226
  _PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*[(\{]')
227
227
 
228
+ # Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
229
+ _FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
230
+
231
+ # Edge types used for subsystem grouping — semantic hierarchy only, not imports
232
+ _SUBSYSTEM_STRUCTURAL_EDGES: frozenset[str] = frozenset({
233
+ "extends", "implements", "injects", "contained_in",
234
+ })
235
+
228
236
  _HTTP_METHOD_MAP: dict[str, str] = {
229
237
  # Spring MVC
230
238
  "@GetMapping": "GET",
@@ -859,8 +867,8 @@ def _build_relations(
859
867
  evidence={"type": "structural", "value": f"member of {enclosing}"},
860
868
  ))
861
869
 
862
- # Fix 5: Event flow edges — publishEvent publishers and @EventListener subscribers.
863
- # listens_to_event: method with @EventListener → resolved event parameter type(s).
870
+ # Event flow edges — listens_to_event and publishes_event.
871
+ # Spring: method with @EventListener → resolved event parameter type(s).
864
872
  for sym in symbols:
865
873
  if sym.type == "method" and "@EventListener" in sym.annotations:
866
874
  for imp_fqn in sym.imports_used:
@@ -872,8 +880,9 @@ def _build_relations(
872
880
  evidence={"type": "annotation", "value": "@EventListener"},
873
881
  ))
874
882
 
875
- # publishes_event: class that calls publishEvent(new XxxEvent(...)) → event type FQN.
876
883
  _class_syms = [s for s in symbols if s.type in ("class", "interface") and "#" not in s.symbol]
884
+
885
+ # Spring: class that calls publishEvent(new XxxEvent(...)) → event type FQN.
877
886
  for m in _PUBLISH_EVENT_RE.finditer(source):
878
887
  event_simple = m.group(1)
879
888
  event_fqn = import_map.get(event_simple, event_simple)
@@ -886,6 +895,32 @@ def _build_relations(
886
895
  evidence={"type": "method_call", "value": f"publishEvent(new {event_simple})"},
887
896
  ))
888
897
 
898
+ # Keycloak SPI: XxxEvent.fire(...) static dispatch → publishes_event.
899
+ for m in _FIRE_EVENT_RE.finditer(source):
900
+ event_simple = m.group(1)
901
+ event_fqn = import_map.get(event_simple, event_simple)
902
+ for cls_sym in _class_syms:
903
+ edges.append(RelationEdge(
904
+ from_symbol=cls_sym.symbol,
905
+ to_symbol=event_fqn,
906
+ type="publishes_event",
907
+ confidence="medium",
908
+ evidence={"type": "method_call", "value": f"{event_simple}.fire(...)"},
909
+ ))
910
+
911
+ # Keycloak SPI: class implementing EventListenerProvider → listens_to_event.
912
+ _ELP_IFACE = "EventListenerProvider"
913
+ for sym in symbols:
914
+ if sym.type == "class" and _ELP_IFACE in (sym.signature or ""):
915
+ event_fqn = import_map.get("Event", "org.keycloak.events.Event")
916
+ edges.append(RelationEdge(
917
+ from_symbol=sym.symbol,
918
+ to_symbol=event_fqn,
919
+ type="listens_to_event",
920
+ confidence="high",
921
+ evidence={"type": "signature", "value": f"implements {_ELP_IFACE}"},
922
+ ))
923
+
889
924
  seen: set[tuple[str, str, str]] = set()
890
925
  unique: list[RelationEdge] = []
891
926
  for e in edges:
@@ -1248,49 +1283,72 @@ def _common_package_prefix(fqns: list[str]) -> str:
1248
1283
 
1249
1284
 
1250
1285
  def _subsystem_label(package_prefix: str) -> str:
1251
- """Derive short human label from package prefix (last meaningful segment)."""
1286
+ """Derive short human label enforcing minimum meaningful depth.
1287
+
1288
+ For org.keycloak.services → "keycloak.services" (not "services" alone).
1289
+ Avoids single-segment labels like "org" or "keycloak" that convey nothing.
1290
+ """
1252
1291
  parts = [p for p in package_prefix.split(".") if p]
1253
- # Skip generic top-level segments
1254
1292
  _SKIP = {"com", "org", "net", "io", "java", "javax"}
1255
1293
  meaningful = [p for p in parts if p not in _SKIP]
1256
- return meaningful[-1] if meaningful else (parts[-1] if parts else package_prefix)
1257
-
1258
-
1259
- def _detect_subsystems(all_fqns: list[str], relations: list[RelationEdge]) -> list[dict]:
1260
- """Connected components of the relation graph (Union-Find, graph-only).
1261
-
1262
- Returns labeled subsystem objects instead of raw FQN arrays.
1294
+ if not meaningful:
1295
+ return parts[-1] if parts else package_prefix
1296
+ # Use last two meaningful segments for disambiguation:
1297
+ # org.keycloak.services ["keycloak", "services"] "keycloak.services"
1298
+ if len(meaningful) >= 2:
1299
+ return f"{meaningful[-2]}.{meaningful[-1]}"
1300
+ return meaningful[-1]
1301
+
1302
+
1303
+ def _canonical_subsystem_pkg(fqn: str) -> str:
1304
+ """Canonical subsystem package for a FQN — minimum depth 3 for org/com/net/io packages.
1305
+
1306
+ org.keycloak.services.FooResource → org.keycloak.services
1307
+ org.keycloak.FooClass → org.keycloak
1308
+ com.example.util.Helper → com.example.util
1309
+ SomeTopLevelClass → SomeTopLevelClass
1310
+
1311
+ Never returns a bare TLD ("org", "com") — that collapses all classes into one subsystem.
1312
+ When only 1 lowercase segment exists before the class boundary, grabs one raw segment
1313
+ (even if uppercase) to force at least 2-segment grouping.
1314
+ """
1315
+ _TOP_LEVEL = {"com", "org", "net", "io", "java", "javax"}
1316
+ parts: list[str] = []
1317
+ for segment in fqn.split("."):
1318
+ if "#" in segment or (segment and segment[0].isupper()):
1319
+ break
1320
+ parts.append(segment)
1321
+ if not parts:
1322
+ return fqn.rsplit(".", 1)[0] if "." in fqn else fqn
1323
+ if parts[0] in _TOP_LEVEL and len(parts) >= 3:
1324
+ return ".".join(parts[:3])
1325
+ # Prevent bare TLD collapse: "org" or "com" alone as subsystem key is meaningless
1326
+ # and groups ALL classes under that TLD into a single giant component.
1327
+ # When only the TLD was collected before hitting a class boundary, grab the next
1328
+ # raw FQN segment (may be uppercase) to produce a 2-segment grouping key.
1329
+ if parts[0] in _TOP_LEVEL and len(parts) == 1:
1330
+ raw = fqn.split(".")
1331
+ if len(raw) >= 2:
1332
+ return f"{raw[0]}.{raw[1].split('#')[0]}"
1333
+ return ".".join(parts)
1334
+
1335
+
1336
+ def _detect_subsystems(all_fqns: list[str], relations: list[RelationEdge]) -> list[dict]: # noqa: ARG001
1337
+ """Group symbols by canonical subsystem package (minimum depth org.keycloak.<module>).
1338
+
1339
+ Uses package-prefix grouping instead of Union-Find on relation edges.
1340
+ Union-Find on imports produced one giant component ("org") for large monorepos.
1341
+ Package-prefix grouping at depth 3 yields meaningful module-level subsystems.
1263
1342
  """
1264
- fqn_set = set(all_fqns)
1265
- parent: dict[str, str] = {fqn: fqn for fqn in all_fqns}
1266
-
1267
- def find(x: str) -> str:
1268
- while parent[x] != x:
1269
- parent[x] = parent[parent[x]]
1270
- x = parent[x]
1271
- return x
1272
-
1273
- def union(x: str, y: str) -> None:
1274
- rx, ry = find(x), find(y)
1275
- if rx != ry:
1276
- parent[rx] = ry
1277
-
1278
- for edge in relations:
1279
- f, t = edge.from_symbol, edge.to_symbol
1280
- if f in fqn_set and t in fqn_set:
1281
- union(f, t)
1282
-
1283
1343
  components: dict[str, list[str]] = {}
1284
1344
  for fqn in all_fqns:
1285
- root = find(fqn)
1286
- components.setdefault(root, []).append(fqn)
1345
+ pkg = _canonical_subsystem_pkg(fqn)
1346
+ components.setdefault(pkg, []).append(fqn)
1287
1347
 
1288
1348
  result: list[dict] = []
1289
- for members in sorted(components.values()):
1349
+ for pkg_prefix, members in sorted(components.items()):
1290
1350
  members = sorted(members)
1291
- pkg_prefix = _common_package_prefix(members)
1292
1351
  label = _subsystem_label(pkg_prefix)
1293
- # Summary: first 3 short class names + total count
1294
1352
  short_names = [m.split(".")[-1].split("#")[0] for m in members[:3]]
1295
1353
  summary = ", ".join(short_names)
1296
1354
  if len(members) > 3:
@@ -1409,6 +1467,56 @@ def _bfs_impact_with_paths(
1409
1467
  # Phase 5 — Assembly: single output contract
1410
1468
  # ---------------------------------------------------------------------------
1411
1469
 
1470
+ def _compute_analysis_gaps(
1471
+ symbols: list[SymbolRecord],
1472
+ spring_summary: dict,
1473
+ route_surface: list[dict],
1474
+ relations: list[RelationEdge],
1475
+ ) -> list[dict]:
1476
+ """Compute structural analysis gaps — real system failures, not cosmetic issues."""
1477
+ gaps: list[dict] = []
1478
+
1479
+ if not symbols:
1480
+ gaps.append({
1481
+ "area": "symbol_extraction",
1482
+ "reason": "No Java symbols extracted — check path or file access",
1483
+ "impact": "high",
1484
+ })
1485
+ return gaps
1486
+
1487
+ controllers = spring_summary.get("controllers", [])
1488
+ if controllers and not route_surface:
1489
+ gaps.append({
1490
+ "area": "route_surface",
1491
+ "reason": (
1492
+ f"{len(controllers)} controller(s) detected but route_surface is empty — "
1493
+ "JAX-RS @Path or Spring @RequestMapping annotations may be missing"
1494
+ ),
1495
+ "impact": "high",
1496
+ })
1497
+
1498
+ # Detect EventListenerProvider implementations via class signature, not class name.
1499
+ # A class named "CustomProcessor" implementing EventListenerProvider would be missed
1500
+ # by a class-name check but is correctly found via its implements clause in signature.
1501
+ _ELP_IFACE = "EventListenerProvider"
1502
+ elp_impls = [
1503
+ sym.symbol for sym in symbols
1504
+ if sym.type == "class" and _ELP_IFACE in (sym.signature or "")
1505
+ ]
1506
+ event_edges = [e for e in relations if e.type in ("listens_to_event", "publishes_event")]
1507
+ if elp_impls and not event_edges:
1508
+ gaps.append({
1509
+ "area": "event_flow",
1510
+ "reason": (
1511
+ f"{len(elp_impls)} EventListenerProvider implementation(s) found but "
1512
+ "no event flow edges detected"
1513
+ ),
1514
+ "impact": "medium",
1515
+ })
1516
+
1517
+ return gaps
1518
+
1519
+
1412
1520
  def _edge_to_dict(edge: RelationEdge) -> dict:
1413
1521
  return {
1414
1522
  "from": edge.from_symbol,
@@ -1629,19 +1737,18 @@ def _assemble(
1629
1737
  by_type.setdefault(e.type, []).append(e.from_symbol)
1630
1738
  reverse_graph_out[target] = by_type
1631
1739
 
1632
- # IC-005: aggregate event flow edges already built in _build_relations
1740
+ # IC-005: aggregate event flow edges already built in _build_relations.
1741
+ # Always emit spring_events (even when empty) so callers don't need key-presence checks.
1633
1742
  _listen_edges = [e for e in sorted_rels if e.type == "listens_to_event"]
1634
1743
  _publish_edges = [e for e in sorted_rels if e.type == "publishes_event"]
1635
- _spring_events: Optional[dict] = None
1636
- if _listen_edges or _publish_edges:
1637
- _spring_events = {
1638
- "listeners": sorted({e.from_symbol for e in _listen_edges}),
1639
- "publishers": sorted({e.from_symbol for e in _publish_edges}),
1640
- "event_types": sorted({e.to_symbol for e in _listen_edges + _publish_edges}),
1641
- "flow_count": len(_listen_edges) + len(_publish_edges),
1642
- }
1744
+ _spring_events: dict = {
1745
+ "listeners": sorted({e.from_symbol for e in _listen_edges}),
1746
+ "publishers": sorted({e.from_symbol for e in _publish_edges}),
1747
+ "event_types": sorted({e.to_symbol for e in _listen_edges + _publish_edges}),
1748
+ "flow_count": len(_listen_edges) + len(_publish_edges),
1749
+ }
1643
1750
 
1644
- return {
1751
+ _base = {
1645
1752
  "schema_version": "final-v1",
1646
1753
  "graph": {
1647
1754
  "nodes": graph_nodes,
@@ -1662,11 +1769,19 @@ def _assemble(
1662
1769
  },
1663
1770
  "subsystems": subsystems,
1664
1771
  "change_set": change_set_out,
1665
- "route_surface": _build_route_surface(sorted_syms, route_diffs, extends_map={
1666
- e.from_symbol: e.to_symbol.split(".")[-1]
1667
- for e in sorted_rels if e.type == "extends"
1668
- }),
1669
- **({"spring_events": _spring_events} if _spring_events else {}),
1772
+ }
1773
+
1774
+ _route_surface = _build_route_surface(sorted_syms, route_diffs, extends_map={
1775
+ e.from_symbol: e.to_symbol.split(".")[-1]
1776
+ for e in sorted_rels if e.type == "extends"
1777
+ })
1778
+ _analysis_gaps = _compute_analysis_gaps(sorted_syms, spring_summary, _route_surface, sorted_rels)
1779
+
1780
+ return {
1781
+ **_base,
1782
+ "route_surface": _route_surface,
1783
+ "spring_events": _spring_events,
1784
+ "analysis_gaps": _analysis_gaps,
1670
1785
  "audit": {
1671
1786
  "dropped_fields": dropped_fields,
1672
1787
  },
@@ -1713,7 +1828,12 @@ def _build_route_surface(
1713
1828
  # JAX-RS: class-level @Path is the resource prefix.
1714
1829
  args = sym.annotation_values.get("@Path", "")
1715
1830
  prefixes = _parse_route_paths(args) if args else [""]
1716
- class_info[simple] = {"fqn": sym.symbol, "prefixes": prefixes, "own_endpoints": []}
1831
+ class_info[simple] = {
1832
+ "fqn": sym.symbol,
1833
+ "prefixes": prefixes,
1834
+ "own_endpoints": [],
1835
+ "has_path_ann": "@Path" in sym.annotations or "@RequestMapping" in sym.annotations,
1836
+ }
1717
1837
 
1718
1838
  routes: list[dict] = []
1719
1839
  seen: set[tuple] = set()
@@ -1732,6 +1852,15 @@ def _build_route_surface(
1732
1852
  # JAX-RS: HTTP verb annotations carry no path; path lives in @Path on the method.
1733
1853
  if ann_name in _JAXRS_HTTP_ANNOTATIONS:
1734
1854
  suffix = _parse_route_path(sym.annotation_values.get("@Path", ""))
1855
+ # Skip JAX-RS routes with no server-side path binding — client proxy interfaces
1856
+ # have HTTP verb annotations but neither a class-level @Path nor a method @Path.
1857
+ # Use has_path_ann (annotation presence) not just prefix value: @Path args may be
1858
+ # unparsed (multi-line, constant expression) yet the class IS a server resource.
1859
+ _cls_entry = class_info.get(cls_simple, {})
1860
+ cls_prefixes = _cls_entry.get("prefixes", [""])
1861
+ cls_has_path = _cls_entry.get("has_path_ann", False)
1862
+ if not suffix and not cls_has_path and all(not p for p in cls_prefixes):
1863
+ continue
1735
1864
  else:
1736
1865
  suffix = _parse_route_path(args)
1737
1866
  method = _parse_route_http_method(ann_name, args) or "GET"
@@ -54,10 +54,18 @@ _MAPPER_IFACE_RE = re.compile(
54
54
  _EXTENDS_RE = re.compile(
55
55
  r'(?:class|interface)\s+([A-Z][A-Za-z0-9_]*)\s+extends\s+([A-Z][A-Za-z0-9_]*)'
56
56
  )
57
- _M3_FILTRO_METHOD_RE = re.compile(
58
- r'@M3FiltroSeguridad(?:\([^)]*\))?\s+(?:@[^\s]+\s+)*'
59
- r'(?:public|private|protected)\s+\w[\w<>\[\]]*\s+([a-z][A-Za-z0-9_]*)\s*\('
60
- )
57
+ # Custom AOP annotation registry — extend here for project-specific security/AOP annotations.
58
+ # Each entry: (method_regex, impl_symbol_name).
59
+ # method_regex must capture the annotated method name in group 1.
60
+ _CUSTOM_AOP_ANNOTATIONS: list[tuple[re.Pattern, str]] = [
61
+ (
62
+ re.compile(
63
+ r'@M3FiltroSeguridad(?:\([^)]*\))?\s+(?:@[^\s]+\s+)*'
64
+ r'(?:public|private|protected)\s+\w[\w<>\[\]]*\s+([a-z][A-Za-z0-9_]*)\s*\('
65
+ ),
66
+ "M3FiltroSeguridadImpl",
67
+ ),
68
+ ]
61
69
  _LOMBOK_CLASS_RE = re.compile(
62
70
  r'(@(?:Data|Slf4j|Builder|AllArgsConstructor|NoArgsConstructor)(?:\([^)]*\))?\s+)*'
63
71
  r'(?:public\s+)?(?:class|interface)\s+([A-Z][A-Za-z0-9_]*)',
@@ -1061,19 +1069,20 @@ class SemanticAnalyzer:
1061
1069
  method="heuristic",
1062
1070
  ))
1063
1071
 
1064
- # P3-C: @M3FiltroSeguridad AOP proxy edges
1065
- for m in _M3_FILTRO_METHOD_RE.finditer(content):
1066
- method_name = m.group(1)
1067
- line = content[: m.start()].count("\n") + 1
1068
- calls.append(CallRecord(
1069
- caller_path=rel_path,
1070
- caller_symbol="M3FiltroSeguridadImpl",
1071
- callee_path=rel_path,
1072
- callee_symbol=method_name,
1073
- call_line=line,
1074
- confidence="medium",
1075
- method="heuristic",
1076
- ))
1072
+ # P3-C: Custom AOP annotation proxy edges — driven by _CUSTOM_AOP_ANNOTATIONS registry
1073
+ for _aop_re, _aop_impl in _CUSTOM_AOP_ANNOTATIONS:
1074
+ for m in _aop_re.finditer(content):
1075
+ method_name = m.group(1)
1076
+ line = content[: m.start()].count("\n") + 1
1077
+ calls.append(CallRecord(
1078
+ caller_path=rel_path,
1079
+ caller_symbol=_aop_impl,
1080
+ callee_path=rel_path,
1081
+ callee_symbol=method_name,
1082
+ call_line=line,
1083
+ confidence="medium",
1084
+ method="heuristic",
1085
+ ))
1077
1086
 
1078
1087
  return symbols, calls
1079
1088
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.31.9
3
+ Version: 1.31.10
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -225,7 +225,7 @@ Description-Content-Type: text/markdown
225
225
 
226
226
  **Deterministic, behavior-aware codebase context for AI agents and PR review.**
227
227
 
228
- ![Version](https://img.shields.io/badge/version-1.31.9-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.31.10-blue)
229
229
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
230
230
 
231
231
  ---
@@ -261,7 +261,7 @@ pipx install sourcecode
261
261
 
262
262
  ```bash
263
263
  sourcecode version
264
- # sourcecode 1.31.9
264
+ # sourcecode 1.31.10
265
265
  ```
266
266
 
267
267
  ---
@@ -1,9 +1,9 @@
1
- sourcecode/__init__.py,sha256=D2q8qSBHEQRMIJjcRpX9FbmycgT8mZEoRXNfT0nXRg8,103
1
+ sourcecode/__init__.py,sha256=UgQagLVaAryTapc12dFAKQNuEBR47pY5os4ivkcnviY,104
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=4R13Yb02OrPeB4IH3z6V_g7HWhmGcRHbI8CobCVnRrc,39111
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
5
5
  sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
6
- sourcecode/classifier.py,sha256=-0t0HLc9L9UleMLfclfLM3AXhBjUb_AYyBPDbvgWtac,7755
6
+ sourcecode/classifier.py,sha256=yWeq6agTjkFa3zuNa-gdVIHtjoBoPoVlJnX-b7tdVJs,7851
7
7
  sourcecode/cli.py,sha256=E8aw0iW64z1hckFZ1ekNPsWqKF6Q-motqakbuZa2r1Q,113130
8
8
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
9
9
  sourcecode/confidence_analyzer.py,sha256=ZUn-Nywi5TEQcuozqK_vfOyPT-a1dYYO42elAtVFV-k,16412
@@ -12,7 +12,7 @@ sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0
12
12
  sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
13
13
  sourcecode/contract_pipeline.py,sha256=w18t_MdbrkIeLcCW-VMQYeb9hlWenAOB-NMiME_Fo-Y,27652
14
14
  sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
15
- sourcecode/dependency_analyzer.py,sha256=p4ljXhkcGBbFlhaZuPrsjOVjDXaKLTg0Gor2p4qFPP0,56208
15
+ sourcecode/dependency_analyzer.py,sha256=i3o8pRkC4ABQkb__PsQrCbPwupkbkcztzOjfgVl6HFY,56492
16
16
  sourcecode/doc_analyzer.py,sha256=afA4uJFwXZ_uR2l4J0pQwbeTkRkGmKdN9KhRVYePBUw,24331
17
17
  sourcecode/entrypoint_classifier.py,sha256=MTa7yqbeuJ9XPbGCPuvtR9IqY-SN3hoXXyVtb3iXDhs,4316
18
18
  sourcecode/env_analyzer.py,sha256=GxCidahAAIptTdDFIlVB6URd4HBnBlIX_SqUov3MBRQ,22076
@@ -23,17 +23,17 @@ sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,
23
23
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
24
24
  sourcecode/path_filters.py,sha256=ROFRQ8eSLBEMiixK9f45-RO7um4VEEcjoD5AA4I427I,3739
25
25
  sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
26
- sourcecode/prepare_context.py,sha256=_bIelOcv1vdyffyv38NI_Acu7-YxWLA7o6kgaiasCbY,191161
26
+ sourcecode/prepare_context.py,sha256=EKSH6OvbNlfEO3221rZBvSViRTDgelG08tJOePgkm0U,191277
27
27
  sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
28
28
  sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
29
29
  sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
30
30
  sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
31
31
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
32
- sourcecode/repository_ir.py,sha256=GkqZZx7o0E-L3WeHF7v1JmTp3LmwEoAqNO3gHycOCOw,80923
32
+ sourcecode/repository_ir.py,sha256=N0y33Rn1VYpyL-nWZ3eWed9lrzYGJgQF09Wz3eASf3U,86644
33
33
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
34
34
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
35
35
  sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
36
- sourcecode/semantic_analyzer.py,sha256=12TwXYkYbDcBdu0heX_EmfPM2EkO8a_r5osf0SaeQbs,88956
36
+ sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
37
37
  sourcecode/serializer.py,sha256=upGg24ALEk9almaZeqyiGdritdd4F1bvR2-jr_dv3RM,114144
38
38
  sourcecode/summarizer.py,sha256=lPlKhMh28nueXkPo2xKeD3DUFYVGRlJMIdY-8TSM-ls,17486
39
39
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
@@ -47,7 +47,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
47
47
  sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
48
48
  sourcecode/detectors/heuristic.py,sha256=7cRxrip4yIaggYzZJB6ef8yHKh-gHgiH_pXMFcjlyFU,3723
49
49
  sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
50
- sourcecode/detectors/java.py,sha256=Eku9Klrx1ZcqcrkBlBNujcmDNG7yHYhIZ8MzYYOSmlE,28395
50
+ sourcecode/detectors/java.py,sha256=YmmbRChVt_E4yTLfz3jzdNtbCtyQGS4RD7Xt95_rtdg,28979
51
51
  sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
52
52
  sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
53
53
  sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
@@ -73,8 +73,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
73
73
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
74
74
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
75
75
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
76
- sourcecode-1.31.9.dist-info/METADATA,sha256=IgnbA0RZn8YJ9wvReDafhhJlXYFEynArpjOe14e_DwI,29083
77
- sourcecode-1.31.9.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
78
- sourcecode-1.31.9.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
79
- sourcecode-1.31.9.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
80
- sourcecode-1.31.9.dist-info/RECORD,,
76
+ sourcecode-1.31.10.dist-info/METADATA,sha256=2VJA8qS8dXDSt-Vo836ZP8PXognLIM8GZGditRYmfMc,29086
77
+ sourcecode-1.31.10.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
78
+ sourcecode-1.31.10.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
79
+ sourcecode-1.31.10.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
80
+ sourcecode-1.31.10.dist-info/RECORD,,