* fix(group): add configurable cross-link path exclusions to reduce false positives Add matching.exclude_links_paths and matching.exclude_links_param_only_paths to group.yaml config. These filter out noisy HTTP contracts (health checks, param-only catch-all routes) from cross-link matching while preserving them in the contract registry for documentation purposes. Defaults are empty/false for backward compatibility — no behavior change unless the operator explicitly configures exclusions. * fix(group): address review findings — filter unmatched, normalize trailing slash, add tests - Excluded contracts no longer inflate SyncResult.unmatched (isNoisy guard) - pathPart in buildNoisyContractFilter strips trailing slashes before comparison - 8 new unit tests for buildNoisyContractFilter covering all code paths - Config-parser test asserts defaults for new matching fields * fix(group): normalize configured exclusion paths and add root-path test - Strip trailing slashes from configured exclude_links_paths at Set-build time so root path '/' (which normalizes to '') matches correctly - Add test: exclude_links_paths: ['/'] suppresses http::GET::/ contracts - Add new matching fields as commented examples in fixture group.yaml (DoD §2.4) * docs(group): document exclude_links_paths and exclude_links_param_only_paths config fields Add JSDoc to MatchingConfig interface, update the microservices guide YAML example and field notes, and scaffold the new fields (commented out) in the group create template.
19 KiB
Using GitNexus across gRPC microservices
When to use this guide
This guide is for teams whose product lives in several separate Git repositories — one per service — and whose services talk to each other over gRPC (possibly alongside HTTP and message topics). GitNexus indexes each repo independently, then a group stitches the per-repo indexes into a single cross-repo view that the impact, query, and context tools can traverse. If your services live in one monorepo, much of this still applies — set each service as a member of a group and use the service prefix to scope queries — but the walkthrough assumes the harder multi-repo case.
Mental model
- Each repository has its own
.gitnexus/index (a LadybugDB graph of symbols, relationships, processes).gitnexus analyzein each repo produces that index completely independently. - A group is a higher-level construct stored at
~/.gitnexus/groups/<group>/that references the per-repo indexes by their registry name. - Sync-time extractors walk each member repo and emit contracts — provider or consumer records keyed by a canonical
contractId(grpc::auth.AuthService/Login,http::GET::/orders, etc.). - The sync step matches providers and consumers that share a
contractIdand writes cross-links to<groupDir>/contracts.json. Those cross-links are what letsimpact({repo: "@<group>", target: "X"})hop from one repo into another. - Contracts come from three places: automatic contract extractors (
grpc-extractor,http-route-extractor,topic-extractor), a manifest escape hatch (config.linksingroup.yaml), and — for same-name symbol matches where no contract is declared — the exact-match matching cascade inmatching.ts. - Each repo stays editable and re-indexable on its own. Re-run
gitnexus analyzein a repo when it changes, thengitnexus group sync <group>to refreshcontracts.json.gitnexus group statusreports which members are stale.
Prerequisites
- GitNexus installed and runnable as
gitnexusornpx gitnexus(see the root README.md). - Each service repository checked out locally. No requirement that they share a parent directory — the group references them by registry name.
- Write access to
~/.gitnexus/(the default gitnexus home; seegetDefaultGitnexusDirinstorage.ts).
Step-by-step walkthrough
The example uses three services — a TypeScript API gateway, a Go orders service, and a Python inventory service — with gRPC between them. The gateway is an orders consumer; the orders service is both an orders provider and an inventory consumer; the inventory service is an inventory provider.
1. Index each repository
Run analyze from inside each service repo (or pass the path). The CLI surface lives in gitnexus/src/cli/analyze.ts and is wired in gitnexus/src/cli/index.ts.
cd ~/code/gateway && npx gitnexus analyze
cd ~/code/orders && npx gitnexus analyze
cd ~/code/inventory && npx gitnexus analyze
Useful flags:
--force— reindex even if up to date.--embeddings— generate embedding vectors (needed only if you want semantic search; the exact-match cross-repo cascade does not need them).--name <alias>— register the repo under a specific alias when two repos share a basename (e.g. twoapi/folders).--skip-git— index a checkout that isn't a git repo.
Each run writes a .gitnexus/ folder in the repo and registers the repo in ~/.gitnexus/registry.json. Confirm with npx gitnexus list.
2. Author group.yaml
Create the group directory and edit the config. Either use the CLI scaffolder or write the file directly — both produce the same shape consumed by config-parser.ts.
npx gitnexus group create payments-platform
# or manually:
mkdir -p ~/.gitnexus/groups/payments-platform
$EDITOR ~/.gitnexus/groups/payments-platform/group.yaml
Minimal working group.yaml:
version: 1
name: payments-platform
description: Gateway + orders + inventory (gRPC)
repos:
gateway: gateway
orders: orders
inventory: inventory
# Only add explicit links when the automatic extractors miss something —
# see "When automatic extraction isn't enough" below.
links: []
packages: {}
detect:
http: true
grpc: true
topics: true
shared_libs: true
embedding_fallback: false
matching:
bm25_threshold: 0.7
embedding_threshold: 0.65
max_candidates_per_step: 3
# Exclude noisy paths from cross-link matching (contracts are still extracted)
exclude_links_paths: [/ping, /health, /healthcheck]
exclude_links_param_only_paths: true
Field notes (schema in types.ts):
version— must be1. The parser rejects anything else.name— required; used for the group directory name and all CLI / MCP calls.repos— a mapping from group path (a logical name you choose; can be a hierarchy likebackend/orders) to registry name (the name shown bynpx gitnexus list). Both sides appear throughout the tooling: contract rows use the group path;@<group>/<groupPath>routes tools to a single member.links— optional manifest escape hatch, one entry per explicit cross-repo contract. Validated by the parser:fromandtomust be known repo paths,typemust be one ofhttp | grpc | topic | lib | custom, androlemust beprovider | consumer.detect— toggles per extractor family. Defaults (set inconfig-parser.ts) turnhttp,grpc,topics, andshared_libson; disable the ones you don't use to speed up sync.matching— thresholds for the matching cascade. The exact match is always run; other strategies depend on indexer state. Two optional fields reduce false-positive cross-links in large groups:exclude_links_paths— list of HTTP paths to exclude from cross-link matching (default[]). Contracts at these paths are still extracted and visible in the registry, but they don't produce cross-repo links. Useful for health-check endpoints (/ping,/health) that every service exposes. Trailing slashes are normalized.exclude_links_param_only_paths— whentrue, exclude routes where every segment is{param}(e.g./{param},/{param}/{param}) from cross-link matching (defaultfalse). Mixed routes like/users/{param}are not affected.
3. Sync the group
npx gitnexus group sync payments-platform --verbose
What this does (see sync.ts):
- Opens each member's per-repo LadybugDB.
- Runs the HTTP, gRPC, and topic extractors against the source files.
- Applies manifest
linksthroughmanifest-extractor.ts. - Runs the exact-match cascade, joining providers and consumers that share a normalized
contractId. - Writes
contracts.jsonin the group directory.
Flags:
--exact-only— stop after the exact cascade; skip BM25 and embedding fallback.--skip-embeddings— run exact plus BM25 but not embedding-based matching.--allow-stale— don't warn if a member's index is stale.--json— machine-readable output.
The same operation is available over MCP as group_sync({ name: "payments-platform" }) — see tools.ts.
4. Inspect the registry
Use gitnexus group contracts for the CLI view or read the gitnexus://group/<name>/contracts MCP resource for the same data.
npx gitnexus group contracts payments-platform --type grpc --json
A shortened response:
{
"contracts": [
{
"contractId": "grpc::orders.OrderService/PlaceOrder",
"type": "grpc",
"role": "provider",
"repo": "orders",
"symbolRef": { "filePath": "internal/grpc/order_server.go", "name": "RegisterOrderServiceServer" },
"confidence": 0.8,
"meta": { "service": "OrderService", "method": "PlaceOrder", "source": "go_register" }
},
{
"contractId": "grpc::orders.OrderService/PlaceOrder",
"type": "grpc",
"role": "consumer",
"repo": "gateway",
"symbolRef": { "filePath": "src/clients/orders.ts", "name": "OrderServiceClient" },
"confidence": 0.75,
"meta": { "service": "OrderService", "source": "ts_generated_client" }
}
],
"crossLinks": [
{
"from": { "repo": "gateway", "symbolUid": "…", "symbolRef": { "filePath": "src/clients/orders.ts", "name": "OrderServiceClient" } },
"to": { "repo": "orders", "symbolUid": "…", "symbolRef": { "filePath": "internal/grpc/order_server.go", "name": "RegisterOrderServiceServer" } },
"type": "grpc",
"contractId": "grpc::orders.OrderService/PlaceOrder",
"matchType": "exact",
"confidence": 1.0
}
]
}
Staleness of the underlying indexes shows up in npx gitnexus group status payments-platform or the gitnexus://group/<name>/status resource.
5. Run cross-repo impact with @<group> routing
From any shell (you do not have to cd into a member repo), the normal impact / query / context tools accept repo: "@<group>" to fan out across all members, or repo: "@<group>/<memberPath>" to target one member. Routing is implemented in resolve-at-member.ts and described in tools.ts.
Example MCP calls:
{"tool": "impact", "arguments": {
"repo": "@payments-platform/orders",
"target": "PlaceOrder",
"direction": "upstream",
"crossDepth": 2
}}
{"tool": "query", "arguments": {
"repo": "@payments-platform",
"query": "retry logic around PlaceOrder"
}}
The CLI equivalents still exist for scripting:
npx gitnexus group impact payments-platform \
--repo orders --target PlaceOrder --direction upstream --cross-depth 2
Phase 1 walks within the anchor member; Phase 2 hops across the Contract Bridge wherever a cross-link endpoint matches an impacted symbol. See cross-impact.ts for the bridge query.
How gRPC extraction works
GrpcExtractor (grpc-extractor.ts) runs two passes per member repo:
- Proto map. Every
**/*.protofile is parsed to enumerateservice Foo { rpc Bar(...) }blocks and (transitively) resolve the package name. Each RPC method becomes a provider contract withcontractId = grpc::<package>.<Service>/<Method>andconfidence = 0.85. Parsing uses the vendoredtree-sitter-protogrammar when available and falls back to a length-preserving manual parser (extractServiceBlocks) otherwise, so.protoextraction works on platforms where the grammar fails to build. - Source scan. Every source file whose extension matches
GRPC_SCAN_GLOBis parsed by its language plugin:
| Language | Provider signal | Consumer signal |
|---|---|---|
Go (go.ts) |
pb.RegisterXxxServer(...), pb.UnimplementedXxxServer embedded in struct |
pb.NewXxxClient(conn) |
Java (java.ts) |
extends XxxServiceGrpc.XxxServiceImplBase (with or without @GrpcService) |
XxxServiceGrpc.newBlockingStub(...), newStub(...) |
Python (python.ts) |
add_XxxServicer_to_server(...) (bare or _pb2_grpc. attribute form) |
XxxStub(channel) (ignores Mock/Test/Fake/Stub) |
Node / TS (node.ts) |
NestJS @GrpcMethod('Service','Method') |
@GrpcClient field typed XxxServiceClient, client.getService<X>('Service'), new XxxServiceClient(...), new foo.bar.XxxService(...) in files that call loadPackageDefinition |
For each source-scan detection the extractor looks up the short service name in the proto map and picks:
grpc::<package>.<Service>/<Method>when a method is named and the service resolves against the proto map,grpc::<package>.<Service>/*(wildcard) when only the service is known, orgrpc::<ServiceName>/*when no.protois available at all.
Provider detections land at confidence 0.8 (with proto) or 0.65 (without); consumers at 0.75 or 0.55. NestJS @GrpcMethod is fixed at 0.8 because the decorator is self-describing.
Matching
matching.ts lowercases the package/service segment before comparing contract ids, so bindings that capitalize names differently (auth.AuthService vs auth.authservice) still match. Method names are compared case-sensitively because gRPC's wire path is case-sensitive. Service-only wildcards (grpc::pkg.Svc/*) match any method on the same service during cross-linking.
Known limitations
- Ambiguous proto resolution. If a short service name exists in more than one
.protofile and the source-scan hit can't be narrowed down by shared directory segments (resolveProtoConflictrefuses to guess), the extractor skips contract emission and logs a warning. - Proto packages must be resolvable locally. Transitive imports that point outside the repo produce an empty package segment, which means the contract id collapses to
grpc::<Service>/<Method>. Cross-repo matches still work as long as both sides agree on the empty package. - Rewrite rules are not implemented. If the provider repo writes
grpc::orders.OrderService/PlaceOrderand the consumer repo writesgrpc::orderspb.OrderService/PlaceOrder, they won't cross-link automatically. Useconfig.linksto declare the correspondence (see below). - One sync = one snapshot. Contracts are extracted against the indexed snapshot of each repo. Re-index first, then re-sync; the
statuscommand and resource surface staleness.
When automatic extraction isn't enough
The escape hatch is the links list in group.yaml, handled by ManifestExtractor. Each entry is a one-directional provider/consumer declaration:
version: 1
name: payments-platform
repos:
gateway: gateway
orders: orders
inventory: inventory
links:
# Explicit gRPC method: use when naming mismatches stop the
# automatic matcher from cross-linking.
- from: gateway
to: orders
type: grpc
contract: OrderService/PlaceOrder
role: consumer
# Service-level link when you don't want to enumerate methods.
- from: orders
to: inventory
type: grpc
contract: InventoryService
role: consumer
# Works for HTTP too — use `METHOD::/path` form for the exact
# handler, or just `/path` for a method-agnostic wildcard.
- from: gateway
to: orders
type: http
contract: POST::/orders
role: consumer
What the manifest extractor does (see manifest-extractor.ts):
- Builds a canonical
contractIdwithbuildContractId— the same canonicalization used by the automatic extractors, so manifest links cross-match automatic contracts on the other side. - Tries to resolve each side to a real graph symbol (the
Routenode for HTTP, aFunction|Method/Class|Interfacefor gRPC, aPackage|Moduleforlib). - If resolution fails, falls back to a deterministic synthetic uid (
manifest::<repo>::<contractId>) so both sides still line up in cross-impact — name-only links still work when the symbol isn't in the graph. - Emits both a provider and a consumer
StoredContract(confidence1.0,source: "manifest") and aCrossLinkwithmatchType: "manifest".
Use links for exactly the cases the extractor can't infer: different package names across repos (see #701), hand-rolled transports, cases where the provider repo isn't checked out locally but you still want a record, or any contract whose provider and consumer simply don't share a surface the extractors know how to pattern-match.
History: the manifest extractor used to be silently skipped by the sync pipeline; that was fixed in #827 (tracking issue #826). If you ever see config.links with zero cross-links in contracts.json, make sure you're on a build that includes that fix, then re-run group sync.
Troubleshooting
contracts.jsonis empty after a sync. Either no member repo contained a recognizable gRPC pattern, or the extractors are disabled indetect. Confirmdetect.grpc: trueand re-run with--verbose.- A known provider/consumer pair doesn't cross-link. Most common cause: the package segment differs. Check the raw contract ids with
gitnexus group contracts <name> --unmatched— if you see two same-method contracts with different package prefixes, add a manifestlinks:entry to bridge them (no automatic rewrite rules yet). matchType: "manifest"is missing entirely. The extractor needsconfig.linksto be non-empty and the sync pipeline to actually call it — verify you're on a post-#827 build. Empty contract rows for manifest links usually meanresolveSymbolcouldn't find a graph match; the synthetic uid still lets cross-impact work, it just won't carry a file path.- Ambiguous proto warnings. Look for
[grpc-extractor] Ambiguous proto resolutionin the sync logs; that means a service name exists in multiple.protofiles under the same repo and the path-distance heuristic couldn't pick a winner. Resolve by renaming the service or declaring the intended pairing inconfig.links. - Cross-impact says "stale". Both sides need a fresh per-repo index and a fresh group sync. Order matters:
gitnexus analyzein each changed repo, thengitnexus group sync <name>. Usegitnexus group status <name>to see which side is behind.
Related docs and references
- AGENTS.md — authoritative list of MCP tools and resources, including group-mode routing and the
gitnexus://group/…resources. - ARCHITECTURE.md — overall data flow and the call-resolution DAG that the per-repo indexer uses.
gitnexus/src/core/group/—service.ts,sync.ts,config-parser.ts,matching.ts.gitnexus/src/core/group/extractors/grpc-extractor.tsandgrpc-patterns/— gRPC detection.gitnexus/src/core/group/extractors/manifest-extractor.ts— theconfig.linksescape hatch.gitnexus/src/mcp/tools.ts— MCP tool schemas (group_list,group_sync, plus@<group>routing onimpact/query/context).gitnexus/src/cli/group.ts— CLI command definitions and flags.- Upstream issues: #701, #826, #906.