Skip to content

feat: Tier 1 cross-repo gRPC matching + production-readiness fixes#293

Open
sponger94 wants to merge 10 commits intoDeusData:mainfrom
sponger94:tier1-grpc-cross-repo
Open

feat: Tier 1 cross-repo gRPC matching + production-readiness fixes#293
sponger94 wants to merge 10 commits intoDeusData:mainfrom
sponger94:tier1-grpc-cross-repo

Conversation

@sponger94
Copy link
Copy Markdown

@sponger94 sponger94 commented Apr 26, 2026

Summary

Lands the full Tier 1 (gRPC) cross-repo intelligence stack in one PR, plus the production-readiness fixes that surfaced when running it against a real .NET microservice fleet using NuGet-distributed .proto contracts and C# 12 primary constructors. Strictly additive on the matching path; reuses pass_cross_repo.c match_typed_routes (Phase D for GRPC_CALLS) without changing it.

Closes the design discussion in #292 for Tier 1. Tiers 2–4 (typed message pub/sub, attribute-driven HTTP, config-resolved discovery) remain as separate follow-up PRs.

Companion docs (in this PR under .planning/):

  • cbm-cross-repo-proposal.md — full Tier 1 design, Tier 1g deferral rationale, sequencing.
  • tier1-extractor-fixes.md — production-readiness gap log.

What ships

