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
7 changes: 4 additions & 3 deletions docs/AGENT-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,13 @@ 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=…)`. For **method** Symbols, the map may include **override-axis virtual keys** (`OVERRIDDEN_BY`, `OVERRIDDEN_BY.DECLARES_CLIENT`, `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 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).
- **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>`. Two are emitted today:
**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`.

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.
Expand All @@ -240,7 +241,7 @@ Note on counting semantics: composed counts measure **edge rows**, not distinct
**Override-axis keys (method Symbols).** Dispatch-axis signals computed at describe-time from `IMPLEMENTS` / `EXTENDS` plus matching `Symbol.signature` (not stored as their own rel types):

- `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.
- `OVERRIDDEN_BY.DECLARES_CLIENT` / `OVERRIDDEN_BY.DECLARES_PRODUCER` / `OVERRIDDEN_BY.EXPOSES` — same dispatch-down walk, then count outgoing `DECLARES_CLIENT` / `DECLARES_PRODUCER` / `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` (map row) — merges **stored** `[:OVERRIDES]` `in`/`out` (subtype→supertype edges in Kuzu) with the dispatch-up rollup (distinct upstream declarations one `IMPLEMENTS`/`EXTENDS` hop away, same `signature`). The rollup alone always reported `in: 0`; merging fixes `in` when this method is also a super declaration with incoming override edges. A class implementing two interfaces that both declare the same signature yields `out: 2` on the rollup arm (and matches stored outbound edges when materialization aligns). Prefer `neighbors(ids=<method_id>, direction="out", edge_types=["OVERRIDES"])` to list declaration ids, and `direction="in"` for overriders.

Walk recipe (manual, if you need types in the middle): `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.
Expand Down
34 changes: 33 additions & 1 deletion mcp_hints.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Pure MCP v2 road-sign hint generation (no graph I/O, no search, no LLM).

Locked v1 catalog: ``propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`` Appendix A.
Locked v1 catalog: ``propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`` Appendix A
(issue #161 producer/override-route amendments in that appendix).
v2 resolve + neighbors fuzzy-strategy catalog: ``propose/completed/HINTS-V2-PROPOSE.md`` Appendix A.
v3 empty-neighbors structural catalog: ``propose/completed/HINTS-V3-PROPOSE.md`` §3.1–3.3.
Priority cap: same propose §7.12 / ``plans/completed/PLAN-HINTS.md`` principles.
Expand Down Expand Up @@ -31,16 +32,30 @@
"routes via members: neighbors(['{id}'],'out',['DECLARES']) "
"then neighbors(member_ids,'out',['EXPOSES'])"
)
TPL_DESCRIBE_TYPE_PRODUCERS_VIA_MEMBERS = (
"producers via members: neighbors(['{id}'],'out',['DECLARES']) "
"then neighbors(member_ids,'out',['DECLARES_PRODUCER'])"
)
TPL_DESCRIBE_METHOD_OVERRIDERS = "overriders: neighbors(['{id}'],'in',['OVERRIDES'])"
TPL_DESCRIBE_METHOD_CLIENTS_IN_OVERRIDERS = (
"clients in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
"then neighbors(overrider_ids,'out',['DECLARES_CLIENT'])"
)
TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS = (
"producers in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
"then neighbors(overrider_ids,'out',['DECLARES_PRODUCER'])"
)
TPL_DESCRIBE_METHOD_ROUTES_IN_OVERRIDERS = (
"routes in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
"then neighbors(overrider_ids,'out',['EXPOSES'])"
)
TPL_DESCRIBE_METHOD_OUTBOUND_CLIENT = "outbound client: neighbors(['{id}'],'out',['DECLARES_CLIENT'])"
TPL_DESCRIBE_METHOD_OUTBOUND_PRODUCER = "outbound producer: neighbors(['{id}'],'out',['DECLARES_PRODUCER'])"
TPL_DESCRIBE_METHOD_INBOUND_ROUTE = "inbound route: neighbors(['{id}'],'out',['EXPOSES'])"
TPL_DESCRIBE_METHOD_MANY_CALLS = "many CALLS — consider filtering by target microservice"
TPL_DESCRIBE_ROUTE_DECLARING = "declaring method: neighbors(['{id}'],'in',['EXPOSES'])"
TPL_DESCRIBE_CLIENT_DECLARING = "declaring method: neighbors(['{id}'],'in',['DECLARES_CLIENT'])"
TPL_DESCRIBE_PRODUCER_DECLARING = "declaring method: neighbors(['{id}'],'in',['DECLARES_PRODUCER'])"

TPL_FIND_EMPTY_RESOLVE = "no matches — try resolve(identifier, hint_kind='{kind}') for canonical lookup"
TPL_FIND_PAGE_FULL = "result page full at {limit} — narrow filter or paginate"
Expand Down Expand Up @@ -418,6 +433,9 @@ def generate_hints(
if kind == "client":
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_CLIENT_DECLARING.format(id=node_id)))
return finalize_hint_list(pairs)
if kind == "producer":
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_PRODUCER_DECLARING.format(id=node_id)))
return finalize_hint_list(pairs)

