sourcecode 1.31.6__py3-none-any.whl → 1.31.7__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.6"
3
+ __version__ = "1.31.7"
sourcecode/cli.py CHANGED
@@ -2495,6 +2495,10 @@ def _extract_java_endpoints(root: "Path") -> "dict[str, Any]":
2495
2495
 
2496
2496
  endpoints: list[dict] = []
2497
2497
  seen: set[tuple] = set()
2498
+ # Fix 1: inheritance projection — tracks class data so controllers with ONLY
2499
+ # inherited endpoints (no own @RequestMapping methods) are not silently dropped.
2500
+ _class_info: dict[str, dict] = {}
2501
+ _EXTENDS_RE_LOCAL = _re.compile(r'\bextends\s+(\w+)')
2498
2502
 
2499
2503
  from sourcecode.path_filters import is_test_path as _is_test_path
2500
2504
 
@@ -2579,14 +2583,18 @@ def _extract_java_endpoints(root: "Path") -> "dict[str, Any]":
2579
2583
  # Extract class-level base path and locate class body start
2580
2584
  lines = content.splitlines()
2581
2585
 
2582
- # First pass: find class/interface declaration line index
2586
+ # First pass: find class/interface declaration line index and extends clause.
2583
2587
  class_body_start = 0
2588
+ extends_class_name: Optional[str] = None
2584
2589
  for i, line in enumerate(lines):
2585
2590
  stripped_l = line.strip()
2586
2591
  if (not stripped_l.startswith("//") and not stripped_l.startswith("*")
2587
2592
  and ("class " in stripped_l or "interface " in stripped_l)
2588
2593
  and _CLASS_RE.search(stripped_l)):
2589
2594
  class_body_start = i + 1
2595
+ _ext_m = _EXTENDS_RE_LOCAL.search(stripped_l)
2596
+ if _ext_m:
2597
+ extends_class_name = _ext_m.group(1)
2590
2598
  break
2591
2599
 
2592
2600
  # Second pass: extract class-level @RequestMapping path (only before class body).
@@ -2656,6 +2664,7 @@ def _extract_java_endpoints(root: "Path") -> "dict[str, Any]":
2656
2664
  pending_annotations: list[tuple[str, str]] = [] # (http_verb, path_suffix)
2657
2665
  pending_filtro: Optional[str] = None
2658
2666
  in_block_comment = False
2667
+ file_own_endpoints: list[tuple] = [] # (http_verb, path_suffix, handler, filtro)
2659
2668
 
2660
2669
  for i in range(class_body_start, len(lines)):
2661
2670
  line = lines[i]
@@ -2708,6 +2717,7 @@ def _extract_java_endpoints(root: "Path") -> "dict[str, Any]":
2708
2717
  handler = mm.group(1) if mm else ""
2709
2718
  if handler and not handler.startswith("class"):
2710
2719
  for http_verb, path_suffix in pending_annotations:
2720
+ file_own_endpoints.append((http_verb, path_suffix, handler, pending_filtro))
2711
2721
  for _cb in class_bases: # Bug #1B: one endpoint per class prefix
2712
2722
  full_path = (_cb + "/" + path_suffix).replace("//", "/").rstrip("/") or "/"
2713
2723
  if not full_path.startswith("/"):
@@ -2733,6 +2743,50 @@ def _extract_java_endpoints(root: "Path") -> "dict[str, Any]":
2733
2743
  pending_annotations = []
2734
2744
  pending_filtro = None
2735
2745
 
