Skip to content

propose: @FeignClient is a caller, not an exposer#25

Merged
HumanBean17 merged 2 commits into
masterfrom
propose/feign-not-an-exposer
May 5, 2026
Merged

propose: @FeignClient is a caller, not an exposer#25
HumanBean17 merged 2 commits into
masterfrom
propose/feign-not-an-exposer

Conversation

@HumanBean17
Copy link
Copy Markdown
Owner

What

Adds propose/FEIGN-NOT-AN-EXPOSER-PROPOSE.md — a draft proposal to fix the design issue where @FeignClient methods (HTTP callers) are emitted as if they also exposed the endpoints they call.

TL;DR for the design issue

Verified on tests/fixtures/cross_service_smoke:

Routes for /chat/joinOperator:
  ms=svc-a  fw=feign     kind=http_consumer  id=r:2350f45b...
  ms=svc-b  fw=spring_mvc kind=http_endpoint  id=r:64685ba1...

EXPOSES edges to /chat/joinOperator:
  svc-a::BFeignClient#joinOperator()    -> consumer Route   ❌ wrong
  svc-b::JoinControllerB#joinOperator() -> endpoint Route   ✅ correct

HTTP_CALLS:
  svc-a::BFeignClient#joinOperator() -> r:2350f45b... (svc-a's own consumer Route)
                                        match=intra_service                       ❌ should be cross_service to r:64685ba1...

So one HTTP wire produces:

  • ✅ correct EXPOSES from svc-b's controller
  • ❌ extra incorrect EXPOSES from svc-a's Feign caller
  • ❌ HTTP_CALLS landing on the wrong Route, mislabeled as intra_service

How the bug exists despite the annotation literally saying "client"

Not a typo — a design assumption that didn't generalise: "any method with a @*Mapping annotation declares a Route, and any method declaring a Route gets an EXPOSES edge."

ast_java.py:2178 already correctly distinguishes the two cases:

kind = "http_consumer" if feign_iface else "http_endpoint"

But build_ast_graph.py:1354-1364 emits EXPOSES unconditionally per RouteDecl, regardless of kind. The semantic distinction was made; it's just not honoured at emission. One conditional fixes it.

What's in the proposal

  • §1 — current state with the verified failure-mode reproduction (5 wrong-answer queries)
  • §2 — the fix: 5 characters (and decl.kind != "http_consumer") plus matcher extension to pair consumer Routes against endpoint Routes
  • §3 — five risks: tests asserting the wrong behaviour, downstream tools depending on the bug, match label shifts, third-party services, older graph compat
  • §4 — verification: counter deltas on cross_service_smoke, full inertness on bank-chat-system (zero Feign there), determinism, new fixture
  • §5 — single-PR scope (~150 LOC), DoD: 271 passed (266 + ~5 new tests)
  • §9 — six [TBD] items, including a recommendation to audit ASYNC_CALLS for the symmetric bug before implementation

What's NOT in the proposal

  • Removing or generalising the FEIGN_CLIENT role — separate larger discussion (§8 Future work)
  • HTTP_CLIENT as a generalised role
  • Schema bump (none needed — change is in which edges get emitted)
  • Touching RestTemplate / WebClient brownfield code

Status

Draft — open for review. The user wants this queued; not implementing now. After review and §9 [TBD] resolution, an implementable plan derives at plans/PLAN-FEIGN-NOT-AN-EXPOSER.md.

Sibling proposal: #24 (shared-module multi-attribution). Both touch the cross-service edge story but on independent axes — multi-attribution fixes shared-DTO false-positives in CALLS/EXTENDS/IMPLEMENTS/INJECTS; this proposal fixes wrong-direction edges in EXPOSES/HTTP_CALLS.

Reported-by: review on cross_service_smoke

A @FeignClient method is a client — it makes outbound HTTP calls.
But the indexer emits an EXPOSES edge from every Feign method to a
consumer-side Route node, treating the caller as if it also exposed
the endpoint. The same wire ends up represented as two unrelated
Route nodes (one consumer-side in the caller, one endpoint-side in
the server) and the HTTP_CALLS edge lands on the wrong one,
producing intra_service match labels for what are really
cross-service calls.

The root cause: ast_java.py:2178 already distinguishes
kind=http_consumer (Feign) from kind=http_endpoint (Controller),
but build_ast_graph.py:1354-1364 emits EXPOSES unconditionally per
RouteDecl. The semantic distinction was made; it's just not
honoured at emission.

Single-PR fix (~150 LOC): suppress EXPOSES emission when
kind=http_consumer, extend the cross-service matcher to pair
consumer Routes against endpoint Routes across microservices,
add pass4_feign_exposes_suppressed counter to graph_meta with
new _META_PR_FEIGN tier.

Status: draft. Six [TBD] items including a recommendation to
audit ASYNC_CALLS for the symmetric bug before implementation.
Audited all five RouteDecl emission sites in ast_java.py for the
symmetric Feign-style EXPOSES bug. Verdict: bug does not exist on
the async side. Async uses structurally different annotations per
direction (@KafkaListener consumer vs KafkaTemplate.send producer);
only listeners emit RouteDecl, producers emit OutgoingCall. Verified
empirically on cross_service_smoke fixture. Fix stays HTTP-only.
@HumanBean17 HumanBean17 merged commit 2d74773 into master May 5, 2026
HumanBean17 added a commit that referenced this pull request May 5, 2026
…#27)

Implements the merged proposes (#25 #26) as per-PR plans following
the PLAN-POST-TIER1B-FOLLOWUPS structure: scope, out-of-scope,
background, failure modes, resolution with code anchors, tests
table, manual evidence, definition of done, risk register,
followups, references.

PLAN-FEIGN-NOT-AN-EXPOSER: single PR (PR-F1), gates EXPOSES
emission on route.kind != 'http_consumer', surfaces suppression
count in GraphMeta. No schema bump (additive nullable column).

PLAN-CROSS-SERVICE-RESOLUTION-FLAG: single PR (PR-G1), adds
cross_service_resolution flag (auto | brownfield_only) to
.lancedb-mcp.yml. Gates pass6_match_edges cross-service
candidates on _is_brownfield_sourced helper. Ontology bump 7->8.
HumanBean17 added a commit that referenced this pull request May 6, 2026
* 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.
@HumanBean17 HumanBean17 deleted the propose/feign-not-an-exposer branch May 10, 2026 21:18
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