Producer + consumer detection. pass_idl_scan now extracts producer-side typed gRPC clients from four signal sources — local-var type_assigns, constructor parameters, factory return types (Go pb.NewFooClient, Java fooGrpc.newBlockingStub), DI registrations (services.AddGrpcClient<T>, @GrpcClient, NestJS @Client), and class fields/properties. Consumer-side handler binding (Python *Servicer, Java *ImplBase, C# *ServiceBase) plus IDL Route emission from .proto files were already in place; this PR completes the producer half so the existing Phase D matcher emits real CROSS_GRPC_CALLS end-to-end.

False-positive guards: type-name denylist for System.Net.*, Microsoft.Extensions.Http, RestSharp, Refit, Flurl, java.net.http, okhttp3, reqwest, urllib, httpx; method-name normalization (*Async strip, lowerCamelCase → PascalCase); var-scope lookup with explicit class/file/function priority and a fail-closed flag for project-wide arrays.

Production-readiness fixes (tier1-extractor-fixes.md Gaps 1–7). Short version:

  1. pass_idl_scan was registered only on the sequential pipeline; wired into parallel + both incremental paths so repos crossing the ~50-file parallel threshold no longer silently skip it.
  2. C# 12 primary-constructor params now emit Field defs (modern .NET 8+/9+ controllers were invisible to the field walker before).
  3. tree-sitter-protobuf emits rpc Functions as flat siblings of the service Class, not children — added a line-range fallback so proto Routes are emitted regardless of DEFINES_METHOD shape.
  4. /api/layout already returned linked_projects, but the graph-UI tab silently dropped them when filtering — passes them through now and renders satellite galaxies with cross-repo edges.
  5. Incremental indexing wasn't attaching ctx->result_cache before pass_idl_scan, so producer edges never refreshed on edits — fixed in both incremental paths.
  6. Project-wide stub-var lookup had a name-only fallback that could bind a _client call to an unrelated class — now fail-closed for project-wide arrays, name-only fallback retained only for per-file arrays.
  7. Cross-package gRPC route-key collisions: keyed routes still use bare <service>/<method> since cross-repo matching joins on that key and the consumer side has no proto-package source. Added a idl_scan.route_collision warning when an existing Route's file_path differs, and a service_qn Route property carrying the proto Class qualified_name so a future FQN-aware matcher can recover provenance. The full FQN data model is deferred to a focused follow-on PR (Tier 1g) — see cbm-cross-repo-proposal.md §5.7. Half-shipping it without consumer-side derivation produces dormant nodes that don't change matching behavior.

Graph UI satellite rendering. /api/layout now computes a layout per linked project, places it on a circle around the primary cluster sized so satellites don't overlap, and populates cross_edges. The Three.js scene renders linked projects as offset NodeCloud + EdgeLines groups with inter-galaxy edges through a new EdgeLines targetNodes prop.

Files changed

File / Group Lines Purpose
src/pipeline/pass_idl_scan.c +747 / -72 producer detection + proto rpc fallback + safer stub-var lookup + collision mitigation
src/pipeline/pipeline.c +11 parallel-path wiring
src/pipeline/pipeline_incremental.c +26 incremental wiring
internal/cbm/extract_defs.c +85 C# 12 primary-ctor
internal/cbm/extract_type_assigns.c +20 factory pattern
internal/cbm/lang_specs.c +1 / -1 C# field_types (property + field)
Makefile.cbm +14 dump-csharp target
tests/dump_csharp.c +134 extractor inspection helper
graph-ui/src/components/{EdgeLines,GraphScene,GraphTab}.tsx +166 / -22 cross-galaxy rendering
src/ui/http_server.c +123 / -5 /api/layout linked-project response
.planning/{cbm-cross-repo-proposal,tier1-extractor-fixes}.md new design + production-readiness notes

Test coverage

10 unit tests in tests/test_pipeline.c cover the IDL Route + consumer-side HANDLES paths. Producer-side detection and the production-readiness fixes are validated end-to-end against a real .NET microservice fleet (NuGet-distributed contracts, C# 12 primary ctors, mixed proto-owner / consumer / mixed shapes). Full suite: 2609 passed, 38 skipped, 0 failed (clang ASan+UBSan build).

Companion issues

Test plan

  • 10 unit tests in tests/test_pipeline.c — all pass on local clang ASan+UBSan build.
  • Full pipeline suite — no regressions.
  • Real-world fleet test — multi-service .NET monorepo with NuGet-distributed contracts; producer detection fires across consumer projects, cross-repo edges emit correctly.
  • Pass runs as a no-op on repos without .proto files and without typed-stub assignments.
  • Denylist verified — HttpClient.GetAsync does not produce GRPC_CALLS or stray Routes.
  • Graph UI cross-galaxy rendering — viewer shows linked projects as offset satellites with cross-repo edges.
  • Incremental reindex on a touched controller refreshes producer-side GRPC_CALLS (was previously stale until full reindex).
  • Reviewer to verify Linux/macOS GCC build cleanliness once CI runs.

Introduces a new sequential pass (pass_idl_scan) that runs after pass_calls.
Two responsibilities, both purely additive:

1. Emit canonical Route nodes from .proto-derived service/rpc definitions.
   For each Class node with file_path ending in ".proto", iterate its
   DEFINES_METHOD edges (rpc methods) and create a Route node per rpc with
   QN __route__grpc__<Service>/<rpc>, plus a HANDLES edge from the rpc
   Function node back to the Route.

2. Bind consumer-side gRPC handler classes via INHERITS edges. For each
   inheritance whose base class name matches a known server-stub suffix
   (Servicer, ServicerBase, ImplBase, ServiceBase, AsyncServicer, Base —
   tried longest-first), strip the suffix to derive the expected service
   name, walk methods of the inheriting class, strip *Async wrappers from
   method names, and emit HANDLES edges to the matching Route node.

The HANDLES edges are the rendezvous point for the existing cross-repo
matcher (pass_cross_repo.c match_typed_routes for GRPC_CALLS) — once
producer-side typed-client GRPC_CALLS edges land in a follow-up pass,
end-to-end CROSS_GRPC_CALLS edges become possible without further changes.

Producer-side detection (typed gRPC client method calls) is intentionally
deferred — see the design proposal for the full Tier 1-4 roadmap. This PR
ships the consumer half, which is the larger code surface and the part
where cross-language genericity is exercised (Python servicer, Java
ImplBase, C# ServiceBase all hit the same code path).

Coverage: 4 unit tests in tests/test_pipeline.c covering Python, C# (with
*Async stripping), Java, and the negative case (non-proto class skipped).
2603 tests pass overall, no regressions.
testdata/cross-repo/grpc/ holds reference snippets across the four target
ecosystems for pass_idl_scan validation:
  - contracts/promo.proto: shared IDL with package + service + 2 rpcs
  - server-python/promo_server.py: Python *Servicer subclass
  - server-csharp/PromoCodeService.cs: .NET *Base subclass with *Async methods
  - server-java/PromoCodeServiceImpl.java: Java *ImplBase subclass

These are reference fixtures, not buildable projects — no .csproj, pom.xml,
or requirements.txt. Their purpose is to give reviewers a realistic shape
of what the indexer encounters in real consumer codebases. Unit tests in
tests/test_pipeline.c mirror these shapes with synthetic gbuf nodes.

Producer-side fixtures (client-csharp, client-go, client-python) are
intentionally absent and will land alongside the producer-side typed-call
detection in the follow-up Tier 1b PR.
@sponger94 sponger94 changed the title feat(pipeline): add pass_idl_scan for gRPC IDL Route + HANDLES emission (Tier 1a) feat: extract gRPC services from .proto + bind Python/Java/.NET handlers Apr 26, 2026
@sponger94 sponger94 marked this pull request as draft April 26, 2026 12:59
…_scan

Closes the cross-repo gRPC matching loop by adding the producer half:
walks per-file extraction results (CBMFileResult.type_assigns) to find
variables assigned to generated gRPC client/stub types, then emits
GRPC_CALLS edges for each var.Method(...) call site.

Detection rules:
  - Stub-type suffixes (longest-first): BlockingStub, FutureStub, AsyncStub,
    AsyncClient, Stub, Client. Covers Python grpcio (*Stub), Java
    protoc-gen-grpc-java (*BlockingStub, *FutureStub), C# Grpc.Tools
    (*Client, *AsyncClient), Rust tonic (*Client).
  - Suffix-stripped service name MUST match a Class node in the gbuf with
    a .proto file_path. Filters out false positives from non-gRPC classes
    that happen to end in "Client" (HttpClient, WebClient, etc.).
  - Method name has *Async suffix stripped and first character capitalized
    before route lookup. Bridges Java's lowerCamelCase invocations and
    C#'s *Async wrapper convention.
  - Caller node resolved via enclosing_func_qn → file_node fallback,
    mirroring pass_calls.c calls_find_source.

Together with the existing consumer-side HANDLES edges, pass_cross_repo.c
match_typed_routes (Phase D) now produces CROSS_GRPC_CALLS edges
end-to-end without further changes.

Coverage: 4 new tests in tests/test_pipeline.c — Python *Stub, C# *Client
with *Async stripping, Java *BlockingStub with lowerCamelCase, plus a
negative case verifying HttpClient does not get a GRPC_CALLS edge.
2607 tests pass overall, 0 regressions.

Producer-side fixtures added under testdata/cross-repo/grpc/client-*
mirroring the server-* layout. Go grpc-go (pointer types + struct
embedding) and TS @grpc/grpc-js (dynamic stubs) remain out of scope —
documented in fixture README.
@sponger94 sponger94 changed the title feat: extract gRPC services from .proto + bind Python/Java/.NET handlers feat: cross-repo gRPC matching from .proto + Python/Java/.NET clients & handlers Apr 26, 2026
@sponger94 sponger94 marked this pull request as ready for review April 26, 2026 13:13
…detection

Real-world testing on a snoonu microservice fleet (gateway-service +
loyalty-gateway + ~10 .NET services) exposed a critical gap: producer-side
detection was gated on idl_service_set_contains(known_services, service),
where known_services was populated only from .proto files in the SAME
repo. In real fleets contracts ship via NuGet/Maven/PyPI packages
(Snoonu.Promo.V1.Contracts, etc.) — the producer never has a local .proto
and the gate filtered out every legitimate stub-var call.

Three fixes:

  1. Drop the local-proto gate. Producer-side detection runs on suffix
     shape alone (BlockingStub, FutureStub, AsyncStub, AsyncClient, Stub,
     Client). pass_cross_repo Phase D handles the actual cross-repo match
     by looking up Routes in target stores; non-matching GRPC_CALLS edges
     are inert (no CROSS_GRPC_CALLS) and cost only one stray local Route
     per unique stub type.

  2. Add a type-name denylist (k_non_grpc_type_markers) for prefixes that
     end in "Client" but are definitively not gRPC: System.Net.*,
     Microsoft.Extensions.Http, RestSharp, Refit, Flurl, java.net.http,
     okhttp3, reqwest, urllib, httpx. Cuts off the obvious false-positive
     surface that the local-proto gate was masking.

  3. Relax var-scope lookup with a file-scope fallback. C# class-field
     pattern (`_client = new XClient(channel)` in ctor → `_client.Method`
     in another method) has different enclosing_func_qn between
     assignment and call sites; the previous strict-match implementation
     missed it entirely. Lookup now prefers same-function scope, falls
     back to any same-var-name match in the file.

Tests:
  - idl_scan_skips_unknown_service_for_producer_call → renamed to
    idl_scan_denylist_skips_httpclient (denylist-driven instead of
    proto-list-driven), plus assertion that no stray Route is emitted.
  - idl_scan_emits_grpc_calls_without_local_proto: producer detection
    fires when no Class node from a .proto exists in the gbuf.
  - idl_scan_resolves_class_field_assigned_in_constructor: ctor-scope
    assignment + method-scope call resolves via file-scope fallback.

10 idl_scan tests pass. 2609 tests pass overall, 0 regressions.

Discovered during testing but out of scope for this PR (will be filed
as a separate upstream issue): pass_parallel.c emit_grpc_edge emits
Routes with QN format `__grpc__<service>/<method>` (without the
`__route__` prefix) using greedy suffix-stripping (ServiceClient before
Client), which produces phantom service names like `provider`,
`builder`, `experimentProvider` from local var-name matching. These
coexist in their own QN namespace; pass_idl_scan's `__route__grpc__`
Routes are unaffected.
@sponger94 sponger94 changed the title feat: cross-repo gRPC matching from .proto + Python/Java/.NET clients & handlers feat: Tier 1 cross-repo gRPC matching + production-readiness fixes (1a-1f + Gaps 1-7) Apr 27, 2026
…roperty fields

Three small extractor changes that surface signal Tier 1 producer-side
detection needs:

1. extract_defs.c — C# 12 primary-constructor parameters now emit Field
   defs scoped to the enclosing class. Iterates the class_declaration's
   parameter_list child (via field-name "parameters" or by direct child
   walk for grammars that don't surface the field name) and emits one
   Field per param with parent_class and return_type set. Modern .NET
   8+/9+ controllers/services use this syntax as default; without it the
   class-field walker sees zero typed-client fields.

2. extract_type_assigns.c — recognize Go-style `pb.NewFooClient(ch)` and
   Java-style `fooGrpc.newBlockingStub(ch)` factory calls as constructor-
   typed assignments. Accepts qualified names whose last segment matches
   a typed-stub factory pattern (`New*Client`, `new*Stub`).

3. lang_specs.c — C# now uses cs_field_types (field_declaration +
   property_declaration) for its `field_types` slot, so property
   declarations also emit Field defs.
Extends pass_idl_scan with the producer-side signal sources from the
cross-repo intelligence proposal, plus production-readiness fixes
exposed when running tier1 against a real .NET microservice fleet with
NuGet-distributed proto contracts and C# 12 primary constructors.

Producer-side detection — for each call `var.Method(...)` whose
receiver var resolves to a known stub type, emit a `GRPC_CALLS` edge
to a local Route. Stub vars are discovered via four signal sources:

* Constructor-parameter tracking: walk gbuf Method nodes that look
  like constructors; for each ctor param whose type matches a
  stub-suffix pattern (or appears in the DI registry below), record
  (class_qn, param_name, service_name) with class-wide scope.

* Factory-function inference: when type_assigns gives us
  `var = pb.NewFooClient(...)` / `fooGrpc.newBlockingStub(...)`,
  derive the service from the factory's last segment by stripping
  `New`/`new` and the trailing `Client`/`Stub` suffix.

* DI-registration scanning: harvests stub-type FQNs from
  `services.AddGrpcClient<T>(...)`, `@GrpcClient(...)` annotations,
  and NestJS-style `@Client({...})` decorators. Stub vars whose
  declared type is in this registry are treated as gRPC clients
  even without the conventional suffix.

* Field/property type tracking: for class fields whose declared type
  matches a stub-suffix pattern or DI-registered FQN, record
  (class_qn, field_name, service_name) with class-wide scope.

Production-readiness fixes:

* Proto rpc → service mapping fallback. tree-sitter-protobuf emits
  rpc Functions as flat siblings of the service Class rather than
  children, so DEFINES_METHOD edges may not exist. When that happens,
  match rpc Functions by file_path equality + start_line/end_line
  containment within the service Class. Optimized to O(N+F) via a
  single pre-pass that collects proto Classes and Functions into flat
  arrays (avoids quadratic blowup on heavy proto-defining repos).

* Safer stub-var fallback. idl_stub_var_arr_find_ext() takes a new
  allow_name_only_fallback flag. The class_vars lookup (project-wide)
  passes false so a class-scope variable can only match calls whose
  enclosing function lives under the same class; without this guard
  two unrelated classes both declaring `_client` would silently bind
  to each other's typed-client and emit wrong GRPC_CALLS edges.

* Cross-package collision visibility. Routes are still keyed
  __route__grpc__<service>/<method> using the bare service name, since
  cross-repo matching joins on that key and the consumer side has no
  proto-package source. When a second .proto with the same bare key is
  upserted, log a warning at idl_scan.route_collision so the operator
  sees the ambiguity, and write the service node's qualified_name as a
  service_qn property so a future FQN-aware matcher can recover
  provenance.

The full FQN-keyed Route data model (Tier 1g) is intentionally deferred
to a focused follow-on PR — see .planning/cbm-cross-repo-proposal.md
§5.7 for the rationale and four-piece sequencing.
pass_idl_scan needs ctx->result_cache populated to read producer-side
typed-client signals out of CBMFileResult during emission. The full
sequential pipeline already attached seq_cache before pass_definitions
and ran pass_idl_scan with it. The other three pipeline paths didn't:

* Full parallel — built a cache during parallel_extract +
  parallel_resolve but never invoked pass_idl_scan, then freed cache.
  Threshold for parallel is ~50 files, so every real-world repo
  silently skipped Tier 1 producer-side emission.

* Incremental sequential — called pass_idl_scan but never attached a
  result cache, so the pass returned early at
  `if (!ctx->result_cache)` and producer-side edges never refreshed.

* Incremental parallel — built a cache for extract+resolve but never
  called pass_idl_scan at all.

Fix mirrors the full sequential pattern in all three call sites:
allocate a CBMFileResult ** cache, attach to ctx->result_cache before
pass_idl_scan runs, run, then free.
End-to-end support for visualizing inter-repo links in the embedded
Three.js graph viewer. When the active project's store has CROSS_*
edges with target_project pointing to a sibling .db in the cache, the
viewer now renders each linked project as an offset satellite cluster
with edges connecting back to the primary cluster.

Backend (src/ui/http_server.c):
* /api/layout — for each distinct target_project found in the source
  store's CROSS_* edges, compute a layout for the linked project's
  store, place it on a circle around the primary cluster sized by
  primary + satellite radii so satellites don't bury inside, and
  populate cross_edges by joining the source CROSS_* edges to their
  Route's qualified_name (canonical across both stores) and looking up
  the matching Route id in the linked store.
* layout_radius() — bounding-radius helper used to choose spacing.

Frontend:
* GraphScene.tsx — renders data.linked_projects?.map() as additional
  NodeCloud + EdgeLines groups offset by linked_projects[i].offset.
  Inter-galaxy edges go through a new EdgeLines invocation with a
  targetNodes prop pointing at the offset-shifted satellite nodes.
* EdgeLines.tsx — new optional targetNodes prop. When set, edge.target
  ids are resolved against targetNodes instead of nodes. Existing
  intra-cluster usage is unchanged. Also adds CROSS_* / GRPC_CALLS /
  GRAPHQL_CALLS / TRPC_CALLS edge-type colors.
* GraphTab.tsx — filteredData now passes linked_projects through (was
  silently dropped, leaving the scene with no satellites). Filter
  init / enableAll union in labels and edge types from each linked
  project so satellites stay visible by default.

Without these changes, indexing a multi-service fleet produces
CROSS_GRPC_CALLS edges in SQLite that never reach the canvas — the
matching backend was correct, the rendering pipeline just had no path
for inter-galaxy data.
Standalone binary that runs the C# extractor over one file and prints
the resulting defs / type_assigns / calls / Field nodes with their
parent_class and return_type. Useful when iterating on producer-side
gRPC detection — being able to point the extractor at a real source
file and read structured output is how a few of the C# 12 primary-ctor
edge cases got found.

Built via `make -f Makefile.cbm dump-csharp`. Not wired into the main
test suite or CI.
Two planning documents covering the rationale for everything else in
this branch.

cbm-cross-repo-proposal.md:
* Updated Tier 1 sections to reflect the producer-side detection that
  ships in this branch (ctor params, factories, DI registry, fields).
* New §5.7 Tier 1g — Contract-package FQN extraction. Explains why the
  whole 1g family is deferred to a focused follow-on PR rather than
  landed here, and sequences the work into four pieces (producer dual
  emission, AST-time package extraction, deterministic collision
  resolution, NuGet/Maven consumer-side cache scan).
* Updated §5.8 sub-tier sequencing accordingly.

tier1-extractor-fixes.md (new):
* Documents the four production-readiness gaps that prevented Tier 1's
  producer-side detection from firing on real .NET fleets:
  parallel-pipeline wiring, C# 12 primary-ctor extraction, protobuf
  rpc → service fallback, and graph-UI cross-galaxy passthrough.
* Adversarial-review follow-ups: incremental-pipeline wiring,
  safer project-wide stub-var fallback, and the cross-package
  collision warning + service_qn property mitigation.
@sponger94 sponger94 force-pushed the tier1-grpc-cross-repo branch from 5772f82 to 0431efb Compare April 27, 2026 12:16
@sponger94 sponger94 changed the title feat: Tier 1 cross-repo gRPC matching + production-readiness fixes (1a-1f + Gaps 1-7) feat: Tier 1 cross-repo gRPC matching + production-readiness fixes Apr 27, 2026
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