if kind != "symbol":
return finalize_hint_list(pairs)
Expand All @@ -435,6 +453,10 @@ def generate_hints(
pairs.append(
(PRIORITY_DECLARES_TYPE_ROLLUP, TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS.format(id=node_id))
)
if _out_count(edge_summary, "DECLARES.DECLARES_PRODUCER") > 0:
pairs.append(
(PRIORITY_DECLARES_TYPE_ROLLUP, TPL_DESCRIBE_TYPE_PRODUCERS_VIA_MEMBERS.format(id=node_id))
)
return finalize_hint_list(pairs)

if is_method:
Expand All @@ -444,8 +466,18 @@ def generate_hints(
pairs.append(
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_CLIENTS_IN_OVERRIDERS.format(id=node_id))
)
if _out_count(edge_summary, "OVERRIDDEN_BY.DECLARES_PRODUCER") > 0:
pairs.append(
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS.format(id=node_id))
)
if _out_count(edge_summary, "OVERRIDDEN_BY.EXPOSES") > 0:
pairs.append(
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_ROUTES_IN_OVERRIDERS.format(id=node_id))
)
if _out_count(edge_summary, "DECLARES_CLIENT") > 0:
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_OUTBOUND_CLIENT.format(id=node_id)))
if _out_count(edge_summary, "DECLARES_PRODUCER") > 0:
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_OUTBOUND_PRODUCER.format(id=node_id)))
if _out_count(edge_summary, "EXPOSES") > 0:
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_INBOUND_ROUTE.format(id=node_id)))
if _out_count(edge_summary, "CALLS") >= 10:
Expand Down
7 changes: 7 additions & 0 deletions propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,18 @@ The §3.3 table *is* the appendix artifact — every row is a verbatim template
# DescribeOutput
DECLARES.DECLARES_CLIENT.out>0 → "clients via members: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'out',['DECLARES_CLIENT'])"
DECLARES.EXPOSES.out>0 → "routes via members: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'out',['EXPOSES'])"
DECLARES.DECLARES_PRODUCER.out>0 → "producers via members: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'out',['DECLARES_PRODUCER'])" # amendment #161
OVERRIDDEN_BY.out>0 → "overriders: neighbors(['{id}'],'in',['OVERRIDES'])" # requires PR-A; rollup stores counts on .out per override_axis_rollup_for
OVERRIDDEN_BY.DECLARES_CLIENT.out>0 → "clients in overriders: neighbors(['{id}'],'in',['OVERRIDES']) then neighbors(overrider_ids,'out',['DECLARES_CLIENT'])" # requires PR-A
OVERRIDDEN_BY.DECLARES_PRODUCER.out>0 → "producers in overriders: neighbors(['{id}'],'in',['OVERRIDES']) then neighbors(overrider_ids,'out',['DECLARES_PRODUCER'])" # amendment #161
OVERRIDDEN_BY.EXPOSES.out>0 → "routes in overriders: neighbors(['{id}'],'in',['OVERRIDES']) then neighbors(overrider_ids,'out',['EXPOSES'])" # amendment #161
DECLARES_CLIENT.out>0 (method) → "outbound client: neighbors(['{id}'],'out',['DECLARES_CLIENT'])"
DECLARES_PRODUCER.out>0 (method) → "outbound producer: neighbors(['{id}'],'out',['DECLARES_PRODUCER'])" # amendment #161
EXPOSES.out>0 (method) → "inbound route: neighbors(['{id}'],'out',['EXPOSES'])"
CALLS.out>=10 (method) → "many CALLS — consider filtering by target microservice"
kind == route, always → "declaring method: neighbors(['{id}'],'in',['EXPOSES'])"
kind == client, always → "declaring method: neighbors(['{id}'],'in',['DECLARES_CLIENT'])"
kind == producer, always → "declaring method: neighbors(['{id}'],'in',['DECLARES_PRODUCER'])" # amendment #161

# FindOutput
results==[] and filter has identifier-shaped value → "no matches — try resolve(identifier, hint_kind='{kind}') for canonical lookup"
Expand All @@ -273,6 +278,8 @@ File placement (`mcp_hints.py`), function decomposition, integration points in `

## Appendix B — What changed (traceability)

**Amendment (2026-05-16, issue #161 / PR #164)** — five describe templates for the producer axis and override-route rollup, symmetric with the client/route rows above: `DECLARES.DECLARES_PRODUCER`, `DECLARES_PRODUCER`, `OVERRIDDEN_BY.DECLARES_PRODUCER`, `OVERRIDDEN_BY.EXPOSES`, and `kind == producer` declaring-method hint. No ontology or re-index change.

**What stayed unchanged from the first draft**

- §1 frame statement; §2 principles 1–8; §3.1 field shape; §3.2 generation contract; §5 "deliberately does NOT do" table; §8 risks table.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package orolla.abstractproducer;

public abstract class AbstractProducerApi {
public abstract void publish();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package orolla.abstractproducer;

import org.springframework.kafka.core.KafkaTemplate;

public class ConcreteProducerApi extends AbstractProducerApi {
private final KafkaTemplate<String, String> kafkaTemplate;

public ConcreteProducerApi(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}

@Override
public void publish() {
kafkaTemplate.send("orders", "payload");
}
}
Loading
Loading