Skip to content

Commit 3a9712a

Browse files
feat(neighbors): navigate OVERRIDDEN_BY.* composed edge types in one call
Wire override-axis dot-keys through stored [:OVERRIDES] dispatch hops so describe edge_summary counts match neighbors traversal (merge-blocking parity). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2f88612 commit 3a9712a

10 files changed

Lines changed: 417 additions & 51 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/
244244
| `find` | Locate nodes by structured filter. | `kind: "symbol"\|"route"\|"client"\|"producer"`, `filter: NodeFilter \| str`, `limit: int=25`, `offset: int=0` | `{"kind":"symbol","filter":{"role":"CONTROLLER"}}` |
245245
| `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`, …) and an `OVERRIDES` row that **merges** stored `[:OVERRIDES]` in/out with the dispatch-up rollup (per direction `max`). See [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md) (`describe`). | `id: str` | `{"id":"sym:com.bank.chat.core.api.ChatController#joinOperator(JoinOperatorRequest)"}` |
246246
| `resolve` | Identifier-shaped node lookup (symbol / route / client / producer). Returns `status` `one`, `many`, or `none`; prefer over `describe(fqn=…)` when an FQN may collide. See [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md) (`resolve`). | `identifier: str`, `hint_kind: "symbol"|"route"|"client"|"producer" \| null` | `{"identifier":"com.bank.chat.core.api.ChatController","hint_kind":"symbol"}` |
247-
| `neighbors` | Graph walk. **Required**: `direction` and `edge_types` (stored labels; type Symbols may also pass composed `DECLARES.DECLARES_CLIENT`, `DECLARES.DECLARES_PRODUCER`, `DECLARES.EXPOSES` — `out` only — see [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)). | `ids: str \| list[str]`, `direction: "in"\|"out"`, `edge_types: list[str]`, `limit: int=25`, `offset: int=0`, `filter: NodeFilter \| str \| None`, `edge_filter: EdgeFilter \| str \| None` (`CALLS` only; see guide) | `{"ids":"sym:…ChatController","direction":"out","edge_types":["DECLARES.DECLARES_CLIENT"]}` |
247+
| `neighbors` | Graph walk. **Required**: `direction` and `edge_types` (stored labels; type Symbols may pass composed `DECLARES.*`; non-static method Symbols may pass `OVERRIDDEN_BY*` — `out` only — see [`docs/AGENT-GUIDE.md`](./docs/AGENT-GUIDE.md)). | `ids: str \| list[str]`, `direction: "in"\|"out"`, `edge_types: list[str]`, `limit: int=25`, `offset: int=0`, `filter: NodeFilter \| str \| None`, `edge_filter: EdgeFilter \| str \| None` (`CALLS` only; see guide) | `{"ids":"sym:…ChatController","direction":"out","edge_types":["DECLARES.DECLARES_CLIENT"]}` |
248248

249249
**`NodeFilter` notes:**
250250

