Files
codebase-memory-mcp/internal/cbm/service_patterns.h
T
Martin Vogel 92cb0ec211 fix(pipeline): key GraphQL Routes by operation name, not operation text (#598)
When a call resolved to a GraphQL client, emit_graphql_edge named the
Route after the whole first string argument, stripping only a leading
"query " or "mutation " (the mutation strip was also one byte short, so
names kept a leading space). An anonymous `query { ... }` therefore became
one long Route, and even a named operation carried its variables and
selection set in the name ("GetUser($id: ID!) { user(id: $id) { name } }").
Every distinct operation text minted its own Route node, and names were
cut off at 256 bytes.

Add cbm_service_pattern_graphql_operation(), a small GraphQL document
scanner. It skips comments, strings, block strings, `${...}` template
interpolations and leading fragment definitions, and reports the first
operation's type and name without copying any operation text. Routes are
now keyed as follows:

  named operation              __graphql__<Name>                name <Name>
  anonymous query / mutation / __graphql__<type>__anonymous     name (anonymous <type>)
  subscription, incl. the `{ ... }` query shorthand
  no GraphQL document          __graphql__operation__anonymous  name (anonymous operation)
  (URL, bare word, no string argument)

Keys are deterministic and bounded by the GraphQL Name grammar. The
GRAPHQL_CALLS edge keeps "operation" (now the Route name) and gains
"operation_type". The old fallback to the callee name ("client.query")
is gone: a callee is not an operation.

The sequential path (pass_calls.c) emits no GRAPHQL_CALLS at all, so it
had no copy of this logic to fix.

Proof on graphql-python/gql (374a70df, Python GraphQL client, index-only):
GraphQL Routes 170 -> 16, longest QN 266 -> 34 bytes, 106 -> 0 Routes
carrying operation text. GRAPHQL_CALLS 2643 -> 1158 with the same 984
distinct callers: edges are one per (caller, operation) pair, so calls
that used to hit distinct text-keyed Routes now share their operation's
Route.

Tests (edge_types_probe): graphql_route_keyed_by_operation_name_issue598
asserts the exact GraphQL Route set (QN + name) on the parallel path.
graphql_operation_identity_parser_issue598 covers braces inside comments,
strings and interpolations, fragments before the operation, anonymous
operations with variables, name truncation and non-document arguments.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-26 10:21:30 +02:00

95 lines
4.8 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* service_patterns.h — Allowlists for HTTP clients, async dispatch, and config accessors.
*
* Used during call resolution to classify CALLS edges as:
* HTTP_CALLS — synchronous HTTP client calls
* ASYNC_CALLS — async message/task dispatch
* CONFIGURES — config/env access
*
* Lookup is O(1) via hash table initialized once at startup.
*/
#ifndef CBM_SERVICE_PATTERNS_H
#define CBM_SERVICE_PATTERNS_H
#include <stdbool.h>
#include <stddef.h>
/* Edge type returned by pattern match. */
typedef enum {
CBM_SVC_NONE = 0, /* Not a service pattern — use normal CALLS */
CBM_SVC_HTTP = 1, /* Synchronous HTTP client call */
CBM_SVC_ASYNC = 2, /* Async dispatch (message broker, task queue) */
CBM_SVC_CONFIG = 3, /* Config/env accessor */
CBM_SVC_ROUTE_REG = 4, /* Route registration (router.GET, app.get, Route::post) */
CBM_SVC_GRPC = 5, /* gRPC client call (protobuf stub invocation) */
CBM_SVC_GRAPHQL = 6, /* GraphQL client query/mutation */
CBM_SVC_TRPC = 7, /* tRPC client procedure call */
} cbm_svc_kind_t;
/* Initialize the pattern lookup tables. Call once at startup. Thread-safe after init. */
void cbm_service_patterns_init(void);
/* Check if a resolved QN contains a known service library identifier.
* Returns the pattern kind, or CBM_SVC_NONE if no match.
* Matches on library name substrings in the QN (e.g., "requests" in
* "project.venv.requests.api.get"). Import-alias transparent. */
cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn);
/* True for a bare, unqualified call to the native `fetch()` API. Deliberately
* NOT part of the substring tables above: those are matched unconditionally
* against the raw callee name before/regardless of registry resolution
* (the #523 external-library bypass), and "fetch" — unlike "axios" or
* "requests" — collides with a plausible local identifier. Callers must
* only consult this after registry resolution has come back empty, so a
* locally resolvable `function fetch(){}` / `const fetch = () => {}` is
* classified via its real resolved QN instead and never reaches this check. */
bool cbm_service_pattern_is_global_fetch(const char *callee_name);
/* True when a string literal is a plausible HTTP route for the given callee.
* Rejects filesystem paths and non-HTTP string consumers. */
bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name);
/* True when text that reached a path slot is a source comment rather than a
* path: a block comment (slash-star … star-slash), a line comment (slash-slash
* followed by a space), or any text with a line break. The wildcard route
* (slash-star alone) is a path. */
bool cbm_service_pattern_is_comment_text(const char *text);
/* Per-worker TLS cache for cbm_service_pattern_match results. The
* pattern matcher runs once per resolved CALL edge in emit_service_
* edge — that's 6 pattern lists × ~30 patterns × strstr per call ≈
* ~180 strstrs per call. The same resolved QN repeats across most of
* the call edges in a project (e.g. "fmt.Errorf"), so caching turns
* a linear pattern-list scan into one hash lookup. Call _begin once
* per worker thread before the resolve loop and _end at the end. */
void cbm_service_pattern_cache_begin(void);
void cbm_service_pattern_cache_end(void);
/* Get the HTTP method from the callee name suffix (e.g., ".get" → "GET").
* Returns NULL if method cannot be inferred. */
const char *cbm_service_pattern_http_method(const char *callee_name);
/* Get the HTTP method from a route registration callee name suffix
* (e.g., "router.GET" → "GET", "app.post" → "POST").
* Returns NULL if not a known route registration method. */
const char *cbm_service_pattern_route_method(const char *callee_name);
/* Get the broker name for an async QN (e.g., "pubsub" from a Pub/Sub QN).
* Returns NULL if not an async pattern. */
const char *cbm_service_pattern_broker(const char *resolved_qn);
/* GraphQL operation identity of a client call argument (#598).
* Scans `doc` as a GraphQL document (skipping comments, strings, `${...}`
* template interpolations and leading fragment definitions) and reports the
* first operation: *op_type is "query", "mutation" or "subscription" ("query"
* for the `{ ... }` shorthand). Returns true and writes the operation name to
* name_buf when the operation is named. Returns false for an anonymous
* operation, or with *op_type = "operation" when `doc` is not a GraphQL
* document (NULL, a URL, a bare word such as a dict key ...). The operation
* text itself is never copied, so callers can key Routes on a bounded
* identity. */
bool cbm_service_pattern_graphql_operation(const char *doc, const char **op_type, char *name_buf,
size_t name_sz);
#endif /* CBM_SERVICE_PATTERNS_H */