Repository navigation
trace_path reports callers_total: 0 with relation: eq for injected-receiver calls; the resolver already records the unresolved site but drops it before any output #2265
Description
Version
codebase-memory-mcp 0.11.0
Platform
Windows (x64)
Install channel
GitHub release archive / install.ps1
Binary variant
ui
What happened, and what did you expect?
When a function is invoked as a method on an injected receiver — a destructured factory parameter, this.dep inside a class, or an object of functions passed as an argument — trace_path returns callers_total: 0 with callers_total_relation: eq, and the outbound direction returns callees_total: 0, also eq. Each of these calls has a real caller in the same repository.
I am not asking for this call shape to be resolved — that is already tracked in #514 and #1354. I am reporting something narrower and, I believe, cheaper:
- The TS/JS resolver already produces a record for each of these sites, with a reason and exact source span. That record never reaches the graph, the coverage report, or any visible log. It is dropped in memory.
- Meanwhile the public output states the zero is exact.
callers_total_relationonly flips togteon pagination (mcp.c:9394,tr_in.truncated ? "gte" : "eq"), never on incomplete resolution. check_index_coveragewithdiagnostics: "full"answersno_recorded_issuefor the file containing the unresolved site — the tool meant to expose blind spots does not expose this one.
Expected: a zero produced by failed resolution should be distinguishable from a true zero.
This is the part that #1682 set aside: @pcristin proposed "narrowing … to persisting and exposing unresolved callsite diagnostics" and @DeusData agreed it "can remain a separate fo[llow-up]". I could not find that follow-up, so this is it.
Reproduction
Nine plain JavaScript (ESM) files, no TypeScript, no framework. 35 nodes / 43 edges.
// cliente.js — function inside a factory-returned object
export function crearCliente() {
function buscar(id) { return { id }; }
return { buscar };
}
// buscador.js — control: top-level function
export function buscarPlano(id) { return { id, plano: true }; }
// servicio.js — receiver: destructured parameter
export function crearServicio({ cliente }) {
async function procesar(id) { return cliente.buscar(id); }
return { procesar };
}
// servicio-plano.js — injected receiver, top-level target
export function crearServicioPlano({ buscador }) {
function procesarPlano(id) { return buscador.buscarPlano(id); }
return { procesarPlano };
}
// clase.js — receiver: this.dep
export class ServicioClase {
constructor(cliente) { this.cliente = cliente; }
procesarConClase(id) { return this.cliente.buscar(id); }
}
// servicio-tipado.js — same as servicio.js, parameter typed via JSDoc
/** @param {{ cliente: ReturnType<typeof import("./cliente.js").crearCliente> }} deps */
export function crearServicioTipado({ cliente }) {
function procesarTipado(id) { return cliente.buscar(id); }
return { procesarTipado };
}
// ayudante.js + directo.js — control: import + direct call
export function ayudante(x) { return x + 1; }
import { ayudante } from "./ayudante.js";
export function usarDirecto() { return ayudante(1); }
// arranque.js — composition
import { crearCliente } from "./cliente.js";
import { crearServicio } from "./servicio.js";
const servicio = crearServicio({ cliente: crearCliente() });
servicio.procesar(42);codebase-memory-mcp cli index_repository --repo-path <path> --mode full
codebase-memory-mcp cli trace_path --project <p> --function_name <f> --direction inbound --include_evidence true
codebase-memory-mcp cli trace_path --project <p> --function_name <f> --direction outbound --include_evidence true
codebase-memory-mcp cli check_index_coverage --project <p> --paths servicio.js --diagnostics full
Result
| target | call shape | inbound |
caller's outbound |
actual |
|---|---|---|---|---|
ayudante |
import + direct call | ✅ 1 caller, heuristic 0.95 |
✅ 1 callee | 1 |
buscar |
cliente.buscar() — destructured param |
❌ 0, eq |
❌ procesar: 0, eq |
1 |
buscar |
this.cliente.buscar() — class |
❌ 0, eq |
❌ procesarConClase: 0, eq |
+1 |
buscarPlano |
buscador.buscarPlano() — top-level target |
❌ 0, eq |
❌ procesarPlano: 0, eq |
1 |
buscar |
same, JSDoc-typed param | ❌ 0, eq |
❌ procesarTipado: 0, eq |
+1 |
query_graph with MATCH (a)-[e]->(b:Function) WHERE b.name IN ['buscar','buscarPlano'] RETURN a.name, type(e), b.name returns only DEFINES edges — no usage edge of any confidence.
check_index_coverage for servicio.js, servicio-plano.js, clase.js → status: no_recorded_issue, recommended_action: use_graph_with_best_effort_caveat.
All four injected variants fail identically while the direct call succeeds, which isolates the cause to the injected receiver — not the nested definition, not the absence of types.
Where the data is lost (read on main @ def38f3)
internal/cbm/lsp/ts_lsp.c:2825-2858— forcliente.buscar(id),clientehas a scope binding (:2494-2502binds destructured parameters), so the namespace-import branch is skipped and the call goes to type-based dispatch;ts_eval_expr_typeyields no type,ts_lookup_method_for_callfails, andts_emit_unresolved_call_at(ctx, mname, "method_not_in_registry", call_node)runs (:2858). That creates aCBMResolvedCallwithstrategy = "lsp_unresolved",confidence = 0.0, areason, andsite_start_byte/site_end_byte(:285-300). The site, the method name and the reason all exist at this point.src/pipeline/pass_parallel.c:2716and:3426— any record withconfidence < CBM_LSP_CONFIDENCE_FLOOR(0.6f,lsp_resolve.h:39) is never an edge candidate. Correct as graph policy — but the record is not stored anywhere else either.src/pipeline/pass_calls.c:846—pass.done … "unresolved" Nis emitted viacbm_log_info, but it appears neither in the per-project log (which only carries the coverage report) nor incbm-daemon.log; verified on both after indexing.src/mcp/mcp.c:8184-8188—cbm_mcp_edge_strategy_classalready defines the public class"unresolved". The output vocabulary has the word; nothing ever reaches it because anlsp_unresolvednever becomes an edge.src/store/store.c:303-324—index_coveragestoresparse_partial/parse_unusable/ skip phases, and its own comment says "name a new kind so that distinction stays obvious". That is exactly where this would fit without touching the graph tables.
Proposal (cheapest first; any one of them removes the false zero)
- Stop reporting
eqoncallers_total_relation/callees_total_relationwhen unresolved sites could account for the answer. Foroutbound, it is enough that the caller haslsp_unresolvedrecords (already available per function). Forinbound, that any unresolved record's leaf name matches the function name. Result:gte— or a newunknownvalue ifgteis to stay reserved for pagination. - Persist unresolved sites in
index_coverageas a newkind, e.g.unresolved_calls, withdetail = "line:leaf_name:reason,…". It matches the table's declared design (metadata about the graph, kept separate from it) and makescheck_index_coveragestop answeringno_recorded_issuefor the affected file, with a recommended action such asread_source_and_verify_calls. - Have
trace_pathexpose a count and rows of unresolved sites (unresolved_sites_total, with file, line, leaf name andreason), using the"unresolved"class that already exists incbm_mcp_edge_strategy_class. Thereasonvalues already emitted by TS/JS aremethod_not_in_registry,func_not_in_registry,import_symbol_not_in_registry,module_symbol_not_in_registry.
Why it matters
We run cbm with autonomous agents on a Node server built with factory injection (crearX({ dep })) — exactly this shape. "This function has no callers" is the answer that leads an agent to conclude code is dead. We currently mitigate with an operating rule ("confirm any zero with grep"), but that depends on the agent remembering. If the output said "0 resolved, N unresolved", the rule would be unnecessary.
If the maintainers agree on a shape for option 2, I would like to attempt a PR: the data is already in CBMResolvedCall, and both the table and the coverage report already exist.
Logs
Per-project index log after index_repository (MCP mode) contains only the coverage report — no pass.done line and no unresolved count:
# codebase-memory-mcp index coverage report
# project=<p> skipped=0 parse_partial=0
# columns: phase reason path
cbm-daemon.log for the same run: no pass.done pass=calls line either.
Project scale (if relevant)
35 nodes / 43 edges / 9 files (reproduction). Also observed on a 13,094-node / 31,523-edge Node repository.
Confirmations
- I searched existing issues and this is not a duplicate. trace_path data_flow mode doesn't surface arg expressions; NestJS DI patterns defeat ~70% of caller resolution #514, TypeScript: cross-file method calls produce no CALLS edge — type-aware tier resolves same-file calls only, so trace_path(inbound) returns 0 callers #1354 and TypeScript: CALLS edges silently missing for some directly-imported functions — trace_path reports callers_total: 0 (v0.10.5) #1682 cover resolution of these calls; none covers observability of the unresolved site, which TypeScript: CALLS edges silently missing for some directly-imported functions — trace_path reports callers_total: 0 (v0.10.5) #1682 explicitly left as a separate follow-up.
- My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Activity
pcristin commentedon Sep 24, 2026
Thanks for the detailed repro, @BryanQuiceno. The unresolved call records were getting dropped at the point you identified, while
trace_pathcould still report an exact zero.I opened #2305 to keep those TS/JS call sites in per-file index coverage and show them through
check_index_coverage. It also reports the affected CALLS total asunknownwhen unresolved sites mean we cannot know the exact count. It does not add guessed CALLS edges or try to resolve injected receivers.Your examples made it possible to keep this focused on the misleading zero. I would appreciate a look at the coverage output in the PR to see whether it gives your agents enough context to check the source when resolution falls short.
DeusData commentedon Oct 1, 2026
Thank you for the focused reproduction, and thank you @pcristin for linking #2305. I checked the trace output on main at c61b380: src/mcp/mcp.c still chooses callers_total_relation and callees_total_relation solely from the trace truncation flags (lines 9624-9635). That qualifies counts of recorded graph results, not proof that all source-level calls were resolved. Keeping this open as a high-priority output/coverage issue and linking #2305 as the proposed implementation, subject to review. This is distinct from making injected receivers resolve, and it should not be closed merely because a resolver improves. The next review is on our side; thank you for separating the two concerns.