2746
+ # Store per-class data for inheritance projection
2747
+ _class_info[class_name] = {
2748
+ "base_paths": class_bases,
2749
+ "extends_class": extends_class_name,
2750
+ "own_endpoints": file_own_endpoints,
2751
+ }
2752
+
2753
+ # Fix 1: Inheritance projection — controllers whose methods are 100% inherited from a
2754
+ # base class emit 0 endpoints above because no method-level annotations exist in their
2755
+ # file. Walk the inheritance chain and project parent suffixes with the child's base path.
2756
+ for _cls, _data in _class_info.items():
2757
+ if _data["own_endpoints"]:
2758
+ continue
2759
+ _bases = _data["base_paths"]
2760
+ if not _bases or _bases == [""]:
2761
+ continue
2762
+ _chain = _data.get("extends_class")
2763
+ _visited: set[str] = {_cls}
2764
+ while _chain and _chain not in _visited:
2765
+ _visited.add(_chain)
2766
+ _parent = _class_info.get(_chain)
2767
+ if not _parent:
2768
+ break
2769
+ if _parent["own_endpoints"]:
2770
+ for _verb, _suffix, _handler, _filtro in _parent["own_endpoints"]:
2771
+ for _cb in _bases:
2772
+ _fp = (_cb + "/" + _suffix).replace("//", "/").rstrip("/") or "/"
2773
+ if not _fp.startswith("/"):
2774
+ _fp = "/" + _fp
2775
+ _key = (_cls, _handler, _verb, _cb)
2776
+ if _key not in seen:
2777
+ seen.add(_key)
2778
+ _entry: dict[str, Any] = {
2779
+ "method": _verb,
2780
+ "path": _fp,
2781
+ "controller": _cls,
2782
+ "handler": _handler,
2783
+ }
2784
+ if _filtro:
2785
+ _entry["required_permission"] = _filtro
2786
+ endpoints.append(_entry)
2787
+ break
2788
+ _chain = _parent.get("extends_class")
2789
+
2736
2790
  endpoints.sort(key=lambda e: (e.get("controller", ""), e.get("path", "")))
2737
2791
  undocumented = sum(1 for e in endpoints if "required_permission" not in e)
2738
2792
 
@@ -178,8 +178,11 @@ _SPRING_OTHER: frozenset[str] = frozenset({
178
178
  "@PutMapping", "@DeleteMapping", "@PatchMapping", "@Autowired",
179
179
  "@Inject", "@Value", "@Qualifier", "@EnableWebSecurity",
180
180
  "@SpringBootApplication", "@EnableAutoConfiguration",
181
+ "@EventListener", "@Async", "@Scheduled", "@Cacheable", "@CacheEvict",
181
182
  })
182
183
 
