Files
deusdata--codebase-memory-mcp/src/pipeline/pass_cross_repo.c
T
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

838 lines
34 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.
/*
* pass_cross_repo.c — Cross-repo intelligence: match Routes, Channels, and
* async topics across indexed projects to create CROSS_* edges.
*
* For each HTTP_CALLS/ASYNC_CALLS edge in the source project, looks up the
* target Route QN in other project DBs. For each Channel node with EMITS
* edges, looks for matching LISTENS_ON in other projects (and vice versa).
*
* Edges are written bidirectionally: both source and target project DBs
* get a CROSS_* edge so the link is visible from either side.
*/
#include "pipeline/pass_cross_repo.h"
#include "pipeline/pipeline_internal.h" // cbm_route_canon_path
#include "foundation/constants.h"
#include "foundation/log.h"
#include "foundation/platform.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include <sqlite3/sqlite3.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ── Constants ───────────────────────────────────────────────────── */
enum {
CR_PATH_BUF = 1024,
CR_QN_BUF = 512,
CR_PROPS_BUF = 2048,
CR_MAX_EDGES = 4096,
CR_DB_EXT_LEN = 3, /* strlen(".db") */
CR_INIT_CAP = 32,
CR_COL_3 = 3,
CR_COL_4 = 4,
CR_SCHEME_SKIP = 3, /* strlen("://") */
CR_ROUTE_PREFIX_LEN = 9, /* strlen("__route__") */
CR_ANY_LEN = 3, /* strlen("ANY") */
};
#define CR_MS_PER_SEC 1000.0
#define CR_NS_PER_MS 1000000.0
/* TLS buffer for integer-to-string in log calls. */
static CBM_TLS char cr_ibuf[CBM_SZ_32];
static const char *cr_itoa(int v) {
snprintf(cr_ibuf, sizeof(cr_ibuf), "%d", v);
return cr_ibuf;
}
/* ── Helpers ─────────────────────────────────────────────────────── */
static const char *cr_cache_dir(void) {
const char *dir = cbm_resolve_cache_dir();
return dir ? dir : cbm_tmpdir();
}
static void cr_db_path(const char *project, char *buf, size_t bufsz) {
snprintf(buf, bufsz, "%s/%s.db", cr_cache_dir(), project);
}
/* Extract a JSON string property from properties_json.
* Writes into buf, returns buf on success, NULL on miss. */
static const char *json_str_prop(const char *json, const char *key, char *buf, size_t bufsz) {
if (!json || !key) {
return NULL;
}
char pat[CBM_SZ_128];
snprintf(pat, sizeof(pat), "\"%s\":\"", key);
const char *start = strstr(json, pat);
if (!start) {
return NULL;
}
start += strlen(pat);
const char *end = strchr(start, '"');
if (!end) {
return NULL;
}
size_t len = (size_t)(end - start);
if (len >= bufsz) {
len = bufsz - SKIP_ONE;
}
memcpy(buf, start, len);
buf[len] = '\0';
return buf;
}
/* Build CROSS_* edge properties JSON. */
static void build_cross_props(char *buf, size_t bufsz, const char *target_project,
const char *target_function, const char *target_file,
const char *url_or_channel, const char *extra_key,
const char *extra_val) {
int n = snprintf(buf, bufsz,
"{\"target_project\":\"%s\",\"target_function\":\"%s\","
"\"target_file\":\"%s\"",
target_project ? target_project : "", target_function ? target_function : "",
target_file ? target_file : "");
if (url_or_channel && url_or_channel[0]) {
n += snprintf(buf + n, bufsz - (size_t)n, ",\"%s\":\"%s\"",
extra_key ? extra_key : "url_path", url_or_channel);
}
if (extra_val && extra_val[0]) {
n += snprintf(buf + n, bufsz - (size_t)n, ",\"%s\":\"%s\"",
extra_key ? "transport" : "method", extra_val);
}
snprintf(buf + n, bufsz - (size_t)n, "}");
}
/* Delete all CROSS_* edges for a project from a store. */
static void delete_cross_edges(cbm_store_t *store, const char *project) {
cbm_store_delete_edges_by_type(store, project, "CROSS_HTTP_CALLS");
cbm_store_delete_edges_by_type(store, project, "CROSS_ASYNC_CALLS");
cbm_store_delete_edges_by_type(store, project, "CROSS_CHANNEL");
cbm_store_delete_edges_by_type(store, project, "CROSS_GRPC_CALLS");
cbm_store_delete_edges_by_type(store, project, "CROSS_GRAPHQL_CALLS");
cbm_store_delete_edges_by_type(store, project, "CROSS_TRPC_CALLS");
}
/* Insert a CROSS_* edge into a store. Idempotent by construction: the edges
* table is UNIQUE(source_id, target_id, type) and cbm_store_insert_edge
* upserts on conflict, so a pair reached from both match directions or on a
* repeat run never duplicates a row. (#523) */
static void insert_cross_edge(cbm_store_t *store, const char *project, int64_t from_id,
int64_t to_id, const char *edge_type, const char *props) {
cbm_edge_t edge = {
.project = project,
.source_id = from_id,
.target_id = to_id,
.type = edge_type,
.properties_json = props,
};
cbm_store_insert_edge(store, &edge);
}
/* Strip "scheme://host[:port]" from a stored HTTP_CALLS url, returning the
* path. url_path property values are stored raw from the call's first string
* argument, so they can be full URLs ("scheme://host:port/v2/x") — and
* cbm_route_canon_path only canonicalizes placeholder syntax, never strips
* authorities. Returns "/" for a URL with no path after the host (a request
* against the bare base URL targets the root route). (#523) */
static const char *cr_url_path(const char *url) {
if (!url) {
return url;
}
const char *scheme_end = strstr(url, "://");
if (!scheme_end) {
return url; /* already a bare path */
}
const char *path_start = strchr(scheme_end + CR_SCHEME_SKIP, '/');
return path_start ? path_start : "/";
}
/* Look up a node's name and file_path by id. */
static void lookup_node_info(struct sqlite3 *db, int64_t node_id, char *name_out, size_t name_sz,
char *file_out, size_t file_sz) {
name_out[0] = '\0';
file_out[0] = '\0';
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db, "SELECT name, file_path FROM nodes WHERE id = ?1", CBM_NOT_FOUND,
&st, NULL) != SQLITE_OK) {
return;
}
sqlite3_bind_int64(st, SKIP_ONE, node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char *nm = (const char *)sqlite3_column_text(st, 0);
const char *fp = (const char *)sqlite3_column_text(st, SKIP_ONE);
if (nm) {
snprintf(name_out, name_sz, "%s", nm);
}
if (fp) {
snprintf(file_out, file_sz, "%s", fp);
}
}
sqlite3_finalize(st);
}
/* ── Phase A: HTTP Route matching ────────────────────────────────── */
/* Find a Route node in target_store by QN and return the handler function's
* node id, name, and file_path via HANDLES edges. Returns 0 if not found. */
static int64_t find_route_handler(cbm_store_t *target_store, const char *route_qn,
char *handler_name, size_t name_sz, char *handler_file,
size_t file_sz) {
handler_name[0] = '\0';
handler_file[0] = '\0';
struct sqlite3 *db = cbm_store_get_db(target_store);
if (!db) {
return 0;
}
/* Find Route node by QN */
sqlite3_stmt *s = NULL;
if (sqlite3_prepare_v2(
db, "SELECT id FROM nodes WHERE qualified_name = ?1 AND label = 'Route' LIMIT 1",
CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(s, SKIP_ONE, route_qn, CBM_NOT_FOUND, SQLITE_STATIC);
int64_t route_id = 0;
if (sqlite3_step(s) == SQLITE_ROW) {
route_id = sqlite3_column_int64(s, 0);
}
sqlite3_finalize(s);
if (route_id == 0) {
return 0;
}
/* Follow HANDLES edge to find the handler function */
if (sqlite3_prepare_v2(db,
"SELECT n.id, n.name, n.file_path FROM edges e "
"JOIN nodes n ON n.id = e.source_id "
"WHERE e.target_id = ?1 AND e.type = 'HANDLES' LIMIT 1",
CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_int64(s, SKIP_ONE, route_id);
int64_t handler_id = 0;
if (sqlite3_step(s) == SQLITE_ROW) {
handler_id = sqlite3_column_int64(s, 0);
const char *n = (const char *)sqlite3_column_text(s, SKIP_ONE);
const char *f = (const char *)sqlite3_column_text(s, PAIR_LEN);
if (n) {
snprintf(handler_name, name_sz, "%s", n);
}
if (f) {
snprintf(handler_file, file_sz, "%s", f);
}
}
sqlite3_finalize(s);
return handler_id;
}
/* Segment-wise match of a concrete path against a route template path, where a
* "{...}" segment in the template matches any single non-empty concrete
* segment. Both inputs are bare paths (no method prefix, no authority).
* Leading/trailing slashes are insignificant. Returns true on a full match. */
static bool cr_path_matches_template(const char *concrete, const char *templ) {
const char *c = concrete;
const char *t = templ;
while (*c && *t) {
if (*c == '/') {
c++;
}
if (*t == '/') {
t++;
}
const char *cseg = c;
while (*c && *c != '/') {
c++;
}
const char *tseg = t;
while (*t && *t != '/') {
t++;
}
size_t clen = (size_t)(c - cseg);
size_t tlen = (size_t)(t - tseg);
bool t_is_param = (tlen >= PAIR_LEN && tseg[0] == '{' && tseg[tlen - 1] == '}');
if (!t_is_param) {
if (clen != tlen || strncmp(cseg, tseg, clen) != 0) {
return false;
}
} else if (clen == 0) {
return false; /* a parameter never matches an empty segment */
}
}
while (*c == '/') {
c++;
}
while (*t == '/') {
t++;
}
return *c == '\0' && *t == '\0';
}
/* Fallback for when the exact route-QN lookup misses: a concrete client path
* ("/v2/orders/123") never exact-matches a templated route QN
* ("__route__GET__/v2/orders/{}"). Enumerate the target store's Route nodes
* and segment-match the concrete path against each template. On a match, copy
* the route QN into out_qn and return the handler id; returns 0 on no match.
*
* COST: this scans every Route node of the target project once per unmatched
* HTTP_CALLS edge — O(calls × routes) per project pair. Acceptable while both
* factors stay small (calls are capped at CR_MAX_EDGES and it only runs for
* edges the exact lookup missed); revisit with a prepared template index if
* cross-repo matching ever shows up in profiles. (#523) */
static int64_t find_route_handler_fuzzy(cbm_store_t *target_store, const char *concrete_path,
const char *method, char *out_qn, size_t out_qn_sz,
char *handler_name, size_t name_sz, char *handler_file,
size_t file_sz) {
struct sqlite3 *db = cbm_store_get_db(target_store);
if (!db || !concrete_path || !concrete_path[0]) {
return 0;
}
sqlite3_stmt *s = NULL;
if (sqlite3_prepare_v2(db, "SELECT qualified_name FROM nodes WHERE label = 'Route'",
CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
return 0;
}
int64_t found = 0;
while (sqlite3_step(s) == SQLITE_ROW) {
const char *qn = (const char *)sqlite3_column_text(s, 0);
if (!qn || strncmp(qn, "__route__", CR_ROUTE_PREFIX_LEN) != 0) {
continue;
}
/* Split "__route__<METHOD>__<path>" */
const char *rest = qn + CR_ROUTE_PREFIX_LEN;
const char *sep = strstr(rest, "__");
if (!sep) {
continue;
}
size_t mlen = (size_t)(sep - rest);
const char *rpath = sep + PAIR_LEN;
/* Method gate: the route's method must equal the caller's, or be ANY.
* A missing caller method matches any route method. */
if (method && method[0]) {
bool same_method = (strncmp(rest, method, mlen) == 0 && method[mlen] == '\0');
bool route_any = (mlen == CR_ANY_LEN && strncmp(rest, "ANY", CR_ANY_LEN) == 0);
if (!same_method && !route_any) {
continue;
}
}
if (!cr_path_matches_template(concrete_path, rpath)) {
continue;
}
/* A concrete path can match more than one stored template (e.g. a raw
* "{id}" variant and its canonical "{}" form). Only accept a Route that
* actually has a HANDLES edge — the handler is attached to the
* canonical node. Keep scanning otherwise. */
int64_t hid =
find_route_handler(target_store, qn, handler_name, name_sz, handler_file, file_sz);
if (hid != 0) {
snprintf(out_qn, out_qn_sz, "%s", qn);
found = hid;
break;
}
}
sqlite3_finalize(s);
return found;
}
/* Emit CROSS_* edge for a route match: forward into source, reverse into target. */
static void emit_cross_route_bidirectional(cbm_store_t *src_store, const char *src_project,
struct sqlite3 *src_db, int64_t caller_id,
int64_t local_route_id, cbm_store_t *tgt_store,
const char *tgt_project, int64_t handler_id,
const char *route_qn, const char *handler_name,
const char *handler_file, const char *url_path,
const char *method, const char *edge_type) {
/* Forward: caller → local Route in source DB */
char fwd[CR_PROPS_BUF];
build_cross_props(fwd, sizeof(fwd), tgt_project, handler_name, handler_file, url_path,
"url_path", method);
insert_cross_edge(src_store, src_project, caller_id, local_route_id, edge_type, fwd);
/* Reverse: handler → Route in target DB */
struct sqlite3 *tgt_db = cbm_store_get_db(tgt_store);
if (!tgt_db) {
return;
}
sqlite3_stmt *rq = NULL;
if (sqlite3_prepare_v2(tgt_db, "SELECT id FROM nodes WHERE qualified_name = ?1 LIMIT 1",
CBM_NOT_FOUND, &rq, NULL) != SQLITE_OK) {
return;
}
sqlite3_bind_text(rq, SKIP_ONE, route_qn, CBM_NOT_FOUND, SQLITE_STATIC);
int64_t tgt_route_id = 0;
if (sqlite3_step(rq) == SQLITE_ROW) {
tgt_route_id = sqlite3_column_int64(rq, 0);
}
sqlite3_finalize(rq);
if (tgt_route_id == 0) {
return;
}
char caller_name[CBM_SZ_256] = {0};
char caller_file[CBM_SZ_512] = {0};
lookup_node_info(src_db, caller_id, caller_name, sizeof(caller_name), caller_file,
sizeof(caller_file));
char rev[CR_PROPS_BUF];
build_cross_props(rev, sizeof(rev), src_project, caller_name, caller_file, url_path, "url_path",
method);
insert_cross_edge(tgt_store, tgt_project, handler_id, tgt_route_id, edge_type, rev);
}
static int match_http_routes(cbm_store_t *src_store, const char *src_project,
cbm_store_t *tgt_store, const char *tgt_project) {
struct sqlite3 *src_db = cbm_store_get_db(src_store);
if (!src_db) {
return 0;
}
/* Find all HTTP_CALLS edges in source project */
sqlite3_stmt *s = NULL;
if (sqlite3_prepare_v2(src_db,
"SELECT e.source_id, e.target_id, e.properties FROM edges e "
"WHERE e.project = ?1 AND e.type = 'HTTP_CALLS'",
CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(s, SKIP_ONE, src_project, CBM_NOT_FOUND, SQLITE_STATIC);
int count = 0;
while (sqlite3_step(s) == SQLITE_ROW && count < CR_MAX_EDGES) {
int64_t caller_id = sqlite3_column_int64(s, 0);
int64_t route_id = sqlite3_column_int64(s, SKIP_ONE);
const char *props = (const char *)sqlite3_column_text(s, PAIR_LEN);
char url_path[CBM_SZ_256] = {0};
char method[CBM_SZ_32] = {0};
json_str_prop(props, "url_path", url_path, sizeof(url_path));
json_str_prop(props, "method", method, sizeof(method));
if (!url_path[0]) {
continue;
}
/* Build the expected Route QN in the target project (authority-stripped
* and param-canonicalized so client url_path matches the server handler
* regardless of base URL and framework placeholder syntax). */
char route_qn[CR_QN_BUF];
char cpath[CBM_SZ_256];
const char *curl = cbm_route_canon_path(cr_url_path(url_path), cpath, sizeof(cpath));
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method[0] ? method : "ANY", curl);
char handler_name[CBM_SZ_256] = {0};
char handler_file[CBM_SZ_512] = {0};
int64_t handler_id =
find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
handler_file, sizeof(handler_file));
if (handler_id == 0) {
/* Try without method (ANY) */
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s", curl);
handler_id = find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
handler_file, sizeof(handler_file));
}
if (handler_id == 0) {
/* Exact QN lookup missed. A concrete client path ("/v2/orders/123")
* never exact-matches a templated route ("/v2/orders/{}"), so fall
* back to segment-wise template matching. (#523) */
handler_id = find_route_handler_fuzzy(
tgt_store, curl, method[0] ? method : NULL, route_qn, sizeof(route_qn),
handler_name, sizeof(handler_name), handler_file, sizeof(handler_file));
}
if (handler_id == 0) {
continue;
}
emit_cross_route_bidirectional(src_store, src_project, src_db, caller_id, route_id,
tgt_store, tgt_project, handler_id, route_qn, handler_name,
handler_file, url_path, method, "CROSS_HTTP_CALLS");
count++;
}
sqlite3_finalize(s);
return count;
}
/* ── Phase B: Async matching ─────────────────────────────────────── */
static int match_async_routes(cbm_store_t *src_store, const char *src_project,
cbm_store_t *tgt_store, const char *tgt_project) {
struct sqlite3 *src_db = cbm_store_get_db(src_store);
if (!src_db) {
return 0;
}
sqlite3_stmt *s = NULL;
if (sqlite3_prepare_v2(src_db,
"SELECT e.source_id, e.target_id, e.properties FROM edges e "
"WHERE e.project = ?1 AND e.type = 'ASYNC_CALLS'",
CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(s, SKIP_ONE, src_project, CBM_NOT_FOUND, SQLITE_STATIC);
int count = 0;
while (sqlite3_step(s) == SQLITE_ROW && count < CR_MAX_EDGES) {
int64_t caller_id = sqlite3_column_int64(s, 0);
int64_t route_id = sqlite3_column_int64(s, SKIP_ONE);
const char *props = (const char *)sqlite3_column_text(s, PAIR_LEN);
char url_path[CBM_SZ_256] = {0};
char broker[CBM_SZ_128] = {0};
json_str_prop(props, "url_path", url_path, sizeof(url_path));
json_str_prop(props, "broker", broker, sizeof(broker));
if (!url_path[0]) {
continue;
}
char route_qn[CR_QN_BUF];
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", broker[0] ? broker : "async",
url_path);
char handler_name[CBM_SZ_256] = {0};
char handler_file[CBM_SZ_512] = {0};
int64_t handler_id =
find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
handler_file, sizeof(handler_file));
if (handler_id == 0) {
continue;
}
char edge_props[CR_PROPS_BUF];
build_cross_props(edge_props, sizeof(edge_props), tgt_project, handler_name, handler_file,
url_path, "url_path", broker);
insert_cross_edge(src_store, src_project, caller_id, route_id, "CROSS_ASYNC_CALLS",
edge_props);
count++;
}
sqlite3_finalize(s);
return count;
}
/* ── Phase C: Channel matching ───────────────────────────────────── */
/* Try to find a matching listener in target DB for a channel name. */
static bool try_match_channel_listener(cbm_store_t *src_store, const char *src_project,
cbm_store_t *tgt_store, const char *tgt_project,
const char *channel_name, const char *transport,
int64_t emitter_id, int64_t channel_id) {
struct sqlite3 *tgt_db = cbm_store_get_db(tgt_store);
if (!tgt_db) {
return false;
}
sqlite3_stmt *tq = NULL;
if (sqlite3_prepare_v2(tgt_db,
"SELECT n.id, e.source_id, fn.name, fn.file_path FROM nodes n "
"JOIN edges e ON e.target_id = n.id AND e.type = 'LISTENS_ON' "
"JOIN nodes fn ON fn.id = e.source_id "
"WHERE n.project = ?1 AND n.name = ?2 AND n.label = 'Channel' LIMIT 1",
CBM_NOT_FOUND, &tq, NULL) != SQLITE_OK) {
return false;
}
sqlite3_bind_text(tq, SKIP_ONE, tgt_project, CBM_NOT_FOUND, SQLITE_STATIC);
sqlite3_bind_text(tq, PAIR_LEN, channel_name, CBM_NOT_FOUND, SQLITE_STATIC);
bool matched = false;
if (sqlite3_step(tq) == SQLITE_ROW) {
int64_t tgt_channel_id = sqlite3_column_int64(tq, 0);
int64_t listener_id = sqlite3_column_int64(tq, SKIP_ONE);
const char *listener_name = (const char *)sqlite3_column_text(tq, PAIR_LEN);
const char *listener_file = (const char *)sqlite3_column_text(tq, CR_COL_3);
/* Forward edge: emitter → local Channel */
char fwd[CR_PROPS_BUF];
build_cross_props(fwd, sizeof(fwd), tgt_project, listener_name ? listener_name : "",
listener_file ? listener_file : "", channel_name, "channel_name",
transport);
insert_cross_edge(src_store, src_project, emitter_id, channel_id, "CROSS_CHANNEL", fwd);
/* Reverse edge: listener → target Channel */
char caller_name[CBM_SZ_256] = {0};
char caller_file[CBM_SZ_512] = {0};
lookup_node_info(cbm_store_get_db(src_store), emitter_id, caller_name, sizeof(caller_name),
caller_file, sizeof(caller_file));
char rev[CR_PROPS_BUF];
build_cross_props(rev, sizeof(rev), src_project, caller_name, caller_file, channel_name,
"channel_name", transport);
insert_cross_edge(tgt_store, tgt_project, listener_id, tgt_channel_id, "CROSS_CHANNEL",
rev);
matched = true;
}
sqlite3_finalize(tq);
return matched;
}
static int match_channels(cbm_store_t *src_store, const char *src_project, cbm_store_t *tgt_store,
const char *tgt_project) {
struct sqlite3 *src_db = cbm_store_get_db(src_store);
if (!src_db) {
return 0;
}
sqlite3_stmt *s = NULL;
if (sqlite3_prepare_v2(src_db,
"SELECT DISTINCT n.id, n.name, n.qualified_name, n.properties, "
"e.source_id FROM nodes n "
"JOIN edges e ON e.target_id = n.id AND e.type = 'EMITS' "
"WHERE n.project = ?1 AND n.label = 'Channel'",
CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(s, SKIP_ONE, src_project, CBM_NOT_FOUND, SQLITE_STATIC);
int count = 0;
while (sqlite3_step(s) == SQLITE_ROW && count < CR_MAX_EDGES) {
const char *channel_name = (const char *)sqlite3_column_text(s, SKIP_ONE);
const char *channel_qn = (const char *)sqlite3_column_text(s, PAIR_LEN);
if (!channel_name || !channel_qn) {
continue;
}
int64_t channel_id = sqlite3_column_int64(s, 0);
const char *channel_props = (const char *)sqlite3_column_text(s, CR_COL_3);
int64_t emitter_id = sqlite3_column_int64(s, CR_COL_4);
char transport[CBM_SZ_64] = {0};
json_str_prop(channel_props, "transport", transport, sizeof(transport));
if (try_match_channel_listener(src_store, src_project, tgt_store, tgt_project, channel_name,
transport, emitter_id, channel_id)) {
count++;
}
}
sqlite3_finalize(s);
return count;
}
/* ── Phase D: Generic route-type matcher (gRPC, GraphQL, tRPC) ──── */
/* Look up a node's qualified_name by id. Returns true if found. */
static bool lookup_node_qn(struct sqlite3 *db, int64_t node_id, char *out, size_t out_sz) {
out[0] = '\0';
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db, "SELECT qualified_name FROM nodes WHERE id = ?1", CBM_NOT_FOUND, &st,
NULL) != SQLITE_OK) {
return false;
}
sqlite3_bind_int64(st, SKIP_ONE, node_id);
bool found = false;
if (sqlite3_step(st) == SQLITE_ROW) {
const char *qn = (const char *)sqlite3_column_text(st, 0);
if (qn) {
snprintf(out, out_sz, "%s", qn);
found = true;
}
}
sqlite3_finalize(st);
return found;
}
/* Match edges of a given type against Route nodes with a given QN prefix.
* Reuses the same infrastructure as HTTP/async matching. */
static int match_typed_routes(cbm_store_t *src_store, const char *src_project,
cbm_store_t *tgt_store, const char *tgt_project,
const char *edge_type, const char *svc_key, const char *method_key,
const char *cross_edge_type) {
struct sqlite3 *src_db = cbm_store_get_db(src_store);
if (!src_db) {
return 0;
}
char sql[CBM_SZ_256];
snprintf(sql, sizeof(sql),
"SELECT e.source_id, e.target_id, e.properties FROM edges e "
"WHERE e.project = ?1 AND e.type = '%s'",
edge_type);
sqlite3_stmt *s = NULL;
if (sqlite3_prepare_v2(src_db, sql, CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(s, SKIP_ONE, src_project, CBM_NOT_FOUND, SQLITE_STATIC);
int count = 0;
while (sqlite3_step(s) == SQLITE_ROW && count < CR_MAX_EDGES) {
int64_t caller_id = sqlite3_column_int64(s, 0);
int64_t route_id = sqlite3_column_int64(s, SKIP_ONE);
const char *props = (const char *)sqlite3_column_text(s, PAIR_LEN);
char svc_val[CBM_SZ_256] = {0};
char meth_val[CBM_SZ_256] = {0};
json_str_prop(props, svc_key, svc_val, sizeof(svc_val));
json_str_prop(props, method_key, meth_val, sizeof(meth_val));
if (!svc_val[0] && !meth_val[0]) {
continue;
}
/* Look up the Route QN from the target node (already points to the Route). */
char route_qn[CR_QN_BUF] = {0};
if (!lookup_node_qn(src_db, route_id, route_qn, sizeof(route_qn))) {
continue;
}
char handler_name[CBM_SZ_256] = {0};
char handler_file[CBM_SZ_512] = {0};
int64_t handler_id =
find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
handler_file, sizeof(handler_file));
if (handler_id == 0) {
continue;
}
emit_cross_route_bidirectional(src_store, src_project, src_db, caller_id, route_id,
tgt_store, tgt_project, handler_id, route_qn, handler_name,
handler_file, svc_val, svc_key, cross_edge_type);
count++;
}
sqlite3_finalize(s);
return count;
}
/* ── Collect target projects ─────────────────────────────────────── */
/* When target_projects = ["*"], scan the cache directory for all .db files. */
static int collect_all_projects(char ***out) {
const char *dir = cr_cache_dir();
cbm_dir_t *d = cbm_opendir(dir);
if (!d) {
*out = NULL;
return 0;
}
int cap = CR_INIT_CAP;
int count = 0;
char **projects = malloc((size_t)cap * sizeof(char *));
cbm_dirent_t *ent;
while ((ent = cbm_readdir(d)) != NULL) {
size_t len = strlen(ent->name);
if (len < CR_COL_4 || strcmp(ent->name + len - CR_DB_EXT_LEN, ".db") != 0) {
continue;
}
if (strstr(ent->name, "_cross_repo") || strstr(ent->name, "_config")) {
continue;
}
if (strstr(ent->name, "-wal") || strstr(ent->name, "-shm")) {
continue;
}
if (count >= cap) {
cap *= PAIR_LEN;
char **tmp = realloc(projects, (size_t)cap * sizeof(char *));
if (!tmp) {
break;
}
projects = tmp;
}
/* Strip .db extension */
projects[count] = malloc(len - PAIR_LEN);
memcpy(projects[count], ent->name, len - CR_DB_EXT_LEN);
projects[count][len - CR_DB_EXT_LEN] = '\0';
count++;
}
cbm_closedir(d);
*out = projects;
return count;
}
static void free_project_list(char **projects, int count) {
for (int i = 0; i < count; i++) {
free(projects[i]);
}
free(projects);
}
/* ── Entry point ─────────────────────────────────────────────────── */
cbm_cross_repo_result_t cbm_cross_repo_match(const char *project, const char **target_projects,
int target_count) {
cbm_cross_repo_result_t result = {0};
struct timespec t0;
clock_gettime(CLOCK_MONOTONIC, &t0);
/* Open source project store (read-write) */
char src_path[CR_PATH_BUF];
cr_db_path(project, src_path, sizeof(src_path));
cbm_store_t *src_store = cbm_store_open_path(src_path);
if (!src_store) {
return result;
}
/* Clean existing CROSS_* edges for this project */
delete_cross_edges(src_store, project);
/* Resolve target projects */
char **resolved = NULL;
int resolved_count = 0;
bool own_list = false;
if (target_count == SKIP_ONE && strcmp(target_projects[0], "*") == 0) {
resolved_count = collect_all_projects(&resolved);
own_list = true;
} else {
resolved = (char **)target_projects;
resolved_count = target_count;
}
/* Match against each target */
for (int i = 0; i < resolved_count; i++) {
const char *tgt = resolved[i];
if (strcmp(tgt, project) == 0) {
continue; /* skip self */
}
char tgt_path[CR_PATH_BUF];
cr_db_path(tgt, tgt_path, sizeof(tgt_path));
/* Open target store read-write (for bidirectional edge writes) */
cbm_store_t *tgt_store = cbm_store_open_path(tgt_path);
if (!tgt_store) {
continue;
}
result.http_edges += match_http_routes(src_store, project, tgt_store, tgt);
/* Reverse direction: when this pass runs from the provider side, the
* consumer's HTTP_CALLS live in tgt, not src — the forward pass above
* finds nothing because the provider has no outbound calls. This also
* re-creates the provider-side reverse edges that delete_cross_edges
* just wiped, which a provider-side run previously destroyed for good.
* A caller edge lives in exactly one DB, so the two directions scan
* disjoint edge sets and never double-count a pair; the store's
* (source, target, type) upsert keeps re-recorded rows unique. (#523) */
result.http_edges += match_http_routes(tgt_store, tgt, src_store, project);
result.async_edges += match_async_routes(src_store, project, tgt_store, tgt);
result.channel_edges += match_channels(src_store, project, tgt_store, tgt);
result.grpc_edges += match_typed_routes(src_store, project, tgt_store, tgt, "GRPC_CALLS",
"service", "method", "CROSS_GRPC_CALLS");
result.graphql_edges +=
match_typed_routes(src_store, project, tgt_store, tgt, "GRAPHQL_CALLS", "operation",
"operation", "CROSS_GRAPHQL_CALLS");
result.trpc_edges += match_typed_routes(src_store, project, tgt_store, tgt, "TRPC_CALLS",
"procedure", "procedure", "CROSS_TRPC_CALLS");
result.projects_scanned++;
cbm_store_close(tgt_store);
}
cbm_store_close(src_store);
if (own_list) {
free_project_list(resolved, resolved_count);
}
struct timespec t1;
clock_gettime(CLOCK_MONOTONIC, &t1);
result.elapsed_ms = ((double)(t1.tv_sec - t0.tv_sec) * CR_MS_PER_SEC) +
((double)(t1.tv_nsec - t0.tv_nsec) / CR_NS_PER_MS);
int total = result.http_edges + result.async_edges + result.channel_edges + result.grpc_edges +
result.graphql_edges + result.trpc_edges;
cbm_log_info("cross_repo.done", "project", project, "total", cr_itoa(total));
return result;
}