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 @@ -244,7 +244,7 @@ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/
| `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 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)"}` |
| `resolve` | Identifier-shaped node lookup (symbol / route / client). 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" \| null` | `{"identifier":"com.bank.chat.core.api.ChatController","hint_kind":"symbol"}` |
| `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"]}` |
| `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` | `{"ids":"sym:…ChatController","direction":"out","edge_types":["DECLARES.DECLARES_CLIENT"]}` |

**`NodeFilter` notes:**

Expand Down
13 changes: 7 additions & 6 deletions docs/AGENT-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,16 +225,16 @@ 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.DECLARES_PRODUCER`, `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.DECLARES_PRODUCER`, `OVERRIDDEN_BY.EXPOSES`) plus an **`OVERRIDES` row** that merges stored `[:OVERRIDES]` incident counts with the describe-time dispatch-up rollup (per direction `max`, so inbound stored overrides are preserved); see **Override-axis keys (method Symbols)** below — those virtual keys are **not** `neighbors` arguments. The **stored** relationship label **`OVERRIDES`** **is** a valid `EdgeType` for `neighbors` (same spelling as the map key; the map row is the merged view).
- **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.DECLARES_PRODUCER`, `DECLARES.EXPOSES`); those three keys **are** valid in `neighbors(edge_types=…)` for type Symbol origins (`direction="out"` only). For **method** Symbols, the map may include **override-axis virtual keys** (`OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_CLIENT`, `OVERRIDDEN_BY.DECLARES_PRODUCER`, `OVERRIDDEN_BY.EXPOSES`) plus an **`OVERRIDES` row** that merges stored `[:OVERRIDES]` incident counts with the describe-time dispatch-up rollup (per direction `max`, so inbound stored overrides are preserved); see **Override-axis keys (method Symbols)** below — those virtual keys are **not** `neighbors` arguments. The **stored** relationship label **`OVERRIDES`** **is** a valid `EdgeType` for `neighbors` (same spelling as the map key; the map row is the merged view).
- **Args:** `id` (symbol, route, or client id) or **`fqn`** (exact symbol FQN when you do not have the graph id). When both are set, `id` wins. For identifier-shaped inputs and FQN collision handling, see **Identifier resolution** above.

**Composed `edge_summary` keys (type Symbols).** Keys use dot notation: `<parent_relation>.<projected_relation>`. Three are emitted today:

- `DECLARES.DECLARES_CLIENT` — the type's methods declare brownfield HTTP clients (count is the number of `Client` rows reached through `DECLARES → DECLARES_CLIENT`). To enumerate them: `neighbors(ids=<class_id>, direction="out", edge_types=["DECLARES"])` → for each method id, `neighbors(ids=<method_id>, direction="out", edge_types=["DECLARES_CLIENT"])`.
- `DECLARES.DECLARES_PRODUCER` — the type's methods declare async producers. Same walk shape with `DECLARES_PRODUCER`.
- `DECLARES.EXPOSES` — the type's methods expose routes. Same walk shape with `EXPOSES`.
- `DECLARES.DECLARES_CLIENT` — the type's methods declare brownfield HTTP clients (count is the number of `Client` rows reached through `DECLARES → DECLARES_CLIENT`). Primary recipe: `neighbors(ids=<class_id>, direction="out", edge_types=["DECLARES.DECLARES_CLIENT"])` → terminal `Client` nodes with `via_id` in `attrs`. Alternative (two hops): `neighbors(..., ["DECLARES"])` then per-method `DECLARES_CLIENT`.
- `DECLARES.DECLARES_PRODUCER` — producers via members. Use `edge_types=["DECLARES.DECLARES_PRODUCER"]` (or the two-hop `DECLARES` → `DECLARES_PRODUCER` walk).
- `DECLARES.EXPOSES` — routes via members. Use `edge_types=["DECLARES.EXPOSES"]` (or the two-hop `DECLARES` → `EXPOSES` walk).

Composed keys are **read-only**: they cannot be passed to `neighbors(edge_types=…)` (the dot is not a valid `EdgeType` literal — the call fails with a Pydantic `ValidationError`). Use them as a hop affordance only.
These three `DECLARES.*` dot-keys are navigable on **type** Symbol origins (`out` only). `OVERRIDDEN_BY*` virtual keys remain describe-only.

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.