184
+ _PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*[(\{]')
185
+
183
186
  _HTTP_METHOD_MAP: dict[str, str] = {
184
187
  "@GetMapping": "GET",
185
188
  "@PostMapping": "POST",
@@ -787,6 +790,33 @@ def _build_relations(
787
790
  evidence={"type": "structural", "value": f"member of {enclosing}"},
788
791
  ))
789
792
 
793
+ # Fix 5: Event flow edges — publishEvent publishers and @EventListener subscribers.
794
+ # listens_to_event: method with @EventListener → resolved event parameter type(s).
795
+ for sym in symbols:
796
+ if sym.type == "method" and "@EventListener" in sym.annotations:
797
+ for imp_fqn in sym.imports_used:
798
+ edges.append(RelationEdge(
799
+ from_symbol=sym.symbol,
800
+ to_symbol=imp_fqn,
801
+ type="listens_to_event",
802
+ confidence="high",
803
+ evidence={"type": "annotation", "value": "@EventListener"},
804
+ ))
805
+
806
+ # publishes_event: class that calls publishEvent(new XxxEvent(...)) → event type FQN.
807
+ _class_syms = [s for s in symbols if s.type in ("class", "interface") and "#" not in s.symbol]
808
+ for m in _PUBLISH_EVENT_RE.finditer(source):
809
+ event_simple = m.group(1)
810
+ event_fqn = import_map.get(event_simple, event_simple)
811
+ for cls_sym in _class_syms:
812
+ edges.append(RelationEdge(
813
+ from_symbol=cls_sym.symbol,
814
+ to_symbol=event_fqn,
815
+ type="publishes_event",
816
+ confidence="medium",
817
+ evidence={"type": "method_call", "value": f"publishEvent(new {event_simple})"},
818
+ ))
819
+
790
820
  seen: set[tuple[str, str, str]] = set()
791
821
  unique: list[RelationEdge] = []
792
822
  for e in edges:
@@ -1190,6 +1220,8 @@ _EDGE_REASON_TEMPLATES: dict[str, str] = {
1190
1220
  "contained_in": "{from_sym} is a member of {to_sym}",
1191
1221
  "annotated_with": "{from_sym} is annotated with {to_sym}",
1192
1222
  "mapped_to": "Route {to_sym} depends on {from_sym}",
1223
+ "publishes_event": "{from_sym} publishes event {to_sym}",
1224
+ "listens_to_event": "{from_sym} listens for event {to_sym}",
1193
1225
  }
1194
1226
 
1195
1227
  # Edge types to exclude from reverse impact traversal (too noisy / non-dependency semantics)
@@ -1526,13 +1558,43 @@ def _assemble(
1526
1558
  },
1527
1559
  "subsystems": subsystems,
1528
1560
  "change_set": change_set_out,
1529
- "route_surface": route_diffs or [],
1561
+ "route_surface": _build_route_surface(sorted_syms, route_diffs),
1530
1562
  "audit": {
1531
1563
  "dropped_fields": dropped_fields,
1532
1564
  },
1533
1565
  }
1534
1566
 
1535
1567
 
1568
+ # ---------------------------------------------------------------------------
1569
+ # Route surface helper (Fix 4)
1570
+ # ---------------------------------------------------------------------------
1571
+
1572
+ def _build_route_surface(symbols: list[SymbolRecord], route_diffs: Optional[list[dict]]) -> list[dict]:
1573
+ """Return route surface: diffs when --since provided, else static snapshot of all endpoints."""
1574
+ if route_diffs:
1575
+ return route_diffs
1576
+ routes: list[dict] = []
1577
+ for sym in symbols:
1578
+ if sym.symbol_kind != "endpoint":
1579
+ continue
1580
+ ann_name = next((a for a in sym.annotations if a in _ENDPOINT_ANNOTATIONS), None)
1581
+ if not ann_name:
1582
+ continue
1583
+ args = sym.annotation_values.get(ann_name, "")
1584
+ path = _parse_route_path(args)
1585
+ method = _parse_route_http_method(ann_name, args)
1586
+ if not path and not method:
1587
+ continue
1588
+ routes.append({
1589
+ "symbol": sym.symbol,
1590
+ "controller": _enclosing_class(sym.symbol),
1591
+ "path": path,
1592
+ "method": method or "GET",
1593
+ "stable_id": sym.stable_id,
1594
+ })
1595
+ return sorted(routes, key=lambda r: (r["controller"], r["path"]))
1596
+
1597
+
1536
1598
  # ---------------------------------------------------------------------------
1537
1599
  # Public API
1538
1600
  # ---------------------------------------------------------------------------
@@ -1675,7 +1737,17 @@ def apply_ir_size_limits(
1675
1737
  "remove --summary-only to restore full graph"
1676
1738
  ),
1677
1739
  }
