Skip to content

PR-A1: Route schema + literal extractor (B2a)#5

Merged
HumanBean17 merged 1 commit into
masterfrom
feat/b2a-route-schema
May 4, 2026
Merged

PR-A1: Route schema + literal extractor (B2a)#5
HumanBean17 merged 1 commit into
masterfrom
feat/b2a-route-schema

Conversation

@HumanBean17

@HumanBean17 HumanBean17 commented May 4, 2026

Copy link
Copy Markdown
Owner

Scope

Implements PR-A1 from plans/PLAN-TIER1-COMPLETION.md / plans/CURSOR-PROMPTS-TIER1.md: Route nodes, EXPOSES edges ((Symbol)-[:EXPOSES]->(Route)), literal-only annotation extraction, pass4_routes after pass3_calls, ONTOLOGY_VERSION 4 → 5.

Behaviour

  • Spring MVC/WebFlux (literal paths), Feign (literal name/url/path), Kafka/Rabbit/JMS/Stream listeners; SpEL ${…} inside strings and non-string path args increment routes_skipped_unresolved and emit no route for that value.
  • _normalize_path / _route_id in build_ast_graph.py (deterministic, shared contract for later B2b/B6).
  • graph_meta: routes_total, exposes_total, routes_by_framework (JSON string column — Kuzu Python MAP binding limitation), routes_resolved_pct.
  • MCP graph_meta extended with the same route fields.
  • Fix: pass1_parse now computes rel before parse_java(..., filename=rel) (was referencing undefined rel).

Evidence (bank-chat-system)

Route extraction: emitted=9, exposes=9, skipped_unresolved=2, routes_resolved_pct=100.0, by_framework={'spring_mvc': 9}
ontology_version=5, routes_total=9

Tests

  • pytest tests: 170 passed, 3 skipped (default; no heavy gate).
  • ruff check clean on touched paths.

Not in this PR

SpEL/constant-ref routes as nodes (PR-A2), brownfield (PR-A3), MCP route listing tools (PR-A2), HTTP_CALLS / ASYNC_CALLS (B2b).

Ambiguities / plan deltas to confirm

  • GraphMeta routes_by_framework: The plan showed MAP(STRING, INT64). Kuzu’s Python binder rejected a Python dict for that parameter (STRUCT() vs MAP). The implementation uses a STRING JSON blob instead (documented next to the schema in build_ast_graph.py). Consumers (meta(), MCP graph_meta) decode it to a dict.
  • @Bean Function routes: Emitted as stream_binding / stream with empty topic when no @Input / @Output metadata is parsed — enough for PR-A1 smoke; refine if richer binding metadata is needed later.

Made with Cursor

@HumanBean17

Copy link
Copy Markdown
Owner Author

✅ Approved — ready to merge

Reviewed against plans/PLAN-TIER1-COMPLETION.md PR-A1 § and the prompt's "Do not" / "Hard requirements" lists. Reproduced the manual evidence locally.

Scope discipline (all clean)

  • No leak from later PRs. grep on the diff for HTTP_CALLS|ASYNC_CALLS|RouteHint|route_overrides|CodebaseRoute|resolve_routes_for_method|list_routes|find_route_handlers|get_route_by_pathzero hits. None of B2b, A2, or A3 surface area was touched.
  • EXPOSES direction. (Symbol)-[:EXPOSES]->(Route) — locked as planned. Test test_exposes_edge_direction proves the reverse traversal returns 0 rows.
  • Ontology bump 4 → 5 in ast_java.py; consumer in tests/test_ast_graph_build.py asserts int(ov) == ONTOLOGY_VERSION.

Schema

_SCHEMA_ROUTE and _SCHEMA_EXPOSES match the plan byte-for-byte — every column name, type, and primary key.

Helpers (reusable contract for B2b/B6)

  • _normalize_path is deterministic, handles {id:\d+} constraint stripping, trailing-slash collapse, multi-{} paths.
  • _route_id SHA1 hash includes microservice — same path in svc-a and svc-b produces two distinct ids (proven by test_route_id_includes_microservice).

Tests

All 11 plan-numbered cases present in tests/test_route_extraction.py (test_case1test_case11) plus the 3 extension tests in tests/test_ast_graph_build.py. 57/57 pass locally on PR-A1 vs tests/test_route_extraction.py + test_ast_graph_build.py + test_kuzu_queries.py.

The 21 errors + 1 fail you may see in a fresh sandbox come from mcp package not being installed (from mcp.server.fastmcp import FastMCP); same on master, not introduced by this PR.

Manual evidence — reproduced

[pass4] Route extraction: emitted=9, exposes=9, skipped_unresolved=2,
         routes_resolved_pct=100.0, by_framework={'spring_mvc': 9}

graph_meta round-trip via Cypher:

ontology_version : 5
routes_total     : 9
exposes_total    : 9
routes_by_fw json: {'spring_mvc': 9}
routes_resolved% : 100.0

All numbers match the PR description.

Bonus catch worth calling out

pass1_parse was using rel on the line before it was assigned — a latent NameError on the parse-error path. The PR hoists rel = ... ahead of parse_java(content, filename=rel). Quiet good fix, even though it's outside the PR-A1 scope.

Plan deltas — both acceptable

  1. routes_by_framework is STRING (JSON blob), not MAP(STRING, INT64) as the plan specified. Reason: Kuzu's Python binder rejects dict for MAP. Mitigated by:

    • Inline comment at the schema explaining the mismatch.
    • meta() decodes the blob to a dict so MCP consumers see what the plan promised.
    • _META_LEGACY query path so v4 graphs still read.

    Recommend: keep this delta. Document it in the B2b plan if/when we add client_kind counters that would face the same Kuzu issue.

  2. @Bean Function routes emit stream_binding with empty topic when no @Input/@Output metadata is parsed. PR description flags this honestly. Acceptable for B2a (declarations only); B2b's matcher already needs to handle empty-topic routes as unresolved, so no new contract violation.

Nothing to fix before merging

The routes_by_framework JSON-blob choice is the only thing future PRs need to be aware of (PR-A2's MCP list_routes filter and PR-A3's routes_from_brownfield_pct will need to follow the same pattern). Worth a one-line addendum to the plan's PR-A2 / PR-A3 sections after this merges.

LGTM. 🟢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant