Skip to content

cross-repo-intelligence: CROSS_HTTP_CALLS resolves by URL path alone, emitting false cross-service edges when two indexed projects expose the same path #1459

Description

@junk151516

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:

  1. Route-ownership resolution should land before or together with any new framework's route extraction, not after.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    cypherCypher query language parser/executor bugsparsing/qualityGraph extraction bugs, false positives, missing edgesstability/performanceServer crashes, OOM, hangs, high CPU/memoryux/behaviorDisplay bugs, docs, adoption UXwindowsWindows-specific issues

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions