Skip to content

Commit 37738eb

Browse files
add describe override-axis edge_summary rollup for methods (#110)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0d40ef6 commit 37738eb

11 files changed

Lines changed: 321 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/
263263
|---|---|---|---|
264264
| `search` | Locate nodes by NL/code text. | `query: str`, `table: str="java"`, `hybrid: bool=False`, `limit: int=5`, `offset: int=0`, `path_contains: str \| None`, `filter: NodeFilter \| str \| None` | `{"query":"join operator flow","limit":5}` |
265265
| `find` | Locate nodes by structured filter. | `kind: "symbol"\|"route"\|"client"`, `filter: NodeFilter \| str`, `limit: int=25`, `offset: int=0` | `{"kind":"symbol","filter":{"role":"CONTROLLER"}}` |
266-
| `describe` | Full record + edge counts for one node. For **type** symbols, `edge_summary` may also include composed dot-keys (`DECLARES.DECLARES_CLIENT`, `DECLARES.EXPOSES`); see [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md) (`describe`). | `id: str` | `{"id":"sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest)"}` |
266+
| `describe` | Full record + edge counts for one node. For **type** symbols, `edge_summary` may include composed dot-keys (`DECLARES.DECLARES_CLIENT`, `DECLARES.EXPOSES`); for **method** symbols it may include override-axis virtual keys (`OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_CLIENT`, `OVERRIDDEN_BY.EXPOSES`, `OVERRIDES`). See [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md) (`describe`). | `id: str` | `{"id":"sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest)"}` |
267267
| `neighbors` | One-hop walk. **Required**: `direction` and `edge_types`. | `ids: str \| list[str]`, `direction: "in"\|"out"`, `edge_types: list[str]`, `limit: int=25`, `offset: int=0`, `filter: NodeFilter \| str \| None` | `{"ids":"route:chat-core:POST:/chat/joinOperator","direction":"in","edge_types":["HTTP_CALLS","ASYNC_CALLS"]}` |
268268

269269
**`NodeFilter` notes:**

docs/AGENT-GUIDE.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ Exact allowed values for roles, capabilities, client kinds, etc. live in `java_o
195195

196196
#### `describe`
197197

198-
- **Purpose:** Full node payload + `edge_summary`: `in` / `out` counts **per stored graph edge label** (what exists as edges in Kuzu). For **type** Symbols only (`class`, `interface`, `enum`, `record`, `annotation`), the same map may also include **describe-time composed** dot-keys — summaries of member edges, not stored labels — see the next bullets (`DECLARES.DECLARES_CLIENT`, `DECLARES.EXPOSES`); those keys are **not** valid in `neighbors(edge_types=…)`.
198+
- **Purpose:** Full node payload + `edge_summary`: `in` / `out` counts **per stored graph edge label** (what exists as edges in Kuzu). For **type** Symbols only (`class`, `interface`, `enum`, `record`, `annotation`), the same map may also include **describe-time composed** dot-keys — summaries of member edges, not stored labels — see the next bullets (`DECLARES.DECLARES_CLIENT`, `DECLARES.EXPOSES`); those keys are **not** valid in `neighbors(edge_types=…)`. For **method** Symbols, the map may include **override-axis** virtual keys (`OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_CLIENT`, `OVERRIDDEN_BY.EXPOSES`, `OVERRIDES`); see **Override-axis keys (method Symbols)** below — also not `EdgeType` literals.
199199
- **Args:** `id` (symbol, route, or client id).
200200

201201
**Composed `edge_summary` keys (type Symbols).** Keys use dot notation: `<parent_relation>.<projected_relation>`. Two are emitted today:
@@ -207,6 +207,18 @@ Composed keys are **read-only**: they cannot be passed to `neighbors(edge_types=
207207

208208
Note on counting semantics: composed counts measure **edge rows**, not distinct member methods. One method that declares multiple `Client` rows (e.g. a `rest_template` method with several call sites) contributes its full edge count to `DECLARES.DECLARES_CLIENT`. The "does this class have any clients?" predicate is answered by `count > 0`; the count itself is an affordance for how rich the downstream walk will be.
209209

210+
**Override-axis keys (method Symbols).** These name dispatch-axis virtual relations (computed at describe-time from `IMPLEMENTS` / `EXTENDS` plus matching `Symbol.signature`; not stored edges):
211+
212+
- `OVERRIDDEN_BY` — on declarations reachable from implementing / extending classes in one hop: count of **distinct** concrete override methods with the same `signature` string as the described method (not counting the declaration itself).
213+
- `OVERRIDDEN_BY.DECLARES_CLIENT` / `OVERRIDDEN_BY.EXPOSES` — same dispatch-down walk, then count outgoing `DECLARES_CLIENT` / `EXPOSES` edges from those override methods. Counts are **edge rows** on overrides (not distinct methods): one override with multiple client edges contributes the full row count. Omitted when zero.
214+
- `OVERRIDES` — on a concrete method: count of **distinct** upstream declarations (interface / superclass methods with the same `signature`) one `IMPLEMENTS`/`EXTENDS` hop from the declaring class. A class implementing two interfaces that both declare the same signature yields `out: 2` (two declaration symbols).
215+
216+
Walk recipe (declaration side): `neighbors(ids=<method_id>, direction="in", edge_types=["DECLARES"])` → declaring type → `neighbors(ids=<type_id>, direction="in", edge_types=["IMPLEMENTS","EXTENDS"])` → each subtype class → `neighbors(ids=<class_id>, direction="out", edge_types=["DECLARES"])` and filter rows where `signature` matches the interface method.
217+
218+
Static methods suppress the entire override-axis rollup. Constructors do not receive these keys.
219+
220+
These keys are **not** valid `EdgeType` literals — `neighbors(edge_types=["OVERRIDDEN_BY"])` fails at the Pydantic boundary. Use them as hop affordances only.
221+
210222
#### `neighbors`
211223

212224
- **Purpose:** One hop over explicit edge types; returns **edges** with attributes (`confidence`, `strategy`, `match`, …) and the **`other`** node.

kuzu_queries.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@
2222

2323
log = logging.getLogger(__name__)
2424

25+
26+
def _coerce_id_list(raw: Any) -> list[str]:
27+
"""Normalize Kuzu ``collect(DISTINCT ...)`` list results to string ids."""
28+
if raw is None:
29+
return []
30+
if isinstance(raw, list):
31+
return [str(x) for x in raw if x is not None and str(x) != ""]
32+
s = str(raw)
33+
return [s] if s else []
34+
35+
2536
__all__ = [
2637
"KuzuGraph",
2738
"resolve_kuzu_path",
@@ -618,6 +629,72 @@ def member_edge_rollup_for(self, type_id: str) -> dict[str, dict[str, int]]:
618629
rollup[key] = {"in": 0, "out": n}
619630
return rollup
620631

632+
def _edge_row_count_from_method_ids(self, method_ids: list[str], rel: str) -> int:
633+
"""Count outgoing ``rel`` edges from method symbols (describe rollup helper)."""
634+
total = 0
635+
for mid in method_ids:
636+
rows = self._rows(
637+
f"MATCH (x:Symbol {{id: $mid}})-[e:{rel}]->() RETURN count(e) AS n",
638+
{"mid": mid},
639+
)
640+
total += int(rows[0].get("n") or 0) if rows else 0
641+
return total
642+
643+
def override_axis_rollup_for(self, method_id: str) -> dict[str, dict[str, int]]:
644+
"""Dispatch-axis composed keys for method Symbols (describe-time only).
645+
646+
Uses one-hop ``IMPLEMENTS`` / ``EXTENDS`` class edges plus ``Symbol.signature``
647+
equality. Omits keys with zero counts (same convention as ``edge_counts_for``).
648+
Returns ``{}`` for non-methods, constructors (caller should skip), and static methods.
649+
"""
650+
params = {"id": method_id}
651+
gate = self._rows(
652+
"MATCH (m:Symbol {id: $id}) "
653+
"WHERE m.kind = 'method' "
654+
"AND NOT list_contains(COALESCE(m.modifiers, []), 'static') "
655+
"RETURN 1 AS ok LIMIT 1",
656+
params,
657+
)
658+
if not gate:
659+
return {}
660+
661+
rollup: dict[str, dict[str, int]] = {}
662+
663+
down_rows = self._rows(
664+
"MATCH (m:Symbol {id: $id})<-[:DECLARES]-(t:Symbol) "
665+
"MATCH (impl:Symbol)-[:IMPLEMENTS|EXTENDS]->(t) "
666+
"MATCH (impl)-[:DECLARES]->(mover:Symbol) "
667+
"WHERE mover.signature = m.signature AND mover.id <> m.id "
668+
"RETURN collect(DISTINCT mover.id) AS ids",
669+
params,
670+
)
671+
impl_ids = _coerce_id_list(down_rows[0].get("ids") if down_rows else None)
672+
673+
if impl_ids:
674+
distinct_impl = list(dict.fromkeys(impl_ids))
675+
rollup["OVERRIDDEN_BY"] = {"in": 0, "out": len(distinct_impl)}
676+
n_dc = self._edge_row_count_from_method_ids(distinct_impl, "DECLARES_CLIENT")
677+
if n_dc > 0:
678+
rollup["OVERRIDDEN_BY.DECLARES_CLIENT"] = {"in": 0, "out": n_dc}
679+
n_ex = self._edge_row_count_from_method_ids(distinct_impl, "EXPOSES")
680+
if n_ex > 0:
681+
rollup["OVERRIDDEN_BY.EXPOSES"] = {"in": 0, "out": n_ex}
682+
683+
up_rows = self._rows(
684+
"MATCH (m:Symbol {id: $id})<-[:DECLARES]-(impl:Symbol) "
685+
"MATCH (impl)-[:IMPLEMENTS|EXTENDS]->(parent:Symbol) "
686+
"MATCH (parent)-[:DECLARES]->(decl_m:Symbol) "
687+
"WHERE decl_m.signature = m.signature AND decl_m.id <> m.id "
688+
"RETURN collect(DISTINCT decl_m.id) AS ids",
689+
params,
690+
)
691+
decl_ids = _coerce_id_list(up_rows[0].get("ids") if up_rows else None)
692+
if decl_ids:
693+
distinct_decl = list(dict.fromkeys(decl_ids))
694+
rollup["OVERRIDES"] = {"in": 0, "out": len(distinct_decl)}
695+
696+
return rollup
697+
621698
def _scope_counts(self, column: str) -> dict[str, int]:
622699
"""Generic helper: count resolved type symbols grouped by `column`.
623700

mcp_v2.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
{"class", "interface", "enum", "record", "annotation"}
4242
)
4343

44+
_METHOD_SYMBOL_KINDS_FOR_OVERRIDE_ROLLUP = frozenset({"method"})
45+
4446

4547
def _get_sentence_transformer(model_name: str, device: str | None) -> SentenceTransformer:
4648
global _st_model
@@ -129,7 +131,10 @@ class NodeRecord(BaseModel):
129131
"enum, record, annotation), may also include composed dot-keys "
130132
"`DECLARES.DECLARES_CLIENT` and `DECLARES.EXPOSES`: 2-hop summaries "
131133
"(DECLARES to member, then that edge) — edge-row counts, not EdgeType literals; "
132-
"do not pass them to neighbors(edge_types=…)."
134+
"do not pass them to neighbors(edge_types=…). For method Symbols, may include "
135+
"override-axis virtual keys `OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_CLIENT`, "
136+
"`OVERRIDDEN_BY.EXPOSES`, and `OVERRIDES` (same dot convention; also not valid "
137+
"EdgeType literals for neighbors)."
133138
),
134139
)
135140

@@ -335,8 +340,11 @@ def _edge_summary_for_node(
335340
graph: KuzuGraph, node_id: str, *, kind: str, row: dict[str, Any]
336341
) -> dict[str, dict[str, int]]:
337342
summary = dict(graph.edge_counts_for(node_id))
338-
if kind == "symbol" and str(row.get("kind") or "") in _TYPE_SYMBOL_KINDS_FOR_EDGE_ROLLUP:
343+
sym_kind = str(row.get("kind") or "")
344+
if kind == "symbol" and sym_kind in _TYPE_SYMBOL_KINDS_FOR_EDGE_ROLLUP:
339345
summary.update(graph.member_edge_rollup_for(node_id))
346+
elif kind == "symbol" and sym_kind in _METHOD_SYMBOL_KINDS_FOR_OVERRIDE_ROLLUP:
347+
summary.update(graph.override_axis_rollup_for(node_id))
340348
return summary
341349

342350

server.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
_COCOINDEX_TARGET = "java_index_flow_lancedb.py:JavaCodeIndexLance"
2020
_INSTRUCTIONS = (
2121
"Java codebase graph navigator (LanceDB + Kuzu). "
22-
"Tools: search (NL/code locate), find (structured NodeFilter), describe (one node + edge_summary: stored edge-label counts and optional composed keys for type Symbols), "
22+
"Tools: search (NL/code locate), find (structured NodeFilter), describe (one node + edge_summary: stored edge-label counts and optional composed keys for type Symbols and override-axis virtual keys for method Symbols), "
2323
"neighbors (one hop; you MUST pass direction in|out AND edge_types list — no defaults). "
2424
"NodeFilter `filter` is a JSON object (preferred); a JSON-encoded string is also accepted as a fallback. "
2525
"Edge labels: EXTENDS, IMPLEMENTS, INJECTS, DECLARES, DECLARES_CLIENT, CALLS, EXPOSES, HTTP_CALLS, ASYNC_CALLS. "
@@ -333,7 +333,9 @@ async def find(
333333
description=(
334334
"full record + edge_summary: in/out per stored edge label; "
335335
"type Symbols may add composed keys DECLARES.DECLARES_CLIENT, DECLARES.EXPOSES "
336-
"(describe-time 2-hop member summaries; not valid in neighbors edge_types)"
336+
"(describe-time 2-hop member summaries; not valid in neighbors edge_types); "
337+
"method Symbols may add override-axis virtual keys OVERRIDDEN_BY, "
338+
"OVERRIDDEN_BY.DECLARES_CLIENT, OVERRIDDEN_BY.EXPOSES, OVERRIDES (same restriction)"
337339
),
338340
)
339341
async def describe(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package orolla.abstractroute;
2+
3+
import org.springframework.web.bind.annotation.RequestMapping;
4+
5+
public abstract class AbstractApi {
6+
@RequestMapping("/api")
7+
public abstract void handle();
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package orolla.abstractroute;
2+
3+
import org.springframework.web.bind.annotation.PostMapping;
4+
import org.springframework.web.bind.annotation.RestController;
5+
6+
@RestController
7+
public class ConcreteApi extends AbstractApi {
8+
@Override
9+
@PostMapping("/do")
10+
public void handle() {}
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package orolla.diamond;
2+
3+
public interface DiamondA {
4+
void shared();
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package orolla.diamond;
2+
3+
public interface DiamondB {
4+
void shared();
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package orolla.diamond;
2+
3+
public class DiamondC implements DiamondA, DiamondB {
4+
@Override
5+
public void shared() {}
6+
}

0 commit comments

Comments
 (0)