Expand All @@ -248,7 +248,7 @@ Walk recipe (manual, if you need types in the middle): `neighbors(ids=<method_id

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

Virtual keys (`OVERRIDDEN_BY`, …) and composed dot-keys are **not** valid `EdgeType` literals — `neighbors(edge_types=["OVERRIDDEN_BY"])` fails at the Pydantic boundary. Use them as hop affordances only. **`OVERRIDES`** in `edge_types=[...]` selects the **stored** override relationship (same spelling as the `OVERRIDES` map key, whose `in`/`out` merge Kuzu edges with the dispatch-up rollup).
Virtual keys (`OVERRIDDEN_BY`, …) are **not** valid `neighbors` arguments — `neighbors(edge_types=["OVERRIDDEN_BY"])` fails at the Pydantic boundary. **`OVERRIDES`** in `edge_types=[...]` selects the **stored** override relationship (same spelling as the `OVERRIDES` map key, whose `in`/`out` merge Kuzu edges with the dispatch-up rollup).

#### `resolve`

Expand All @@ -261,6 +261,7 @@ Virtual keys (`OVERRIDDEN_BY`, …) and composed dot-keys are **not** valid `Edg
- **Purpose:** One hop over explicit edge types; returns **edges** with attributes (`confidence`, `strategy`, `match`, …) and the **`other`** node.
- **Args:** `ids` (string or array — batch allowed), **`direction`** (`in`|`out`), **`edge_types`** (non-empty list), `limit`, `offset`, optional `filter` on the other node.
- **Batching:** Multiple origins are expanded; pagination slices the **combined** edge list — use larger `limit` when batching many ids.
- **Mixed flat + composed `edge_types`:** flat edges are appended before composed edges, then `limit`/`offset` apply. A small `limit` with e.g. `["DECLARES", "DECLARES.DECLARES_CLIENT"]` may return only member Symbols and no Clients — use the dot-key alone when enumerating terminals.
- **Confidence:** Cross-service edges (`HTTP_CALLS`, `ASYNC_CALLS`) carry confidence, strategy, and match metadata on `edge.attrs` (`attrs.confidence`, `attrs.strategy`, `attrs.match`). Low confidence means the resolver had to guess at the route binding — treat it as a **resolver gap signal**, not a hallucination. Report low-confidence edges with their confidence value, not as facts. Intra-service edges (`CALLS`, `INJECTS`, `IMPLEMENTS`, `EXTENDS`, `DECLARES`, `DECLARES_CLIENT`, `EXPOSES`, `OVERRIDES`) faithfully represent the static graph; the resolved set is still a **lower bound** under reflection / dynamic dispatch (see *What this MCP is NOT*).

### Ontology glossary (version 14)
Expand Down
6 changes: 3 additions & 3 deletions docs/EDGE-NAVIGATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@

**Typical traversals**:

- `type_subject`: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'{direction}',['EXPOSES'])
- `type_subject`: neighbors(['{id}'],'out',['DECLARES.EXPOSES']) — or neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'{direction}',['EXPOSES'])
- `member_subject`: neighbors(['{id}'],'out',['EXPOSES'])
- `alien_subject`: EXPOSES connects method Symbol → Route; use a method Symbol id

Expand All @@ -180,7 +180,7 @@

**Typical traversals**:

- `type_subject`: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'{direction}',['DECLARES_CLIENT'])
- `type_subject`: neighbors(['{id}'],'out',['DECLARES.DECLARES_CLIENT']) — or neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'{direction}',['DECLARES_CLIENT'])
- `member_subject`: neighbors(['{id}'],'out',['DECLARES_CLIENT'])
- `alien_subject`: DECLARES_CLIENT connects method Symbol → Client

Expand All @@ -200,7 +200,7 @@

**Typical traversals**:

- `type_subject`: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'{direction}',['DECLARES_PRODUCER'])
- `type_subject`: neighbors(['{id}'],'out',['DECLARES.DECLARES_PRODUCER']) — or neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'{direction}',['DECLARES_PRODUCER'])
- `member_subject`: neighbors(['{id}'],'out',['DECLARES_PRODUCER'])
- `alien_subject`: DECLARES_PRODUCER connects method Symbol → Producer

Expand Down
5 changes: 5 additions & 0 deletions java_ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ class EdgeSpec:
"then neighbors(member_ids,'{direction}',['{edge}'])"
)

_COMPOSED_MEMBER_TYPE_TRAVERSAL = (
"neighbors(['{id}'],'out',['DECLARES.{edge}']) — or "
"neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'{direction}',['{edge}'])"
)

