Repository navigation
Unresolvable calls are bound to any same-named symbol (Jest describe, RxJS take) and trace_path exposes no confidence/strategy #1355
Description
Activity
- addedparsing/qualityGraph extraction bugs, false positives, missing edgesGraph extraction bugs, false positives, missing edgeswindowsWindows-specific issuesWindows-specific issues
on Jul 30, 2026 I need to correct two bullets in the body that I got wrong, and in the process I found what I think is the actual root cause — it's more specific and more actionable than what I originally filed.
The headline holds up
Re-verified independently: of 40 sampled inbound edges to
…ScheduleOpenShiftViewComponent.take, 40/40 land on a source line literally containingtake(, in files importingtakefromrxjs. Of 30 sampled edges into the twodescribemethods, 30/30 land on adescribe(line in a spec file. So the misattribution is real and not a misreading of the call sites. (Two small figure corrections:takeis imported from'rxjs/operators'in 228 files and from any'rxjs*'specifier in 327; there are 796.spec.tsfiles, not 794.)trace_pathexposing no confidence or strategy is also confirmed — no column intreeorjsonformat, no--min-confidenceflag.Correction 1: my two "not calls at all" bullets are wrong about the mechanism
I claimed an
@Input() set date(...)setter declaration was recorded as a call, with the edge'slinepointing at the declaration. That is not what happens. In the case I looked at, line 98 is the setter declaration and line 99 — the line the edge actually points to — is a real call,const updatedDate = parseDate(date);. I misread the offset and then generalised from it. Same error on thetimesheetbullet: pullingr.calleefor all edges into nodes namedtimesheetgives real function names (getTimesheet6,hasTimesheetPermission3,sumApprovedTimesheets3, …), allimport_map@ 0.95 — not the property access I described.Please disregard both bullets, and my "Expected #2" ("never emit a CALLS edge for a setter declaration or a property access") — it asks for the wrong fix.
There is a smaller property-access class, but it's a different strategy and I should have separated it: of the 264 edges into
PersonalScheduleComponent.date, 19 haver.callee = "this.date"viasuffix_match@ 0.55, and one of thetimesheetedges isr.callee = "this.timesheet", alsosuffix_match@ 0.55. So barethis.<prop>receivers do become call edges, but at low confidence and in small numbers — not the main event.The actual root cause:
import_mapdiscards the imported identifierThis is the part worth acting on.
import_mapappears to reduce the import target to a path segment — a directory name, or a dot-segment of a dotted filename — and then binds the imported symbol to any node whose qualified name ends in that segment, ignoring which identifier was actually imported. Grouping allimport_mapedges byr.calleeagainst the node they landed on:imported symbol ( r.callee)node it was bound to edges parseDatedate145 hasPermissionpermission136 getDayListWithoutTotalsemployee41 permissionDepartmentspermission24 getTimesheettimesheet6 AppStateBuilderAppStateBuilder102 ✅ mergeEntitiesmergeEntities32 ✅ Where the imported name happens to coincide with the segment the edge is correct; otherwise the call is attributed to an unrelated symbol that merely shares a name with a directory or a
*.helper.ts/*.service.tsfilename stem.Why this matters more than the
unique_namecases I led with:import_mapfires at confidence 0.95, which is exactly why filtering on confidence doesn't clean up the ranking — thedatecluster survives>= 0.9.- It's triggered by ordinary Angular file-naming conventions (
date.helper.ts,permission-util.ts, feature folders), so it will hit essentially every Angular codebase.
Revised ask, replacing my original #2:
import_mapshould key on the imported identifier and must not reduce a module path to a bare directory or dot-segment.Correction 2: "the entire top of the inbound-call ranking is fabricated" overstates it
The real unfiltered top 10 is:
project.inject 1627 ScheduleOpenShiftViewComponent.take 457 integrations-ui.component.stories.Input 383 ReportService.describe 377 no-readonly.filter 352 provideTranslateServiceTesting 345 ReportApi.describe 275 project.input 265 PersonalScheduleComponent.date 264 PermissionDirective.permission 207provideTranslateServiceTestingis a genuinely-called test helper, and I haven't characterisedproject.inject/project.input. "Several of the top 10", not "the entire top" — I'd filtered the ranking and then described the filtered result as though it were the whole.On my third ask
Exposing strategy/confidence through
trace_pathoverlaps #786 (MCP freshness/provenance evidence for client trust decisions) — happy to have it folded in there rather than tracked here.Likely shared code site with #1354
cbm_tsjs_suppress_weak_method_match(is_tsjs, is_method, strategy)insrc/pipeline/registry.c(called frompass_calls.c/pass_parallel.c) is asserted intests/test_registry.cto return true for("unique_name", is_method=true)— i.e. weak matches should already be suppressed for TS member calls. Yet the cross-fileMethod→Methodpopulation contains 557unique_name+ 215suffix_matchedges. Either those sites aren't flaggedis_methodor the guard isn't reached, which would explain both the fabricated edges here and the missing ones in #1354.
Posted by Claude Code on behalf of @artaommahe
- addededitor/integrationEditor compatibility and CLI integrationEditor compatibility and CLI integration
on Jul 30, 2026 - addedbugSomething isn't workingSomething isn't workingpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.and removedwindowsWindows-specific issuesWindows-specific issues
on Aug 3, 2026 Thank you for refining the diagnosis from namespace matching to the discarded imported identifier and generic same-name fallback. Current main now exposes edge provenance when evidence is requested, which addresses the missing-strategy visibility portion, but it does not eliminate the false
CALLSedges. PR #1386 is a useful partial fix for weak generic names; the broader binding bug remains open at high priority in0.9.1-rc.Reacted by Maksim PopovEnvironment: v0.9.0 (also reproduced after the 2026-08-10 18:05 self-update), Claude Code 2.1.220
installMethod: native, macOS 26.5.2 (25F84) arm64.Adding a third language to this pattern, plus a detail on how the arbitrary pick behaves at scale.
C#:
XDocument.Savebound to a controller actionProject: 193
.csfiles, 2,391 nodes / 5,768 edges, indexed atmode: moderate.trace_path(function_name="GenerateXmlFiles", mode="calls", direction="both", depth=3)returns
FinanceValidationController.Saveas a hop-2 callee. No such call exists.Ground truth:
ConvertXMLService.cs:95callsdoc.Save(writer)—XDocument.SavefromSystem.Xml.Linq. The repository defines 30 methods namedSave, almost allpublic IActionResult Save([FromBody] …)across controllers. The extractor could not resolve the external member and bound the edge to one of the 30.Confirmed directly:
MATCH (a)-[r:CALLS]->(b) WHERE b.name = 'Save' RETURN a.qualified_name, a.file_path, b.qualified_name, b.file_pathOne row:
ConvertXMLService.GenerateXmlForBatch→FinanceValidationController.Save. Cross-file, and fabricated.So the same failure appears in C# (this), JS/TS (this issue's Jest
describeand RxJStake), Python (#1276) and PHP (#1186). It looks like a resolver-wide behaviour rather than a per-language quirk.On the "no confidence/strategy" half of this issue
Two adjacent behaviours already work correctly and suggest a consistent fix:
trace_pathon an ambiguous bare name returnsstatus: ambiguouswith a list of qualified names rather than guessing. On a symbol with 66 definitions it refused to merge them.- Same-file calls bind to the calling file's own definition and are correct.
Neither protects against the external-member case, but the first is arguably the right model for it. Where an unresolved call has more than one local candidate, the same
ambiguoustreatment would be consistent with existing behaviour rather than new machinery.The schema also already carries
CALL_REFERENCEalongsideCALLS. An unresolved target emitted asCALL_REFERENCE, or as no edge, would let a consumer distinguish "resolved" from "guessed" without a new confidence field.Why this is worth more than a missing edge
A missing edge sends the agent back to grep, which is slower. A fabricated edge arrives structured, with a qualified name and a file path, looking exactly like a finding. In our case it survived several minutes of review before someone checked the source.
A cheap detection heuristic for users, until this is fixed
Risk scales with the intersection of local method names and the member names the code calls on imported objects. Anyone can compute it without the graph:
MATCH (a)-[r:CALLS]->(b) WHERE b.name IN [<library member names you actually call>] RETURN b.name, a.file_path, b.file_pathCross-file rows are candidates for fabrication. Same-file rows are usually genuine.
- added a parent issue
on Aug 10, 2026 Thank you for adding the C# reproduction. The current resolver still includes suffix and same-name fallback paths, so this is useful evidence that the false binding is cross-language rather than isolated to JavaScript or TypeScript. I have linked the issue under the graph-query and resolution epic and retained high priority in the immediate bug train.
Still reproduces on 0.10.8 (this issue is milestoned
0.10.0-rc). Adding two TypeScript sub-cases that reduce more sharply than the test-runner-global case, and one filtering result that cuts against the obvious workaround.Sub-case 1: the callee is a parameter of the enclosing function
The tightest form of this bug I've been able to produce. No globals, no library, no imports — the callee is a
Promiseexecutor parameter, declared two lines above the call site, lexically in scope:// src/loader.ts — no import of `reject` anywhere in the project export function loadText(url: string): Promise<string> { return new Promise<string>((resolve, reject) => { if (!url) { reject(new Error('empty url')); // line 5 return; } resolve(url); }); }
// src/view-model.ts — the ONLY declaration named `reject` in the project export class LoginViewModel { private reject(msg: string): Promise<void> { console.error(msg); return Promise.resolve(); } submit(name: string): Promise<void> { if (!name) return this.reject('name required'); return Promise.resolve(); } }
Observed:
cbm-repro.src.loader.loadText reject cbm-repro.src.view-model.LoginViewModel.reject unique_name "0.75" line 5The executor's parameters are not represented in the graph at all:
MATCH (n) WHERE n.name IN ['reject','resolve'] RETURN labels(n), n.qualified_name ["Method"] cbm-repro.src.view-model.LoginViewModel.reject total: 1and
loadText's ownparam_namesis["url"]— the arrow function's(resolve, reject)are nowhere. Sorejecthas nothing in scope to bind to, and the fallback reaches across the project into aprivatemethod of an unrelated class. Everything needed to not do that was in the same function body.Note the third edge in the same repro resolves correctly:
submit → this.rejectvialsp_ts_method@ 0.95. So the LSP pass is running on these files; the fallback simply fires anyway once the LSP declines to bind.Sub-case 2: qualified callee whose qualifier is ambient, bound to an
interface// node_modules/@fake/engine-types/index.d.ts — ambient, outside indexed source declare namespace native { class Manifest { constructor(content: string, root?: string); } }
// src/manifest-type.ts — the ONLY declaration named `Manifest` in indexed source export interface Manifest { version: string; assets: Record<string, string>; }
// src/updater.ts — calls the ambient global, unrelated to ./manifest-type export function makeManifest(content: string): unknown { return new native.Manifest(content, '/tmp'); }
Observed:
cbm-repro.src.updater.makeManifest native.Manifest cbm-repro.src.manifest-type.Manifest unique_name "0.75" line 3Two things here:
r.calleeis recorded asnative.Manifest, i.e. the resolver knows the call was qualified — and still drops the qualifier to match a bare name. If the qualifier can't be resolved, the bare-name match on the last segment should not be attempted.- The target is an
Interfacenode. ACALLSedge into a type declaration can never be a real call in any language the tool indexes. That looks like a cheap structural invariant to enforce at edge-write time, independent of how the binding bug is ultimately fixed — it would also have caught the same class of edge in the C# and Python reports above without any resolver change.
Control: moving that
declare namespace nativeinto the indexed source changes the edge toqualified_suffix@ 0.90 pointing at the ambient declaration, which is correct. So the failure is specifically "qualifier resolves to nothing indexable".Repro for both:
mkdir -p repro/src repro/node_modules/@fake/engine-types && cd repro printf '{ "name": "cbm-repro", "private": true, "type": "module" }\n' > package.json printf '{ "compilerOptions": { "target": "ES2021", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "types": ["@fake/engine-types"] } }\n' > tsconfig.json printf '{ "name": "@fake/engine-types", "version": "1.0.0", "types": "index.d.ts" }\n' > node_modules/@fake/engine-types/package.json # ...then the four .ts files above, plus the .d.ts at node_modules/@fake/engine-types/index.d.ts codebase-memory-mcp cli index_repository --repo-path "$PWD" --name cbm-repro --mode full codebase-memory-mcp cli query_graph --project cbm-repro \ --query "MATCH (a)-[r:CALLS]->(b) RETURN a.qualified_name, r.callee, b.qualified_name, r.strategy, r.confidence, r.line"
Scale, and why confidence filtering fails in both directions
Private TS pnpm monorepo: 321 files, 4,548 nodes / 9,822 edges,
--mode full. 319 of 1,190CALLSedges (27%) areunique_name(271) orsuffix_match(48).Cross-checking each of those against the calling file's actual
importstatements in source — not against the graph's IMPORTS edges, which are themselves incomplete in this repo (filed separately as #1732) — 69 bind to a name the calling file never imports. They cluster exactly on generic identifiers:13 resolve 13 reject 7 off 5 stop 5 dispose 3 onChange 3 factory 3 Manifest 2 onProgress 2 ItemresolveandrejectarePromiseexecutor parameters (sub-case 1).off/stop/disposeare lifecycle methods invoked on receivers the resolver could not type.Manifestis sub-case 2.LoginVM.rejectranks #5 by inboundCALLS(14) in this repo. Thirteen are fabricated, every one from anew Promise((resolve, reject) => …)body in a different package; the fourteenth is the genuine intra-classthis.rejectcall, correctly resolved vialsp_ts_method@ 0.95.KitContext.resolveranks #6 with 13 and behaves identically, but stays inside one package — so it produces no layering violation to tip anyone off. We only found it by auditing onstrategy.@artaommahe showed that
confidence >= 0.9fails to remove false edges. In our repo it also removes true ones:getRootContainerhas 67 inboundCALLSedges across 48 distinct caller files, and I confirmed every one of those files contains a realgetRootContainer()call — yet 64 of the 67 areunique_name@ 0.75/0.38, because the workspace-package imports that would have resolved them never produced IMPORTS edges. Filtering at 0.9 drops 64 of 67 and the top of our fan-in ranking empties out.So in a workspace monorepo
confidenceis not separable into signal and noise in either direction — false edges sit above the threshold and true edges sit below it.strategyis the usable discriminator today;confidenceis not.
Posted by Claude Code on behalf of @bearluo
- added a commit that references this issue
on Aug 20, 2026 - added a commit that references this issue
on Sep 20, 2026
Version
codebase-memory-mcp 0.9.1-rc.1
Platform
macOS (Apple Silicon)
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
standard
What happened, and what did you expect?
When a called name can't be resolved to a definition, the
unique_name/qualified_suffix/suffix_matchfallbacks bind it to any same-named symbol anywhere in the project — including test-runner globals and library operators that have no relationship to the matched symbol.trace_paththen reports these as callers with no confidence or strategy column, so the caller list reads as fact.Minimal case: a Jest/Jasmine
describe('...', () => {})block in a.spec.tsfile becomes aCALLSedge to an application method nameddescribein an unrelated file, at confidence 0.75.At scale on a private Angular monorepo (11,788 files, 63,840 nodes / 120,805 edges), the entire top of the inbound-call ranking is fabricated:
CALLSScheduleOpenShiftViewComponent.taketake(n)pipe operator (486take(occurrences repo-wide;takeimported fromrxjs/operatorsin 326 files)ReportService.describedescribe()global (794 spec files)ReportApi.describePersonalScheduleComponent.datePermissionDirective.permissionpermissionreferencesFiltering on confidence does not rescue it: with
WHERE r.confidence >= 0.9, the top callee is stillPersonalScheduleComponent.datewith 250 inbound edges.Two of those recorded "call sites" are not calls at all. I opened the source and verified both:
@Input() set date(date: string | Date) { ... }setter declaration in file A is recorded as aCALLSedge (strategyimport_map) to aset date(...)setter in unrelated file B. The edge'slineproperty points at the setter's own declaration line, not at any call.lazySelect(getTimesheet(this.timesheet.id))— wherethis.timesheetis a property access — is recorded as aCALLSedge to a method namedtimesheeton an unrelated API class.I could not reduce those last two to dummy code (plain and
@Input()-decorated setters in two files did not reproduce them in isolation), so I report them as observed-and-source-verified rather than minimally reduced. The test-global case reduces cleanly and is in the repro repo.Expected, in rough priority order:
CALLSedge for a setter declaration or a property access.strategyandconfidenceintrace_pathoutput (a column, or a--min-confidencefilter). That data exists in the store but today is only reachable by hand-writingquery_graph, so the default path for an agent is the unfiltered, unlabelled one.Reproduction
Public repro repo: https://github.com/artaommahe/codebase-memory-mcp-rc-repros — see Case B.
src/app/report.spec.tscontains only Jestdescribe/it/expectglobals.src/app/report.service.tsdefines an unrelated method nameddescribe. Observed (row 2 of 5):Line 9 is the
describe('another suite', ...)block in the spec file.trace_path --function-name describe --direction inbound --include-tests truethen lists the spec as a caller with no indication that the binding is a guess.Project scale (if relevant)
Aggregate figures are from a private Angular monorepo: 11,788 files, 63,840 nodes / 120,805 edges,
--mode full, 10,175.tsfiles, 794.spec.tsfiles.CALLSstrategy mix across all 15,837 edges:unique_name5,923,lsp_ts_method3,051,suffix_match2,147,import_map1,923,same_module1,490,qualified_suffix707,lsp_ts_local408, remainder <100 each. Roughly 5,500 edges carry confidence below 0.5. The linked repro is self-contained dummy code.Confirmations
Posted by Claude Code on behalf of @artaommahe