Skip to content

Commit a2f2697

Browse files
address PR #164 review: e2e producer override coverage and doc sync
Extend override_axis_rollup_smoke with abstract/concrete Kafka producer override; replace synthetic producers-in-overriders hint test with describe_v2 e2e. Amend HINTS-ROAD-SIGNS Appendix A and AGENT-GUIDE for DECLARES_PRODUCER / OVERRIDDEN_BY.DECLARES_PRODUCER parity. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4ef1c2c commit a2f2697

7 files changed

Lines changed: 72 additions & 16 deletions

File tree

docs/AGENT-GUIDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,13 @@ Exact allowed values for roles, capabilities, client kinds, etc. live in `java_o
225225

226226
#### `describe`
227227

228-
- **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).
228+
- **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).
229229
- **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.
230230

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

233233
- `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"])`.
234+
- `DECLARES.DECLARES_PRODUCER` — the type's methods declare async producers. Same walk shape with `DECLARES_PRODUCER`.
234235
- `DECLARES.EXPOSES` — the type's methods expose routes. Same walk shape with `EXPOSES`.
235236

236237
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.
@@ -240,7 +241,7 @@ Note on counting semantics: composed counts measure **edge rows**, not distinct
240241
**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):
241242

242243
- `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).
243-
- `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.
244+
- `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.
244245
- `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.
245246

246247
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.

