add cross-service resolution mode flag for pass6 matching#30
Conversation
Introduce `cross_service_resolution` (auto|brownfield_only), persist it in GraphMeta, and gate pass6 cross-service promotion so brownfield-only mode keeps only fully brownfield-sourced edges. Co-authored-by: Cursor <cursoragent@cursor.com>
Review: PR-G1 —
|
| Sentinel | Status |
|---|---|
FEIGN_CLIENT |
✅ 0 hits — no leakage from PR-H1 |
HTTP_CLIENT |
✅ 0 hits — no capability work |
_TYPE_ANN_TO_CAPABILITY |
✅ 0 hits — ast_java.py capability table untouched |
EXPOSES / feign_not_an_exposer |
✅ 0 hits — no leakage from PR-F1 |
incremental.rebuild / rebuild_partial |
✅ 0 hits — Tier-2 untouched |
@mcp.tool registrations |
✅ 0 new tools — surfaced via existing graph_meta |
REST_CONTROLLER |
✅ 0 hits — controller vocabulary untouched |
| New annotation types | ✅ 0 — reuses existing @CodebaseRoute / @CodebaseClient |
Plan compliance
| # | Step from plan | Verified |
|---|---|---|
| 1 | _load_config_cross_service_resolution reader, mirrors _load_config_microservice_roots |
✅ graph_enrich.py:147 — same caching/error-handling/warn-and-fall-back |
| 2 | cross_service_resolution: str = "auto" field on GraphTables |
✅ build_ast_graph.py:258 |
| 3 | Field populated at the three load_brownfield_overrides call sites |
✅ build_ast_graph.py:1281, 1417, 2002 |
| 4 | _is_brownfield_sourced helper near pass6_match_edges |
✅ build_ast_graph.py:1655-1676 with both-sides-must-be-brownfield logic and defensive empty-candidates guard |
| 5 | Gate inside both HTTP and async loops of pass6_match_edges |
✅ build_ast_graph.py:1746-1753 (HTTP) and 1791-1798 (async); identical structure |
| 6 | GraphMeta schema extended with cross_service_resolution STRING |
✅ build_ast_graph.py:1878 |
| 7 | ONTOLOGY_VERSION 7 → 8 with phase-comment update |
✅ ast_java.py:74 (with "Phase 6" comment) |
| 8 | KuzuGraph.meta() extended with None fallback for old graphs |
✅ kuzu_queries.py:350 (full mode) + :426 (default to None when not in tier) — the existing tiered _META_FULL/_META_PRE_E3/_META_LEGACY fallback chain absorbs the new column cleanly |
| 9 | Verbose log line per propose §2.5 (count + first 5 examples) | ✅ build_ast_graph.py:1822-1830 — capped at 5 |
| 10 | README brownfield section updated | ✅ Mentions ontology_version 8, both modes, the strict both-sides rule, and the rebuild requirement |
Tests
274 passed, 4 skipped in 101.29s
Plan target was 280 = 266 baseline + 8 (this PR) + 6 (PR-F1, not yet shipped). Math: 266 + 8 = 274 ✅. The full +14 target lands once PR-F1 merges.
All 8 plan-enumerated tests are present in tests/test_cross_service_resolution_flag.py (273 LOC) and pass in 1.03s on their own.
Manual evidence reproduced
Auto mode (default, on the cross_service_smoke fixture):
[pass6] http_match={'ambiguous': 1, 'cross_service': 1, 'intra_service': 1, 'phantom': 1, 'unresolved': 1},
async_match={'cross_service': 1}, cross_service_calls_total=2
brownfield_only mode (after dropping .lancedb-mcp.yml with the flag):
[pass6] cross_service_resolution=brownfield_only:
0 cross_service edges from brownfield layers,
2 auto-cross-service candidates suppressed -> unresolved
[pass6] http_match={'ambiguous': 1, 'intra_service': 1, 'phantom': 1, 'unresolved': 2},
async_match={'unresolved': 1}, cross_service_calls_total=0
meta() round-trip:
auto: 'auto'
bo: 'brownfield_only'
Determinism: ✅ two consecutive auto-mode builds produced identical sorted route IDs (9/9 match).
Notes that earned my trust
- Gate symmetry. HTTP and async loops use identical 4-line guard blocks. No copy-paste drift in the
suppressed_auto_cross_countorsuppressed_auto_crossaccumulator handling. - Defensive empty-candidates guard.
_is_brownfield_sourcedreturnsFalsewhencandidatesis empty — this is the correct (and conservative) interpretation when both sides are unknown, even though_match_call_edgerarely returnscross_servicewith empty candidates today. - Reset of
cross_service_calls_totalat the top of pass6. Future-proofs against incremental rebuild (Tier-2). The inline comment makes the intent explicit, which is exactly the right note for a follow-up implementer. - Tiered
meta()fallback absorbed the new column without a new tier — the existing_META_PRE_E3/_META_LEGACYchain does the work, and old graphs returnNonevia themeta_mode == "pre_e3"branch's lack of column read. - Test add brownfield route overrides and codebase route composition (PR-A3) #8 (
test_brownfield_client_with_auto_route_does_not_match) explicitly exercises the asymmetric case the propose [TBD-2] resolved — this is the test that protects the "both sides must be brownfield" decision against future relaxation by accident. - README update documents the rebuild requirement and the strict both-sides rule prominently. Users who upgrade and skip rebuild will read it immediately because it sits inside the brownfield section.
Observations (non-blocking)
_load_config_cross_service_resolutionredefines yaml import locally (graph_enrich.py:158). The sibling_load_config_microservice_rootsdoes the same, so this is faithful mirroring — not a regression. If yaml is ever promoted to a top-level import in this file, both should change together.suppressed_auto_crossaccumulator is shared between HTTP and async loops. This means the "first 5 examples" line could end up mixing channels. Probably fine, but if a future debug session needs to know which channel a suppressed FQN came from, splitting intosuppressed_auto_cross_http/suppressed_auto_cross_asyncis a 4-LOC follow-up.getattr(c, "source_layer", "builtin")in_is_brownfield_sourced— the default"builtin"is correct (it's not in_BROWNFIELD_LAYERS, so it short-circuits toFalse), butRouteRow.source_layeris a declared field on master, so thegetattrdefensive default is dead code today. Leave it for forward compat with synthetic phantoms.- Test Document Kuzu MAP-as-STRING pattern in plan + prompts #6 inlines a 26-line
CREATE NODE TABLE GraphMeta(...)schema for the legacy fixture. If the schema changes again (e.g. PR-F1 adds another column), this test will need to be updated. Acceptable trade-off — it's documenting the "what does the v7 schema look like" contract for the fallback path. - No CLI override (
--cross-service-resolution=...) per plan §Out-of-scope. Confirmed — only YAML reads it. Listed as follow-up AST by Opus #1.
Plan deltas needed
None. The plan and PR are in lockstep.
Ready to merge. Next: PR-F1 (Feign-not-an-exposer) per plans/PLAN-FEIGN-NOT-AN-EXPOSER.md. Once both F1 and G1 are in, PR-H1 (CLIENT role rename, propose merged in #28) can land on top with ontology 8→9.
Keep brownfield-only suppression counts unchanged but log separate first-five examples for HTTP and async to make pass6 debugging clearer. Co-authored-by: Cursor <cursoragent@cursor.com>
…ty (#32) Implements propose merged in #28. Sequences after PR-F1 (#31, merged) and PR-G1 (#30, merged). Hard rename — no deprecation alias, MCP bundle has no users yet and breaking changes are explicitly allowed. Single PR, ~115 LOC, ontology bump 8→9. Single source-of-truth flips at ast_java.py:91 (role) and ast_java.py:114 (HTTP_CLIENT capability); VALID_ROLES / VALID_CAPABILITIES are auto-derived so no java_ontology.py edit needed. 9 new tests in tests/test_client_role_rename.py target ~290 passed, 4 skipped (281 baseline + 9). Plan delta from propose: test #4 asserts warn-and-drop behaviour (stderr warning + override silently dropped) matching actual graph_enrich.py:443-447, 481-486, NOT raised ValueError as the propose's example suggested. Out of scope: async role/capability changes (MESSAGE_PRODUCER already covers Kafka/Rabbit/JMS), auto-promoting RestTemplate/WebClient to HTTP_CLIENT (brownfield-only opt-in), backwards-compat alias, RegisterRestClient (followup #3).
* chore: tidy completed plans/proposes and refresh stale docs Move completed plans to plans/completed/: - PLAN-CLIENT-ROLE-RENAME.md (PR #33 merged) - PLAN-CROSS-SERVICE-RESOLUTION-FLAG.md (PR #30 merged) - PLAN-FEIGN-NOT-AN-EXPOSER.md (PR #31 merged) Move completed proposes to propose/completed/: - CLIENT-ROLE-RENAME-PROPOSE.md (PR #28 merged) - CROSS-SERVICE-RESOLUTION-FLAG-PROPOSE.md (PR #26 merged) - FEIGN-NOT-AN-EXPOSER-PROPOSE.md (PR #25 merged) Refresh active docs: - README.md "Deferred" section: trace_request_flow, find_route_callers, HTTP_CALLS/ASYNC_CALLS are shipped (not deferred). Add explicit pointers to the still-active TIER2-INCREMENTAL-REBUILD and REFRESH-CODE-INDEX-AUTO-MODE proposes for the incremental Kuzu work. - CODEBASE_REQUIREMENTS.md A.3: drop the stale 'ontology version 3' literal (now 9) and fix references to PLAN-CAPABILITIES-MODEL and CALL-GRAPH-PROPOSE to use their completed/ paths. Tense matches reality (call-graph layer is shipped, not deferred). - CODEBASE_REQUIREMENTS.md B.9: same fix for the propose/DEFERRED-CALL-GRAPH-PROPOSE.md reference; the propose lives under propose/completed/CALL-GRAPH-PROPOSE.md. No code changes. Test baseline unchanged: 290 passed, 4 skipped. * docs: add inline Java stubs for @CodebaseRoute / @CodebaseClient / @CodebaseProducer Per pushback on PR #34: the route, client, and producer brownfield annotations were mentioned 4x in README + CODEBASE_REQUIREMENTS but their @interface stubs were never shown inline. Users had to spelunk through tests/fixtures/ to know what to copy into their project. README §5 'Brownfield overrides — Last resort — source stubs' now has three explicit subsections: - 3a. Roles & capabilities — @CodebaseRole / @CodebaseCapability / @CodebaseCapabilities (class-level), with usage example. - 3b. Routes — @CodebaseRoute / @CodebaseRoutes + CodebaseRouteFrameworkKind / CodebaseRouteKind (method-level), with HTTP-endpoint and Kafka-consumer usage examples. - 3c. Clients & producers — @CodebaseClient / @CodebaseClients and @CodebaseProducer / @CodebaseProducers (method-level), with rest_template + kafka_send usage examples. Stub Java in the doc matches the verbatim sources under tests/fixtures/brownfield_route_stubs/ and brownfield_client_stubs/ (also referenced for copy-paste). Enum values mirror VALID_ROUTE_* and VALID_CLIENT_KINDS in java_ontology.py. CODEBASE_REQUIREMENTS.md A.2.1 updated to enumerate all three annotation families (roles, routes, clients/producers) and link to the matching README sections instead of only mentioning role stubs. No code change. Test baseline unchanged: 290 passed, 4 skipped.
Summary
.lancedb-mcp.ymlreader forcross_service_resolution(auto/brownfield_only) and thread the mode into graph build tablesbrownfield_onlyso only fully brownfield-sourced caller+route matches remain cross-service, with suppression loggingKuzuGraph.meta()/ MCPgraph_meta, bump ontology to 8, and document the new config in READMEmeta()fallbackTest plan
ruff check .pytest tests/test_cross_service_resolution_flag.py tests/test_call_edge_matching.py -v --tb=shortpytest tests -q --tb=linepytest tests/test_mcp_tools.py -q --tb=line -k graph_metaMade with Cursor