@@ -431,7 +431,7 @@ Ontology **15** (CALLS-NOISE) adds `CALLS.callee_declaring_role`, `GraphMeta.pas
431431

432432
Ontology **14** introduces `EDGE_SCHEMA` in `java_ontology.py` as the canonical edge navigation schema (see `docs/EDGE-NAVIGATION.md`). **`HTTP_CALLS` is `Client → Route`** (SCHEMA-V2 PR-B). **`ASYNC_CALLS` is `Producer → Route`** with `DECLARES_PRODUCER` (SCHEMA-V2 PR-C). Run one full reprocess after upgrading through the SCHEMA-V2 sequence (or when you need the v14 ontology gate).
433433

434-
Ontology **13** materializes stored `OVERRIDES` edges between method Symbols (subtype override → supertype declaration, matching `signature` on a direct `IMPLEMENTS` / `EXTENDS` hop). `neighbors(edge_types=["OVERRIDES"])` traverses this relationship; `OVERRIDDEN_BY*` keys in `edge_summary` remain describe-time rollups only.
434+
Ontology **13** materializes stored `OVERRIDES` edges between method Symbols (subtype override → supertype declaration, matching `signature` on a direct `IMPLEMENTS` / `EXTENDS` hop). `neighbors(edge_types=["OVERRIDES"])` traverses this relationship; `OVERRIDDEN_BY*` dot-keys in `edge_summary` are also navigable on method Symbol origins (`out` only).
435435

436436
Ontology **12** renames `@CodebaseClient` to `@CodebaseHttpClient`, types HTTP `method` as the shared `CodebaseHttpMethod` enum on both inbound and outbound stubs, and makes inbound layer-C HTTP routes **replace** same-method built-in Spring rows (no merge). Rebuild after upgrading so `meta_chain` keys and annotation simple names match the extractor.
437437

docs/AGENT-GUIDE.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,26 @@ Use these strings **verbatim** in `neighbors(..., edge_types=[...])`.
7676
| Service boundary | `EXPOSES` | method Symbol → Route (handler exposes route) |
7777
| Cross-service | `HTTP_CALLS`, `ASYNC_CALLS` | `HTTP_CALLS`: Client → Route; `ASYNC_CALLS`: Producer → Route |
7878

79-
**Composed edges (type Symbol origin, `direction="out"` only):**
79+
**Composed edges type Symbol origin (`direction="out"` only):**
8080

8181
| Edge type | Meaning |
8282
| --------- | ------- |
8383
| `DECLARES.DECLARES_CLIENT` | Members' HTTP clients in one hop |
8484
| `DECLARES.DECLARES_PRODUCER` | Members' async producers in one hop |
8585
| `DECLARES.EXPOSES` | Members' exposed routes in one hop |
8686

87-
**Not valid in `edge_types`:** `OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_CLIENT`, `OVERRIDDEN_BY.DECLARES_PRODUCER`, `OVERRIDDEN_BY.EXPOSES` (describe-only virtual keys).
87+
**Composed edges — non-static method Symbol origin (`direction="out"` only):**
88+
89+
| Edge type | Meaning |
90+
| --------- | ------- |
91+
| `OVERRIDDEN_BY` | Concrete overrider methods (stored `[:OVERRIDES]` dispatch hop) |
92+
| `OVERRIDDEN_BY.DECLARES_CLIENT` | Clients declared on overriders (`via_id` = overrider method) |
93+
| `OVERRIDDEN_BY.DECLARES_PRODUCER` | Producers on overriders |
94+
| `OVERRIDDEN_BY.EXPOSES` | Routes exposed by overriders |
95+
96+
**Stored vs virtual direction (base override axis):** `neighbors(decl_id, "out", ["OVERRIDDEN_BY"])` returns the same overrider method ids as `neighbors(decl_id, "in", ["OVERRIDES"])` on the same declaration method. Prefer the dot-key when `describe.edge_summary` advertises `OVERRIDDEN_BY`.
97+
98+
Do not mix `DECLARES.*` and `OVERRIDDEN_BY.*` in one `edge_types` list on a single origin id — the handler rejects the whole request (only one axis applies per node).
8899

89100
### Argument shapes
90101

@@ -201,7 +212,7 @@ Full node + `edge_summary`. Args: `id` (any kind) or `fqn` (symbol only; `id` wi
201212

202213
- **Stored keys** — counts for edges that exist in the graph.
203214
- **Type symbols** (`class`, `interface`, `enum`, `record`, `annotation`) may add composed keys `DECLARES.DECLARES_CLIENT`, `DECLARES.DECLARES_PRODUCER`, `DECLARES.EXPOSES` — navigable via `neighbors` with those dot-keys (`out` only).
204-
- **Method symbols** may add virtual keys `OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_*`, `OVERRIDDEN_BY.EXPOSES` (describe only), plus an **`OVERRIDES`** row merging stored `[:OVERRIDES]` counts with a dispatch-up rollup (`in`/`out` per direction uses `max` of stored vs rollup). Use `neighbors(..., ["OVERRIDES"])` to list override edges. Static methods and constructors do not get override-axis keys.
215+
- **Method symbols** may add virtual keys `OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_*`, `OVERRIDDEN_BY.EXPOSES` (navigable via `neighbors` on method origins, `out` only), plus an **`OVERRIDES`** row merging stored `[:OVERRIDES]` counts with a dispatch-up rollup (`in`/`out` per direction uses `max` of stored vs rollup). Use `neighbors(..., ["OVERRIDDEN_BY.DECLARES_CLIENT"])` (etc.) or one-hop `neighbors(..., ["OVERRIDES"])`. Static methods and constructors do not get override-axis keys.
205216

206217
Composed counts are **edge rows**, not distinct methods; `count > 0` means "there is something to walk".
207218

@@ -249,7 +260,8 @@ Returns **edges** with `attrs` (`confidence`, `strategy`, `match`, … on cross-
249260
| Empty `search` | Wrong `table`, no index, or chunk miss | Try `table="all"`; `find` with `fqn_prefix`; read source files directly |
250261
| Empty results across several tools | Index missing, stale, or wrong project | You cannot rebuild the index via MCP — ask the operator; meanwhile use open files / `rg` |
251262
| Result vs open file disagree | Stale or partial index | Trust the file; say index may be stale |
252-
| Used virtual key in `neighbors` | `OVERRIDDEN_BY*` is describe-only | Use stored `OVERRIDES` or manual walk via `DECLARES` → type → `IMPLEMENTS`/`EXTENDS` |
263+
| Mixed composed families on one id | `DECLARES.*` + `OVERRIDDEN_BY.*` together | Split calls — type keys need a type id; override keys need a method id |
264+
| Override dot-key on type / DECLARES on method | Wrong Symbol origin for axis | Read `describe.edge_summary`; use the axis that matches the node kind |
253265

254266
After two failed attempts on the same intent, stop and report tool name, args, and response snippet.
255267

docs/EDGE-NAVIGATION.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,19 @@
254254
- `alien_subject`: ASYNC_CALLS connects Producer→Route; use DECLARES_PRODUCER from a method Symbol, or neighbors(producer_id,'out',['ASYNC_CALLS']) from a Producer id
255255

256256

257+
## Override-axis composed keys (method Symbol, `direction="out"` only)
258+
259+
Virtual `edge_summary` / `neighbors` dot-keys (not stored graph edge labels). The dispatch hop uses materialized `[:OVERRIDES]`; terminal hops use stored `DECLARES_CLIENT`, `DECLARES_PRODUCER`, or `EXPOSES`.
260+
261+
| Dot-key | Recipe |
262+
| --- | --- |
263+
| `OVERRIDDEN_BY` | `neighbors(['{id}'],'out',['OVERRIDDEN_BY'])` — same overrider method ids as `neighbors(['{id}'],'in',['OVERRIDES'])` on a declaration method |
264+
| `OVERRIDDEN_BY.DECLARES_CLIENT` | `neighbors(['{id}'],'out',['OVERRIDDEN_BY.DECLARES_CLIENT'])` — clients on all overriders (`via_id` = overrider method in attrs) |
265+
| `OVERRIDDEN_BY.DECLARES_PRODUCER` | `neighbors(['{id}'],'out',['OVERRIDDEN_BY.DECLARES_PRODUCER'])` |
266+
| `OVERRIDDEN_BY.EXPOSES` | `neighbors(['{id}'],'out',['OVERRIDDEN_BY.EXPOSES'])` |
267+
268+
Do not mix `DECLARES.*` and `OVERRIDDEN_BY.*` in one `edge_types` list on a single origin id.
269+
257270
## Graph storage (not MCP `neighbors` edge_types)
258271

259272
### `UnresolvedCallSite` + `UNRESOLVED_AT` (ontology 15 / CALLS-NOISE PR-3)

kuzu_queries.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@
3535
)
3636
_MEMBER_EDGE_COMPOSED_REL_BY_KEY: dict[str, str] = dict(_MEMBER_EDGE_COMPOSED_REL_MAP)
3737

38+
_OVERRIDE_AXIS_COMPOSED_REL_MAP: tuple[tuple[str, str | None], ...] = (
39+
("OVERRIDDEN_BY", None),
40+
("OVERRIDDEN_BY.DECLARES_CLIENT", "DECLARES_CLIENT"),
41+
("OVERRIDDEN_BY.DECLARES_PRODUCER", "DECLARES_PRODUCER"),
42+
("OVERRIDDEN_BY.EXPOSES", "EXPOSES"),
43+
)
44+
_OVERRIDE_AXIS_COMPOSED_REL_BY_KEY: dict[str, str | None] = dict(_OVERRIDE_AXIS_COMPOSED_REL_MAP)
45+
OVERRIDE_AXIS_COMPOSED_EDGE_TYPES: frozenset[str] = frozenset(_OVERRIDE_AXIS_COMPOSED_REL_BY_KEY)
46+
3847

3948
def _coerce_id_list(raw: Any) -> list[str]:
4049
"""Normalize Kuzu ``collect(DISTINCT ...)`` list results to string ids."""
@@ -667,6 +676,34 @@ def member_edge_traversal_for(self, type_id: str, composed_key: str) -> list[dic
667676
{"id": type_id, "rel": rel},
668677
)
669678

679+
def override_axis_traversal_for(self, method_id: str, composed_key: str) -> list[dict[str, Any]]:
680+
"""Override-axis composed traversal for a method Symbol (neighbors dot-key path).
681+
682+
Uses stored ``[:OVERRIDES]`` for the dispatch hop (aligned with ``override_axis_rollup_for``
683+
overrider ids). Base key returns overrider method ids only; composed keys return terminal
684+
rows with full edge attr projection plus ``via_id`` (overrider method id).
685+
"""
686+
rel = _OVERRIDE_AXIS_COMPOSED_REL_BY_KEY.get(composed_key)
687+
if rel is None and composed_key != "OVERRIDDEN_BY":
688+
return []
689+
if rel is None:
690+
return self._rows(
691+
"MATCH (decl:Symbol {id: $id})<-[:OVERRIDES]-(mover:Symbol) "
692+
"RETURN mover.id AS other_id",
693+
{"id": method_id},
694+
)
695+
return self._rows(
696+
"MATCH (decl:Symbol {id: $id})<-[:OVERRIDES]-(mover:Symbol)-[e]->(term) "
697+
"WHERE label(e) = $rel "
698+
"RETURN mover.id AS via_id, label(e) AS stored_edge_type, "
699+
"term.id AS other_id, e.confidence AS confidence, e.strategy AS strategy, "
700+
"e.match AS match, e.mechanism AS mechanism, e.annotation AS annotation, "
701+
"e.field_or_param AS field_or_param, e.source AS source, "
702+
"e.call_site_line AS call_site_line, e.call_site_byte AS call_site_byte, "
703+
"e.arg_count AS arg_count, e.resolved AS resolved",
704+
{"id": method_id, "rel": rel},
705+
)
706+
670707
def count_calls_for_symbol(self, origin_id: str, *, direction: Literal["in", "out"]) -> int:
671708
"""Count CALLS edges incident on a Symbol (hints / diagnostics)."""
672709
if direction == "out":

mcp_hints.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"Road-sign hints pointing to likely next calls. Each hint is a short string "
2020
"referencing one MCP V2 tool call. Hints are advisory and may be safely ignored. "
2121
"Maximum 5 hints per output. Describe-time type rollup hints may recommend "
22-
"DECLARES.* dot-keys for neighbors(); empty neighbors structural hints never use "
22+
"DECLARES.* and OVERRIDDEN_BY.* dot-keys for neighbors() on matching Symbol origins; "
23+
"empty neighbors structural hints never use "
2324
"dot-key edge labels. For neighbors with multiple origin ids, empty-result "
2425
"structural hints describe the first origin only. On neighbors with "
2526
"edge_types=['CALLS'] only, optional edge_filter projects the ordered CALLS stream "
@@ -40,18 +41,15 @@
4041
TPL_DESCRIBE_TYPE_PRODUCERS_VIA_MEMBERS = (
4142
"producers via members: neighbors(['{id}'],'out',['DECLARES.DECLARES_PRODUCER'])"
4243
)
43-
TPL_DESCRIBE_METHOD_OVERRIDERS = "overriders: neighbors(['{id}'],'in',['OVERRIDES'])"
44+
TPL_DESCRIBE_METHOD_OVERRIDERS = "overriders: neighbors(['{id}'],'out',['OVERRIDDEN_BY'])"
4445
TPL_DESCRIBE_METHOD_CLIENTS_IN_OVERRIDERS = (
45-
"clients in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
46-
"then neighbors(overrider_ids,'out',['DECLARES_CLIENT'])"
46+
"clients in overriders: neighbors(['{id}'],'out',['OVERRIDDEN_BY.DECLARES_CLIENT'])"
4747
)
4848
TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS = (
49-
"producers in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
50-
"then neighbors(overrider_ids,'out',['DECLARES_PRODUCER'])"
49+
"producers in overriders: neighbors(['{id}'],'out',['OVERRIDDEN_BY.DECLARES_PRODUCER'])"
5150
)
5251
TPL_DESCRIBE_METHOD_ROUTES_IN_OVERRIDERS = (
53-
"routes in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
54-
"then neighbors(overrider_ids,'out',['EXPOSES'])"
52+
"routes in overriders: neighbors(['{id}'],'out',['OVERRIDDEN_BY.EXPOSES'])"
5553
)
5654
TPL_DESCRIBE_METHOD_OUTBOUND_CLIENT = "outbound client: neighbors(['{id}'],'out',['DECLARES_CLIENT'])"
5755
TPL_DESCRIBE_METHOD_OUTBOUND_PRODUCER = "outbound producer: neighbors(['{id}'],'out',['DECLARES_PRODUCER'])"

0 commit comments

Comments
 (0)