1678
- out["reverse_graph"] = {}
1740
+ # Fix 3: keep bounded reverse graph instead of wiping it.
1741
+ full_rg: dict = ir.get("reverse_graph") or {}
1742
+ if full_rg:
1743
+ _rg_sorted = sorted(
1744
+ full_rg.items(),
1745
+ key=lambda x: sum(len(v) for v in x[1].values()),
1746
+ reverse=True,
1747
+ )
1748
+ out["reverse_graph"] = dict(_rg_sorted[:50])
1749
+ else:
1750
+ out["reverse_graph"] = {}
1679
1751
  out["impact"] = {
1680
1752
  "global_score": (ir.get("impact") or {}).get("global_score", 0),
1681
1753
  "ranked_nodes": ranked[:20],
@@ -1703,10 +1775,19 @@ def apply_ir_size_limits(
1703
1775
 
1704
1776
  if kept_fqns is not None or max_edges is not None:
1705
1777
  if kept_fqns is not None:
1706
- # Priority: edges where both endpoints are kept nodes
1707
- priority = [e for e in edges if e["from"] in kept_fqns and e["to"] in kept_fqns]
1708
- rest = [e for e in edges if not (e["from"] in kept_fqns and e["to"] in kept_fqns)]
1709
- edges = priority + rest
1778
+ # Fix 2: type-aware priority so semantic edges survive node truncation.
1779
+ # Annotation strings (@Service etc.) and field FQNs are never in kept_fqns,
1780
+ # so "both endpoints kept" drops all injects/annotated_with edges.
1781
+ _SEMANTIC_TYPES = frozenset({"extends", "implements", "injects",
1782
+ "publishes_event", "listens_to_event"})
1783
+ _ANNOTATION_TYPES = frozenset({"annotated_with"})
1784
+ tier1 = [e for e in edges if e["from"] in kept_fqns and e["type"] in _SEMANTIC_TYPES]
1785
+ tier2 = [e for e in edges if e["from"] in kept_fqns and e["type"] in _ANNOTATION_TYPES]
1786
+ tier3 = [e for e in edges
1787
+ if e["from"] in kept_fqns and e["to"] in kept_fqns and e["type"] == "imports"]
1788
+ _seen_e = {(e["from"], e["to"], e["type"]) for e in tier1 + tier2 + tier3}
1789
+ tier4 = [e for e in edges if (e["from"], e["to"], e["type"]) not in _seen_e]
1790
+ edges = tier1 + tier2 + tier3 + tier4
1710
1791
  if max_edges is not None:
1711
1792
  edges = edges[:max_edges]
1712
1793
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.31.6
3
+ Version: 1.31.7
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.6-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.31.7-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.6
264
+ # sourcecode 1.31.7
265
265
  ```
266
266
 
267
267
  ---
@@ -1,10 +1,10 @@
1
- sourcecode/__init__.py,sha256=YtkXxLCwI2P3youz8qWDCC8rLLuveg8_p3Rw5TwvrXs,103
1
+ sourcecode/__init__.py,sha256=8R-nJsTbK4mRxB0Ix8W0xEdhYCBpK8R-2dyS_nBxiDc,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
5
5
  sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
6
6
  sourcecode/classifier.py,sha256=-0t0HLc9L9UleMLfclfLM3AXhBjUb_AYyBPDbvgWtac,7755
7
- sourcecode/cli.py,sha256=lAqlKvLs3nrHKCM6RfsvQr1O_jWi3WOcdjhhlE8fmyg,129082
7
+ sourcecode/cli.py,sha256=L0KDRruMogpw9kq7trZ7dmWQuFC746GIEN1oym6LmCE,131802
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
10
10
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -29,7 +29,7 @@ sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,
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=KH5EehbjOh8ZwwTHcbzrAHiKDoquO49wgSvCX4bVq5k,64391
32
+ sourcecode/repository_ir.py,sha256=fLt33wadCMY77jd34YwkYbUT5TyNu2G0LX0x76Krsvo,68348
33
33
  sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
34
34
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
35
35
  sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
@@ -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.6.dist-info/METADATA,sha256=aKMdH4KYx2KrZ1rx8Ylo73TwfocQ5szGN1U3CDUn_tM,29083
77
- sourcecode-1.31.6.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
78
- sourcecode-1.31.6.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
79
- sourcecode-1.31.6.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
80
- sourcecode-1.31.6.dist-info/RECORD,,
76
+ sourcecode-1.31.7.dist-info/METADATA,sha256=O08_eBOxRxeDd8UkUKyIrW6haJHuQvAddgiFAEma0dQ,29083
77
+ sourcecode-1.31.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
78
+ sourcecode-1.31.7.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
79
+ sourcecode-1.31.7.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
80
+ sourcecode-1.31.7.dist-info/RECORD,,