cross-repo-intelligence resolves an outbound HTTP call to a CROSS_HTTP_CALLS edge by matching the URL path string against routes exposed by other indexed projects, without preferring the caller's own project. When the calling repo defines the same path itself — which it usually does, since that is what its own frontends call — the tool emits a cross-service edge that corresponds to no real cross-service call.
The result is reported as total_cross_edges: <N>, with nothing in the output distinguishing "this call leaves the service" from "another indexed project happens to expose the same path".
Split out of #1133 at @HungChu's suggestion; that issue is about the --target-projects flag parsing to zero targets and the non-deterministic worker crash, which are separate problems. This one is a correctness bug in the output of the runs that succeed.
Environment
- v0.9.0,
ui variant, Windows amd64, pinned binary (SHA-256 verified)
- Source project: 31,287 nodes / 176,218 edges (Python / FastAPI)
- Target project: 13,462 nodes (the source project is an in-progress migration of the target)
Repro
echo '{"repo_path":"<abs path to source repo>",
"mode":"cross-repo-intelligence",
"target_projects":["<target project name>"]}' \
| codebase-memory-mcp cli index_repository
Then audit what it produced:
MATCH (a:Function)-[r:CROSS_HTTP_CALLS]->(b) RETURN a.qualified_name, r.url_path, b.qualified_name
For each url_path, grep the source repo for a route definition on that same path. Every path that the source repo defines itself is a false edge.
What we measured
Output was stable across the 9 successful runs of a 25-invocation matrix (total_cross_edges: 117, identical every time; the graph was not inflated by repetition). Querying the CROSS_HTTP_CALLS edges originating in the source project gave 40, which we audited:
|
|
CROSS_HTTP_CALLS edges originating in the source project |
40 |
obvious noise (httpbin.org/ip) |
3 |
| path collisions |
37, across 20 distinct routes |
| routes hand-audited against the source repo |
7 |
| of those, defined by the source repo itself |
7 / 7 |
| verified genuine cross-service calls |
0 |
Worked example — the most frequent destination, /api/proveedores:
- The source repo defines it itself:
compra/router.py:133, APIRouter(prefix="/api/proveedores").
- The callers the tool attributes to it are that repo's own frontends (
frontend-app, reportes, recepcion-pos, admin) issuing fetch against their own API.
- The repo's only configured pointer at the other service,
LEGACY_API_BASE_URL, has no consumers at all — its own CLAUDE.md states this, and grep confirms it.
So the ground truth is zero HTTP calls from this repo to the target, and the tool reported 117 cross edges.
Why this is worse than a wrong number
We caught it only because a CLAUDE.md in the repo asserts that no HTTP client to the legacy service exists, and that contradicted the graph. Without that contradiction sitting in arm's reach, nobody audits a field named total_cross_edges — it reads as an architectural fact, not as a guess. There is no confidence, no candidates, no warning in the output; a false edge is presented exactly like a true one.
This is the same failure shape as the projects_scanned: 0 / status: success case in #1133: the tool is not wrong loudly, it is wrong in the register of a correct answer.
The precondition is ordinary, not exotic
Our pair is close to a worst case — the two projects share routes by construction, since one is the migration of the other — so it would be fair to ask whether this only bites unusual setups. It does not. The precondition is just two indexed projects exposing the same path.
@HungChu's fleet is the better example precisely because there is nothing unusual about it: four independently-developed NestJS services in one store. An earlier version of this section described that fleet as "every business controller serves /api/v1/<resource>, with an overlapping resource vocabulary" — that was wrong, and the corrected inventory they posted below in this thread is a stronger example, not a weaker one. Versioning there is per-controller with defaultVersion unset, so 26 of 32 controllers carry no version segment at all. The version segment was never what creates the collision; sharing a full path is:
| path |
services declaring it |
how it resolves |
GET /health |
all 4 |
@Controller('health') in each, and prefix-excluded in each ⇒ byte-identical /health in four indexed projects |
GET / |
2 |
also prefix-excluded in both |
/api/projects |
2 |
@Controller('projects'), unversioned in both |
/api/aifunction |
2 |
@Controller('aifunction'), unversioned in both |
GET /health is the one worth putting in the acceptance criteria. It carries no assumptions about the fleet's domain: every service in every fleet exposes a health endpoint, and the idiomatic NestJS way to keep it off a versioned prefix is exactly that exclude, which makes the paths identical rather than merely similar. Any two services in one store collide there. /api/projects and /api/aifunction are the business-resource case — two services with no shared history that happen to expose a resource of the same name.
⚠️ Those four are predicted collisions, read statically from the decorators — not measured edges. #1428 means those routes produce no Route nodes, so that fleet reports total_cross_edges: 0 today and there is nothing in it to audit. The 7/7 hand-audit above remains the only measured evidence in either fleet. Stating this plainly at @HungChu's request, so that nobody reproduces their side, finds zero edges, and concludes the example was inflated.
The honest summary of the pair: one fleet where the bug is measured (FastAPI, 37 of 40 edges false, 7/7 audited routes locally defined), one where the preconditions are verifiable in source but masked by a separate extraction gap (NestJS, 4 collision paths including /health across all four services). That pair is what makes the ordering hazard below concrete.
Ordering hazard with #1428 — please read this before scheduling either fix
This is @HungChu's observation and I think it is the most consequential thing in the #1133 thread.
Right now our two fleets are the two states of this bug at the same moment:
If #1428 (or any other framework's route extraction) is fixed before route-ownership resolution, every affected fleet flips straight from the first state to the second, with no intermediate state in which anyone would notice. Zero edges at least looks like nothing happened. A few hundred plausible-looking edges read as an architectural fact and get believed.
Two consequences for acceptance criteria:
- Route-ownership resolution should land before or together with any new framework's route extraction, not after.
- This is not a NestJS-specific concern. Any improvement to route extraction, for any framework, performs the same flip. It is a precondition for the route-extraction roadmap as a whole, not a dependency of one issue.
Proposed fixes
(1) Prefer the caller's own project when resolving. Emit a cross-repo edge only when the path is not defined locally in the calling project. This alone removes 37 of our 40 edges and all 7 of the audited false ones, and it is the behaviour a reader already assumes from the field name.
(2) Surface ambiguity on the edge, for the genuinely ambiguous case where two projects define a path and the caller defines neither. CALLS edges already carry confidence / candidates / strategy, so this is plumbing existing fields through a new edge type rather than new infrastructure. Worth having independently of (1).
A third, cheap and orthogonal: since a path-only match is a heuristic, the summary field could say so — total_cross_edges reads as a count of facts. Even unverified_cross_edges, or a per-edge match: "path-only", would move it from fact to candidate without any resolution work.
Note on the 3 httpbin.org/ip edges
Distinct from the collisions and much smaller: those come from test/example code and matched on an absolute external URL. Not the point of this issue, but worth knowing they are in the 40 if anyone reproduces the count.
cross-repo-intelligenceresolves an outbound HTTP call to aCROSS_HTTP_CALLSedge by matching the URL path string against routes exposed by other indexed projects, without preferring the caller's own project. When the calling repo defines the same path itself — which it usually does, since that is what its own frontends call — the tool emits a cross-service edge that corresponds to no real cross-service call.The result is reported as
total_cross_edges: <N>, with nothing in the output distinguishing "this call leaves the service" from "another indexed project happens to expose the same path".Split out of #1133 at @HungChu's suggestion; that issue is about the
--target-projectsflag parsing to zero targets and the non-deterministic worker crash, which are separate problems. This one is a correctness bug in the output of the runs that succeed.Environment
uivariant, Windows amd64, pinned binary (SHA-256 verified)Repro
Then audit what it produced:
For each
url_path, grep the source repo for a route definition on that same path. Every path that the source repo defines itself is a false edge.What we measured
Output was stable across the 9 successful runs of a 25-invocation matrix (
total_cross_edges: 117, identical every time; the graph was not inflated by repetition). Querying theCROSS_HTTP_CALLSedges originating in the source project gave 40, which we audited:CROSS_HTTP_CALLSedges originating in the source projecthttpbin.org/ip)Worked example — the most frequent destination,
/api/proveedores:compra/router.py:133,APIRouter(prefix="/api/proveedores").frontend-app,reportes,recepcion-pos,admin) issuingfetchagainst their own API.LEGACY_API_BASE_URL, has no consumers at all — its ownCLAUDE.mdstates this, and grep confirms it.So the ground truth is zero HTTP calls from this repo to the target, and the tool reported 117 cross edges.
Why this is worse than a wrong number
We caught it only because a
CLAUDE.mdin the repo asserts that no HTTP client to the legacy service exists, and that contradicted the graph. Without that contradiction sitting in arm's reach, nobody audits a field namedtotal_cross_edges— it reads as an architectural fact, not as a guess. There is noconfidence, nocandidates, no warning in the output; a false edge is presented exactly like a true one.This is the same failure shape as the
projects_scanned: 0/status: successcase in #1133: the tool is not wrong loudly, it is wrong in the register of a correct answer.The precondition is ordinary, not exotic
Our pair is close to a worst case — the two projects share routes by construction, since one is the migration of the other — so it would be fair to ask whether this only bites unusual setups. It does not. The precondition is just two indexed projects exposing the same path.
@HungChu's fleet is the better example precisely because there is nothing unusual about it: four independently-developed NestJS services in one store. An earlier version of this section described that fleet as "every business controller serves
/api/v1/<resource>, with an overlapping resource vocabulary" — that was wrong, and the corrected inventory they posted below in this thread is a stronger example, not a weaker one. Versioning there is per-controller withdefaultVersionunset, so 26 of 32 controllers carry no version segment at all. The version segment was never what creates the collision; sharing a full path is:GET /health@Controller('health')in each, and prefix-excluded in each ⇒ byte-identical/healthin four indexed projectsGET //api/projects@Controller('projects'), unversioned in both/api/aifunction@Controller('aifunction'), unversioned in bothGET /healthis the one worth putting in the acceptance criteria. It carries no assumptions about the fleet's domain: every service in every fleet exposes a health endpoint, and the idiomatic NestJS way to keep it off a versioned prefix is exactly thatexclude, which makes the paths identical rather than merely similar. Any two services in one store collide there./api/projectsand/api/aifunctionare the business-resource case — two services with no shared history that happen to expose a resource of the same name.Routenodes, so that fleet reportstotal_cross_edges: 0today and there is nothing in it to audit. The 7/7 hand-audit above remains the only measured evidence in either fleet. Stating this plainly at @HungChu's request, so that nobody reproduces their side, finds zero edges, and concludes the example was inflated.The honest summary of the pair: one fleet where the bug is measured (FastAPI, 37 of 40 edges false, 7/7 audited routes locally defined), one where the preconditions are verifiable in source but masked by a separate extraction gap (NestJS, 4 collision paths including
/healthacross all four services). That pair is what makes the ordering hazard below concrete.Ordering hazard with #1428 — please read this before scheduling either fix
This is @HungChu's observation and I think it is the most consequential thing in the #1133 thread.
Right now our two fleets are the two states of this bug at the same moment:
@Controller/@Getdecorator routes are not extracted, so there are noRoutenodes, so no collisions are possible:total_cross_edges: 0. Silently zero. The four colliding paths listed above sit in their source right now, masked by the extraction gap — predicted, not measured.If #1428 (or any other framework's route extraction) is fixed before route-ownership resolution, every affected fleet flips straight from the first state to the second, with no intermediate state in which anyone would notice. Zero edges at least looks like nothing happened. A few hundred plausible-looking edges read as an architectural fact and get believed.
Two consequences for acceptance criteria:
Proposed fixes
(1) Prefer the caller's own project when resolving. Emit a cross-repo edge only when the path is not defined locally in the calling project. This alone removes 37 of our 40 edges and all 7 of the audited false ones, and it is the behaviour a reader already assumes from the field name.
(2) Surface ambiguity on the edge, for the genuinely ambiguous case where two projects define a path and the caller defines neither.
CALLSedges already carryconfidence/candidates/strategy, so this is plumbing existing fields through a new edge type rather than new infrastructure. Worth having independently of (1).A third, cheap and orthogonal: since a path-only match is a heuristic, the summary field could say so —
total_cross_edgesreads as a count of facts. Evenunverified_cross_edges, or a per-edgematch: "path-only", would move it from fact to candidate without any resolution work.Note on the 3
httpbin.org/ipedgesDistinct from the collisions and much smaller: those come from test/example code and matched on an absolute external URL. Not the point of this issue, but worth knowing they are in the 40 if anyone reproduces the count.