EDGE_SCHEMA: dict[str, EdgeSpec] = {
"EXTENDS": EdgeSpec(
name="EXTENDS",
Expand Down
33 changes: 28 additions & 5 deletions kuzu_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@

log = logging.getLogger(__name__)

# Composed describe / neighbors dot-keys (not stored graph edge labels).
_MEMBER_EDGE_COMPOSED_REL_MAP: tuple[tuple[str, str], ...] = (
("DECLARES.DECLARES_CLIENT", "DECLARES_CLIENT"),
("DECLARES.DECLARES_PRODUCER", "DECLARES_PRODUCER"),
("DECLARES.EXPOSES", "EXPOSES"),
)
_MEMBER_EDGE_COMPOSED_REL_BY_KEY: dict[str, str] = dict(_MEMBER_EDGE_COMPOSED_REL_MAP)


def _coerce_id_list(raw: Any) -> list[str]:
"""Normalize Kuzu ``collect(DISTINCT ...)`` list results to string ids."""
Expand Down Expand Up @@ -629,11 +637,7 @@ def member_edge_rollup_for(self, type_id: str) -> dict[str, dict[str, int]]:
"""
params = {"id": type_id}
rollup: dict[str, dict[str, int]] = {}
for key, rel in (
("DECLARES.DECLARES_CLIENT", "DECLARES_CLIENT"),
("DECLARES.DECLARES_PRODUCER", "DECLARES_PRODUCER"),
("DECLARES.EXPOSES", "EXPOSES"),
):
for key, rel in _MEMBER_EDGE_COMPOSED_REL_MAP:
rows = self._rows(
f"MATCH (t:Symbol {{id: $id}})-[:DECLARES]->(m:Symbol)-[e:{rel}]->() "
"RETURN count(e) AS n",
Expand All @@ -644,6 +648,25 @@ def member_edge_rollup_for(self, type_id: str) -> dict[str, dict[str, int]]:
rollup[key] = {"in": 0, "out": n}
return rollup

def member_edge_traversal_for(self, type_id: str, composed_key: str) -> list[dict[str, Any]]:
"""2-hop DECLARES member traversal for a type Symbol (neighbors dot-key path)."""
rel = _MEMBER_EDGE_COMPOSED_REL_BY_KEY.get(composed_key)
if rel is None:
return []
# Untyped [e] + label(e) filter: typed unions fail the binder when RETURN references
# columns that exist on only some rel types (same pattern as flat neighbors_v2).
return self._rows(
"MATCH (t:Symbol {id: $id})-[:DECLARES]->(m:Symbol)-[e]->(term) "
"WHERE label(e) = $rel "
"RETURN m.id AS via_id, label(e) AS stored_edge_type, "
"term.id AS other_id, e.confidence AS confidence, e.strategy AS strategy, "
"e.match AS match, e.mechanism AS mechanism, e.annotation AS annotation, "
"e.field_or_param AS field_or_param, e.source AS source, "
"e.call_site_line AS call_site_line, e.call_site_byte AS call_site_byte, "
"e.arg_count AS arg_count, e.resolved AS resolved",
{"id": type_id, "rel": rel},
)

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
Expand Down
16 changes: 7 additions & 9 deletions mcp_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,22 @@
MCP_HINTS_FIELD_DESCRIPTION = (
"Road-sign hints pointing to likely next calls. Each hint is a short string "
"referencing one MCP V2 tool call. Hints are advisory and may be safely ignored. "
"Maximum 5 hints per output. Hints never recommend dot-key edge labels (composed "
"rollups) as neighbors() arguments. For neighbors with multiple origin ids, "
"empty-result structural hints describe the first origin only."
"Maximum 5 hints per output. Describe-time type rollup hints may recommend "
"DECLARES.* dot-keys for neighbors(); empty neighbors structural hints never use "
"dot-key edge labels. For neighbors with multiple origin ids, empty-result "
"structural hints describe the first origin only."
)

# --- Appendix A verbatim templates (substitute {id}, {kind}, {limit}) ---

TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS = (
"clients via members: neighbors(['{id}'],'out',['DECLARES']) "
"then neighbors(member_ids,'out',['DECLARES_CLIENT'])"
"clients via members: neighbors(['{id}'],'out',['DECLARES.DECLARES_CLIENT'])"
)
TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS = (
"routes via members: neighbors(['{id}'],'out',['DECLARES']) "
"then neighbors(member_ids,'out',['EXPOSES'])"
"routes via members: neighbors(['{id}'],'out',['DECLARES.EXPOSES'])"
)
TPL_DESCRIBE_TYPE_PRODUCERS_VIA_MEMBERS = (
"producers via members: neighbors(['{id}'],'out',['DECLARES']) "
"then neighbors(member_ids,'out',['DECLARES_PRODUCER'])"
"producers via members: neighbors(['{id}'],'out',['DECLARES.DECLARES_PRODUCER'])"
)
TPL_DESCRIBE_METHOD_OVERRIDERS = "overriders: neighbors(['{id}'],'in',['OVERRIDES'])"
TPL_DESCRIBE_METHOD_CLIENTS_IN_OVERRIDERS = (
Expand Down
Loading
Loading