Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/
|---|---|---|---|
| `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}` |
| `find` | Locate nodes by structured filter. | `kind: "symbol"\|"route"\|"client"`, `filter: NodeFilter \| str`, `limit: int=25`, `offset: int=0` | `{"kind":"symbol","filter":{"role":"CONTROLLER"}}` |
| `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)"}` |
| `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)"}` |
| `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"]}` |

**`NodeFilter` notes:**
Expand Down
14 changes: 13 additions & 1 deletion docs/AGENT-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ Exact allowed values for roles, capabilities, client kinds, etc. live in `java_o

#### `describe`

- **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=…)`.
- **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.
- **Args:** `id` (symbol, route, or client id).

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

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.

**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):

- `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).
- `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.
- `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).

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.

Static methods suppress the entire override-axis rollup. Constructors do not receive these keys.

These keys are **not** valid `EdgeType` literals — `neighbors(edge_types=["OVERRIDDEN_BY"])` fails at the Pydantic boundary. Use them as hop affordances only.

#### `neighbors`

- **Purpose:** One hop over explicit edge types; returns **edges** with attributes (`confidence`, `strategy`, `match`, …) and the **`other`** node.
Expand Down
77 changes: 77 additions & 0 deletions kuzu_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@

log = logging.getLogger(__name__)


def _coerce_id_list(raw: Any) -> list[str]:
"""Normalize Kuzu ``collect(DISTINCT ...)`` list results to string ids."""
if raw is None:
return []
if isinstance(raw, list):
return [str(x) for x in raw if x is not None and str(x) != ""]
s = str(raw)
return [s] if s else []


__all__ = [
"KuzuGraph",
"resolve_kuzu_path",
Expand Down Expand Up @@ -618,6 +629,72 @@ def member_edge_rollup_for(self, type_id: str) -> dict[str, dict[str, int]]:
rollup[key] = {"in": 0, "out": n}
return rollup

def _edge_row_count_from_method_ids(self, method_ids: list[str], rel: str) -> int:
"""Count outgoing ``rel`` edges from method symbols (describe rollup helper)."""
total = 0
for mid in method_ids:
rows = self._rows(
f"MATCH (x:Symbol {{id: $mid}})-[e:{rel}]->() RETURN count(e) AS n",
{"mid": mid},
)
total += int(rows[0].get("n") or 0) if rows else 0
return total

def override_axis_rollup_for(self, method_id: str) -> dict[str, dict[str, int]]:
"""Dispatch-axis composed keys for method Symbols (describe-time only).

Uses one-hop ``IMPLEMENTS`` / ``EXTENDS`` class edges plus ``Symbol.signature``
equality. Omits keys with zero counts (same convention as ``edge_counts_for``).
Returns ``{}`` for non-methods, constructors (caller should skip), and static methods.
"""
params = {"id": method_id}
gate = self._rows(
"MATCH (m:Symbol {id: $id}) "
"WHERE m.kind = 'method' "
"AND NOT list_contains(COALESCE(m.modifiers, []), 'static') "
"RETURN 1 AS ok LIMIT 1",
params,
)
if not gate:
return {}

rollup: dict[str, dict[str, int]] = {}

down_rows = self._rows(
"MATCH (m:Symbol {id: $id})<-[:DECLARES]-(t:Symbol) "
"MATCH (impl:Symbol)-[:IMPLEMENTS|EXTENDS]->(t) "
"MATCH (impl)-[:DECLARES]->(mover:Symbol) "
"WHERE mover.signature = m.signature AND mover.id <> m.id "
"RETURN collect(DISTINCT mover.id) AS ids",
params,
)
impl_ids = _coerce_id_list(down_rows[0].get("ids") if down_rows else None)

if impl_ids:
distinct_impl = list(dict.fromkeys(impl_ids))
rollup["OVERRIDDEN_BY"] = {"in": 0, "out": len(distinct_impl)}
n_dc = self._edge_row_count_from_method_ids(distinct_impl, "DECLARES_CLIENT")
if n_dc > 0:
rollup["OVERRIDDEN_BY.DECLARES_CLIENT"] = {"in": 0, "out": n_dc}
n_ex = self._edge_row_count_from_method_ids(distinct_impl, "EXPOSES")
if n_ex > 0:
rollup["OVERRIDDEN_BY.EXPOSES"] = {"in": 0, "out": n_ex}

up_rows = self._rows(
"MATCH (m:Symbol {id: $id})<-[:DECLARES]-(impl:Symbol) "
"MATCH (impl)-[:IMPLEMENTS|EXTENDS]->(parent:Symbol) "
"MATCH (parent)-[:DECLARES]->(decl_m:Symbol) "
"WHERE decl_m.signature = m.signature AND decl_m.id <> m.id "
"RETURN collect(DISTINCT decl_m.id) AS ids",
params,
)
decl_ids = _coerce_id_list(up_rows[0].get("ids") if up_rows else None)
if decl_ids:
distinct_decl = list(dict.fromkeys(decl_ids))
rollup["OVERRIDES"] = {"in": 0, "out": len(distinct_decl)}

return rollup

def _scope_counts(self, column: str) -> dict[str, int]:
"""Generic helper: count resolved type symbols grouped by `column`.

Expand Down
12 changes: 10 additions & 2 deletions mcp_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
{"class", "interface", "enum", "record", "annotation"}
)

_METHOD_SYMBOL_KINDS_FOR_OVERRIDE_ROLLUP = frozenset({"method"})


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

Expand Down Expand Up @@ -335,8 +340,11 @@ def _edge_summary_for_node(
graph: KuzuGraph, node_id: str, *, kind: str, row: dict[str, Any]
) -> dict[str, dict[str, int]]:
summary = dict(graph.edge_counts_for(node_id))
if kind == "symbol" and str(row.get("kind") or "") in _TYPE_SYMBOL_KINDS_FOR_EDGE_ROLLUP:
sym_kind = str(row.get("kind") or "")
if kind == "symbol" and sym_kind in _TYPE_SYMBOL_KINDS_FOR_EDGE_ROLLUP:
summary.update(graph.member_edge_rollup_for(node_id))
elif kind == "symbol" and sym_kind in _METHOD_SYMBOL_KINDS_FOR_OVERRIDE_ROLLUP:
summary.update(graph.override_axis_rollup_for(node_id))
return summary


Expand Down
6 changes: 4 additions & 2 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
_COCOINDEX_TARGET = "java_index_flow_lancedb.py:JavaCodeIndexLance"
_INSTRUCTIONS = (
"Java codebase graph navigator (LanceDB + Kuzu). "
"Tools: search (NL/code locate), find (structured NodeFilter), describe (one node + edge_summary: stored edge-label counts and optional composed keys for type Symbols), "
"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), "
"neighbors (one hop; you MUST pass direction in|out AND edge_types list — no defaults). "
"NodeFilter `filter` is a JSON object (preferred); a JSON-encoded string is also accepted as a fallback. "
"Edge labels: EXTENDS, IMPLEMENTS, INJECTS, DECLARES, DECLARES_CLIENT, CALLS, EXPOSES, HTTP_CALLS, ASYNC_CALLS. "
Expand Down Expand Up @@ -333,7 +333,9 @@ async def find(
description=(
"full record + edge_summary: in/out per stored edge label; "
"type Symbols may add composed keys DECLARES.DECLARES_CLIENT, DECLARES.EXPOSES "
"(describe-time 2-hop member summaries; not valid in neighbors edge_types)"
"(describe-time 2-hop member summaries; not valid in neighbors edge_types); "
"method Symbols may add override-axis virtual keys OVERRIDDEN_BY, "
"OVERRIDDEN_BY.DECLARES_CLIENT, OVERRIDDEN_BY.EXPOSES, OVERRIDES (same restriction)"
),
)
async def describe(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package orolla.abstractroute;

import org.springframework.web.bind.annotation.RequestMapping;

public abstract class AbstractApi {
@RequestMapping("/api")
public abstract void handle();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package orolla.abstractroute;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConcreteApi extends AbstractApi {
@Override
@PostMapping("/do")
public void handle() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package orolla.diamond;

public interface DiamondA {
void shared();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package orolla.diamond;

public interface DiamondB {
void shared();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package orolla.diamond;

public class DiamondC implements DiamondA, DiamondB {
@Override
public void shared() {}
}
Loading
Loading