Skip to content

Commit ed3097d

Browse files
add describe-time producer and override-route hints (#164)
* add describe-time producer and override-route hints Close #161: emit symmetric road-sign hints for DECLARES_PRODUCER, DECLARES.DECLARES_PRODUCER, OVERRIDDEN_BY.DECLARES_PRODUCER, and OVERRIDDEN_BY.EXPOSES rollups plus producer-node declaring-method hint. Co-authored-by: Cursor <cursoragent@cursor.com> * 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> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e6a44a6 commit ed3097d

7 files changed

Lines changed: 179 additions & 4 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: 33 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.
@@ -31,16 +32,30 @@
3132
"routes via members: neighbors(['{id}'],'out',['DECLARES']) "
3233
"then neighbors(member_ids,'out',['EXPOSES'])"
3334
)
35+
TPL_DESCRIBE_TYPE_PRODUCERS_VIA_MEMBERS = (
36+
"producers via members: neighbors(['{id}'],'out',['DECLARES']) "
37+
"then neighbors(member_ids,'out',['DECLARES_PRODUCER'])"
38+
)
3439
TPL_DESCRIBE_METHOD_OVERRIDERS = "overriders: neighbors(['{id}'],'in',['OVERRIDES'])"
3540
TPL_DESCRIBE_METHOD_CLIENTS_IN_OVERRIDERS = (
3641
"clients in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
3742
"then neighbors(overrider_ids,'out',['DECLARES_CLIENT'])"
3843
)
44+
TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS = (
45+
"producers in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
46+
"then neighbors(overrider_ids,'out',['DECLARES_PRODUCER'])"
47+
)
48+
TPL_DESCRIBE_METHOD_ROUTES_IN_OVERRIDERS = (
49+
"routes in overriders: neighbors(['{id}'],'in',['OVERRIDES']) "
50+
"then neighbors(overrider_ids,'out',['EXPOSES'])"
51+
)
3952
TPL_DESCRIBE_METHOD_OUTBOUND_CLIENT = "outbound client: neighbors(['{id}'],'out',['DECLARES_CLIENT'])"
53+
TPL_DESCRIBE_METHOD_OUTBOUND_PRODUCER = "outbound producer: neighbors(['{id}'],'out',['DECLARES_PRODUCER'])"
4054
TPL_DESCRIBE_METHOD_INBOUND_ROUTE = "inbound route: neighbors(['{id}'],'out',['EXPOSES'])"
4155
TPL_DESCRIBE_METHOD_MANY_CALLS = "many CALLS — consider filtering by target microservice"
4256
TPL_DESCRIBE_ROUTE_DECLARING = "declaring method: neighbors(['{id}'],'in',['EXPOSES'])"
4357
TPL_DESCRIBE_CLIENT_DECLARING = "declaring method: neighbors(['{id}'],'in',['DECLARES_CLIENT'])"
58+
TPL_DESCRIBE_PRODUCER_DECLARING = "declaring method: neighbors(['{id}'],'in',['DECLARES_PRODUCER'])"
4459

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

422440
if kind != "symbol":
423441
return finalize_hint_list(pairs)
@@ -435,6 +453,10 @@ def generate_hints(
435453
pairs.append(
436454
(PRIORITY_DECLARES_TYPE_ROLLUP, TPL_DESCRIBE_TYPE_ROUTES_VIA_MEMBERS.format(id=node_id))
437455
)
456+
if _out_count(edge_summary, "DECLARES.DECLARES_PRODUCER") > 0:
457+
pairs.append(
458+
(PRIORITY_DECLARES_TYPE_ROLLUP, TPL_DESCRIBE_TYPE_PRODUCERS_VIA_MEMBERS.format(id=node_id))
459+
)
438460
return finalize_hint_list(pairs)
439461

440462
if is_method:
@@ -444,8 +466,18 @@ def generate_hints(
444466
pairs.append(
445467
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_CLIENTS_IN_OVERRIDERS.format(id=node_id))
446468
)
469+
if _out_count(edge_summary, "OVERRIDDEN_BY.DECLARES_PRODUCER") > 0:
470+
pairs.append(
471+
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_PRODUCERS_IN_OVERRIDERS.format(id=node_id))
472+
)
473+
if _out_count(edge_summary, "OVERRIDDEN_BY.EXPOSES") > 0:
474+
pairs.append(
475+
(PRIORITY_OVERRIDDEN_AXIS, TPL_DESCRIBE_METHOD_ROUTES_IN_OVERRIDERS.format(id=node_id))
476+
)
447477
if _out_count(edge_summary, "DECLARES_CLIENT") > 0:
448478
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_OUTBOUND_CLIENT.format(id=node_id)))
479+
if _out_count(edge_summary, "DECLARES_PRODUCER") > 0:
480+
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_OUTBOUND_PRODUCER.format(id=node_id)))
449481
if _out_count(edge_summary, "EXPOSES") > 0:
450482
pairs.append((PRIORITY_LEAF_FOLLOWUP, TPL_DESCRIBE_METHOD_INBOUND_ROUTE.format(id=node_id)))
451483
if _out_count(edge_summary, "CALLS") >= 10:

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+
}

0 commit comments

Comments
 (0)