add Client graph node and DECLARES_CLIENT edge (PR-LC1)#40
Conversation
Introduce first-class outbound Client nodes with DECLARES_CLIENT edges, persist client counters in GraphMeta, and bump ontology_version to 10 so pass5 outputs and metadata stay deterministic for list_clients follow-up work. Co-authored-by: Cursor <cursoragent@cursor.com>
Review: PR-LC1 —
|
| Sentinel | Status |
|---|---|
list_clients (LC3 tool name) |
✅ 0 hits |
@mcp.tool registration |
✅ 0 hits |
ClientRowDto / ClientsListOutput |
✅ 0 hits |
find_route_callers regression coverage (LC2 territory) |
✅ 0 hits |
test_pass6_* (LC2 test bucket) |
✅ 0 hits |
pass6 references |
✅ 2 hits — both are fixture-builder import + invocation, no pass6 logic changes |
test_list_clients |
✅ 0 hits |
LC2 (pass6 hint retarget) and LC3 (MCP surface + DTOs + query helper) territory is untouched. This is exactly the "schema + extraction + persistence" scope the plan calls out for LC1.
Plan compliance
| # | Plan item | Verified |
|---|---|---|
| 1 | Client node DDL with all 16 fields |
✅ build_ast_graph.py:218-225 — every field in plan §1 present, PRIMARY KEY(id) |
| 2 | DECLARES_CLIENT(FROM Symbol TO Client, confidence, strategy) |
✅ build_ast_graph.py:234-237 |
| 3 | Create/drop lifecycle wired | ✅ DROP TABLE IF EXISTS Client and DECLARES_CLIENT present in _drop_all |
| 4 | Row dataclasses + GraphTables collections |
✅ ClientRow, DeclaresClientRow dataclasses + client_rows, declares_client_rows on tables |
| 5 | Graph-meta counters using STRING-JSON pattern | ✅ clients_total INT64, declares_client_total INT64, clients_by_kind STRING (json.dumps(clients_by_kind)) — matches Tier1 MAP-as-STRING convention |
| 6 | ast_java.py extraction returns outbound payloads |
✅ call.client_kind, call.method_fqn, call.feign_target_name, etc. consumed in pass5 emitter |
| 7 | Feign method synthesis without explicit @CodebaseClient |
✅ pass5 branch keys off call.client_kind == "feign_method" (line 185), uses call.feign_target_name for target_service |
| 8 | resolve_http_client_for_method composition feeds Client rows |
✅ via call.resolution_strategy → _client_source_layer |
| 9 | source_layer stamped from winning strategy |
✅ _client_source_layer whitelist {layer_a_meta, layer_b_ann, layer_b_fqn, layer_c_source}, fallback builtin |
| 10 | Ontology bump 9 → 10 | ✅ ast_java.py:ONTOLOGY_VERSION = 10 + Phase 8 comment line |
| 11 | All six plan-mandated tests | ✅ all present with exact names: test_client_rows_emitted_for_codebase_client_annotations, test_client_rows_synthesized_for_feign_methods, test_declares_client_edge_targets_client_id, test_client_id_is_deterministic_across_rebuilds, test_client_source_layer_reflects_winning_override_layer, test_client_schema_persisted_and_queryable |
| 12 | README ontology v10 reindex callout | ✅ new bullet at README.md:120-122; glossary line v9 → v10 at README.md:19 |
Deterministic id contract
def _client_id(*, microservice, member_fqn, client_kind, path, method) -> str:
key = f"{microservice}|{member_fqn}|{client_kind}|{path}|{method}"
return f"c:{hashlib.sha1(key.encode()).hexdigest()[:16]}"Five-tuple matches the propose verbatim. c: prefix is symmetric to the existing r: Route id convention. test_client_id_is_deterministic_across_rebuilds test-locks this. ✅
Source-layer stamping correctness
The _client_source_layer test (test_client_source_layer_reflects_winning_override_layer) is the highest-signal test in the suite — it sets up a YAML http_client_overrides block and a conflicting @CodebaseClient annotation in source, then asserts the YAML wins (layer_b_fqn) and overrides the source path/target. This is the right direction to test (YAML over source); it confirms the resolver-composition output is what's persisted, not just whichever row was emitted first.
Notes that earned my trust
- Dedup keys for
DECLARES_CLIENT:dkey = (member.node_id, cid)withdeclares_client_seenset — prevents a single member declaring the sameClientrow twice (two annotations producing identical_client_id). Defensive and cheap. source_layerwhitelist with explicitbuiltinfallback rather than passing throughcall.resolution_strategyraw — guards against future strategy strings leaking into the persisted enum.clients_by_kindsorted beforejson.dumps— preserves determinism in the meta blob across rebuilds.- Feign synthesis reuses
call.feign_target_nameinstead of re-deriving from interface annotation — single source of truth, no parallel resolver. member_idlinkage stored onClientrow directly (not justmember_fqn) — gives LC2's pass6 query a fast indexable path without needing aSymbol-side join.
Tests
Skipping local test verification per your instruction. PR description claims 298 passed, 4 skipped — that's +8 over the post-PR-#38 baseline of 290/4, which exactly matches the six new test_client_node_extraction.py tests plus the two new schema-table assertions (Client, DECLARES_CLIENT) added to test_ast_graph_build.py::test_schema_has_all_expected_tables. Math checks out.
Observations (non-blocking)
_CREATE_CLIENTCypher uses positional-style$paramtemplate strings, but the param map in_write_kuzuis built dynamically with all 16 keys hand-listed. Consistent with how_CREATE_ROUTEis handled today, but a future helper that derives the param dict from the dataclass viaasdict()would prevent drift if the schema grows. Not a blocker — same pattern as Route.path_templateandpath_regexare populated forClientrows but not yet consumed by anything in this PR — they're forward-compatibility for path-prefix filtering in LC3. Worth a one-line code comment so a future reader doesn't assume they're dead fields. Defer to LC3 if it adds the filter.microservice STRINGforClient.target_service— plan resolved this as "keep STRING for v1" (line 50), and the implementation matches. No action; calling it out so the decision stays visible.builtinsource_layer label — when none of the four tracked layers wins, the row is stampedbuiltin. There's a small risk this masks a 5th unforeseen strategy in the future. A# pragma: no coveror explicit assertion path could help; today the test suite covers all four expected branches but not the fallthrough.- Phase 8 comment in
ast_java.pysays "first-class Client node + DECLARES_CLIENT relation" — accurate, but doesn't mention "ontology v10 also reserves outbound declarations independently fromRoute". Minor wording polish, not blocking.
Plan deltas needed
None. LC1 done-criteria are all met:
- ✅
ClientandDECLARES_CLIENTtables exist and are populated - ✅ Ontology version bumped 9 → 10 and reflected in metadata
- ✅ Deterministic id contract implemented and test-locked
- ✅ Full tests green (per PR description)
Ready to merge. Next: PR-LC2 — pass6 hint recovery retarget from transient route hints to persisted Client rows. The member_id field stamped on Client here will give LC2 the indexable join key it needs.
Use dataclass serialization for Client inserts, add defensive unknown strategy logging, and clarify outbound Client path/source-layer intent in comments to keep LC1 persistence easier to maintain. Co-authored-by: Cursor <cursoragent@cursor.com>
Scope
Implements PR-LC1 from
plans/PLAN-LIST-CLIENTS-MCP-TOOL.md.This PR introduces first-class outbound client declarations in the graph layer by adding:
Clientnode tableDECLARES_CLIENT (Symbol -> Client)relationClient.idgeneration9to10Why
After the brownfield annotations v2 split, outbound declarations must be represented independently from inbound
Routerows. This PR establishes the persistence/model foundation needed for follow-up work (pass6hint migration in LC2 and MCPlist_clientssurface in LC3).Changes
Graph schema and persistence (
build_ast_graph.py)Clientnode DDL andDECLARES_CLIENTrelation DDL.GraphTablescollections for client rows and declaration edges.resolve_http_client_for_method(...)output.source_layerbased on winning strategy (layer_a_meta,layer_b_ann,layer_b_fqn,layer_c_source, otherwisebuiltin).c:<sha1[:16]>) from(microservice, member_fqn, client_kind, path, method).GraphMetaclient counters and by-kind JSON map.Ontology/versioning (
ast_java.py)ONTOLOGY_VERSIONto10.Schema assertions (
tests/test_ast_graph_build.py)ClientandDECLARES_CLIENT.LC1 tests (
tests/test_client_node_extraction.py)DECLARES_CLIENTtarget linkageDocs (
README.md)Client/DECLARES_CLIENTand graph meta counters.Reindex / Compatibility
9 -> 10.Validation
Lint
ruff check .All checks passed!Tests
pytest tests/test_client_node_extraction.py -q6 passedpytest tests/test_ast_graph_build.py::test_schema_has_all_expected_tables -q1 passedpytest tests -v298 passed, 4 skipped in 249.66s (0:04:09)Follow-ups
Clientrows.list_clientsMCP tool and query helper/docs.Made with Cursor