mcp_hints.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Pure MCP v2 road-sign hint generation (no graph I/O, no search, no LLM).
22
3-
Locked v1 catalog: ``propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`` Appendix A.
3+
Locked v1 catalog: ``propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`` Appendix A
4+
(issue #161 producer/override-route amendments in that appendix).
45
v2 resolve + neighbors fuzzy-strategy catalog: ``propose/completed/HINTS-V2-PROPOSE.md`` Appendix A.
56
v3 empty-neighbors structural catalog: ``propose/completed/HINTS-V3-PROPOSE.md`` §3.1–3.3.
67
Priority cap: same propose §7.12 / ``plans/completed/PLAN-HINTS.md`` principles.

propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,18 @@ The §3.3 table *is* the appendix artifact — every row is a verbatim template
250250
# DescribeOutput
251251
DECLARES.DECLARES_CLIENT.out>0 → "clients via members: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'out',['DECLARES_CLIENT'])"
252252
DECLARES.EXPOSES.out>0 → "routes via members: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'out',['EXPOSES'])"
253+
DECLARES.DECLARES_PRODUCER.out>0 → "producers via members: neighbors(['{id}'],'out',['DECLARES']) then neighbors(member_ids,'out',['DECLARES_PRODUCER'])" # amendment #161
253254
OVERRIDDEN_BY.out>0 → "overriders: neighbors(['{id}'],'in',['OVERRIDES'])" # requires PR-A; rollup stores counts on .out per override_axis_rollup_for
254255
OVERRIDDEN_BY.DECLARES_CLIENT.out>0 → "clients in overriders: neighbors(['{id}'],'in',['OVERRIDES']) then neighbors(overrider_ids,'out',['DECLARES_CLIENT'])" # requires PR-A
256+
OVERRIDDEN_BY.DECLARES_PRODUCER.out>0 → "producers in overriders: neighbors(['{id}'],'in',['OVERRIDES']) then neighbors(overrider_ids,'out',['DECLARES_PRODUCER'])" # amendment #161
257+
OVERRIDDEN_BY.EXPOSES.out>0 → "routes in overriders: neighbors(['{id}'],'in',['OVERRIDES']) then neighbors(overrider_ids,'out',['EXPOSES'])" # amendment #161
255258
DECLARES_CLIENT.out>0 (method) → "outbound client: neighbors(['{id}'],'out',['DECLARES_CLIENT'])"
259+
DECLARES_PRODUCER.out>0 (method) → "outbound producer: neighbors(['{id}'],'out',['DECLARES_PRODUCER'])" # amendment #161
256260
EXPOSES.out>0 (method) → "inbound route: neighbors(['{id}'],'out',['EXPOSES'])"
257261
CALLS.out>=10 (method) → "many CALLS — consider filtering by target microservice"
258262
kind == route, always → "declaring method: neighbors(['{id}'],'in',['EXPOSES'])"
259263
kind == client, always → "declaring method: neighbors(['{id}'],'in',['DECLARES_CLIENT'])"
264+
kind == producer, always → "declaring method: neighbors(['{id}'],'in',['DECLARES_PRODUCER'])" # amendment #161
260265
261266
# FindOutput
262267
results==[] and filter has identifier-shaped value → "no matches — try resolve(identifier, hint_kind='{kind}') for canonical lookup"
@@ -273,6 +278,8 @@ File placement (`mcp_hints.py`), function decomposition, integration points in `
273278

274279
## Appendix B — What changed (traceability)
275280

281+
**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.
282+
276283
**What stayed unchanged from the first draft**
277284

278285
- §1 frame statement; §2 principles 1–8; §3.1 field shape; §3.2 generation contract; §5 "deliberately does NOT do" table; §8 risks table.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package orolla.abstractproducer;
2+
3+
public abstract class AbstractProducerApi {
4+
public abstract void publish();
5+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package orolla.abstractproducer;
2+
3+
import org.springframework.kafka.core.KafkaTemplate;
4+
5+
public class ConcreteProducerApi extends AbstractProducerApi {
6+
private final KafkaTemplate<String, String> kafkaTemplate;
7+
8+
public ConcreteProducerApi(KafkaTemplate<String, String> kafkaTemplate) {
9+
this.kafkaTemplate = kafkaTemplate;
10+
}
11+
12+
@Override
13+
public void publish() {
14+
kafkaTemplate.send("orders", "payload");
15+
}
16+
}

tests/test_mcp_hints.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -199,18 +199,19 @@ def test_hints_describe_method_clients_in_overriders_emits(kuzu_graph) -> None:
199199
assert want in out.hints
200200

201201

202-
def test_hints_describe_method_producers_in_overriders_emits() -> None:
203-
node_id = "sym:com.example.T#m()"
204-
rec = {
205-
"id": node_id,
206-
"kind": "symbol",
207-
"fqn": "com.example.T#m()",
208-
"data": {"kind": "method"},
209-
"edge_summary": {"OVERRIDDEN_BY.DECLARES_PRODUCER": {"in": 0, "out": 1}},
210-
}
211-
hints = generate_hints("describe", {"success": True, "record": rec})
212-
want = mcp_hints.TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS.format(id=node_id)
213-
assert want in hints
202+
def test_hints_describe_method_producers_in_overriders_emits(override_axis_graph: KuzuGraph) -> None:
203+
rows = override_axis_graph._rows( # noqa: SLF001
204+
"MATCH (t:Symbol {fqn: $fqn})-[:DECLARES]->(m:Symbol) "
205+
"WHERE m.kind = 'method' AND m.name = 'publish' "
206+
"RETURN m.id AS id LIMIT 1",
207+
{"fqn": "orolla.abstractproducer.AbstractProducerApi"},
208+
)
209+
assert rows
210+
mid = str(rows[0]["id"])
211+
out = describe_v2(mid, graph=override_axis_graph)
212+
assert out.success and out.record
213+
want = mcp_hints.TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS.format(id=mid)
214+
assert want in out.hints
214215

215216

216217
def test_hints_describe_method_routes_in_overriders_emits(override_axis_graph: KuzuGraph) -> None:

tests/test_mcp_v2_compose.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,31 @@ def test_describe_method_no_overrides_silent(kuzu_graph) -> None:
420420
assert "OVERRIDES" not in es
421421

422422

423+
def test_describe_abstract_method_with_producer_override_emits_declares_producer(
424+
override_axis_graph: KuzuGraph,
425+
) -> None:
426+
rows = override_axis_graph._rows( # noqa: SLF001
427+
"MATCH (t:Symbol {fqn: $fqn})-[:DECLARES]->(m:Symbol) "
428+
"WHERE m.kind = 'method' AND m.name = 'publish' "
429+
"RETURN m.id AS id LIMIT 1",
430+
{"fqn": "orolla.abstractproducer.AbstractProducerApi"},
431+
)
432+
assert rows
433+
mid = str(rows[0]["id"])
434+
impl_ids = _dispatch_down_override_method_ids(override_axis_graph, mid)
435+
assert impl_ids
436+
want_ob = len(impl_ids)
437+
want_dp = _edge_row_count_from_methods(override_axis_graph, impl_ids, "DECLARES_PRODUCER")
438+
assert want_dp >= 1
439+
out = describe_v2(mid, graph=override_axis_graph)
440+
assert out.success is True
441+
assert out.record is not None
442+
assert out.record.edge_summary is not None
443+
es = out.record.edge_summary
444+
assert es.get("OVERRIDDEN_BY") == {"in": 0, "out": want_ob}
445+
assert es.get("OVERRIDDEN_BY.DECLARES_PRODUCER") == {"in": 0, "out": want_dp}
446+
447+
423448
def test_describe_abstract_method_with_route_override_emits_exposes(override_axis_graph: KuzuGraph) -> None:
424449
rows = override_axis_graph._rows( # noqa: SLF001
425450
"MATCH (t:Symbol {fqn: $fqn})-[:DECLARES]->(m:Symbol) "

0 commit comments

Comments
 (0)