Version
codebase-memory-mcp 0.10.8
Platform
Windows 11 Pro 26H2 (10.0.26200), x64
Install channel
GitHub release archive
Binary variant
standard
What happened, and what did you expect?
#180 (ES/TS module specifiers produce zero IMPORTS edges) was closed by #184, which builds a package map from package.json. That map keys on main / exports / types. In a source-first monorepo those fields point at build output (./dist/index.js) which is gitignored and therefore — correctly — not indexed. The specifier resolves to a path that has no node in the graph, the IMPORTS edge is silently dropped, and every cross-package call falls through to the unique_name fallback.
So #180's mapping step now succeeds syntactically but lands on nothing. The end state for the user is the same one #180 described.
Observed on a private TS pnpm monorepo (4 workspace packages + 2 apps, 321 files, 4,548 nodes / 9,822 edges, --mode full):
-
apps/demo/assets/foundation/net/auth.ts has 14 imported bindings — 13 from @cck/core, 1 from '../catalog'. The graph holds exactly one IMPORTS edge for that file, the relative one:
MATCH (f:File)-[imp:IMPORTS]->(m) WHERE f.file_path = 'apps/demo/assets/foundation/net/auth.ts'
RETURN imp.local_name, m.qualified_name
LOGIN_UI <project>.apps.demo.assets.foundation.catalog
total: 1
-
packages/core/package.json declares "main": "./dist/index.js" and "exports": { ".": { "import": "./dist/index.js" } }. packages/core/dist/index.js exists on disk but .gitignore has dist/, so the indexer skips it: MATCH (f:File) WHERE f.file_path CONTAINS 'dist/' returns 0 rows. Meanwhile packages/core/src/index.ts is in the graph as <project>.packages.core.src.index.
-
The resulting CALLS strategy mix — unique_name is doing the work import_map should be doing:
| strategy |
confidence |
edges |
same_module |
0.90 |
434 |
lsp_ts_local / _import / _method / _namespace |
0.95 |
219 |
import_map |
0.95 |
141 |
qualified_suffix |
0.90 |
76 |
unique_name |
0.75 / 0.38 |
271 |
suffix_match |
0.55 … 0.08 |
48 |
319 of 1,190 CALLS edges (27%) are resolved by name alone; 162 of those cross a package boundary. All 141 import_map edges come from relative imports; not one comes from a @cck/* specifier.
Because unique_name also binds unresolvable names to unrelated same-named symbols (#1355), the two compound: the missing IMPORTS edges push cross-package calls into the guessing path, and the guessing path then invents layering violations. In our repo that surfaced as packages/engine → apps/demo and packages/core → apps/demo edges in the Graph UI that do not exist in the source at all.
Expected: when a package specifier's declared entry point resolves to a path with no node in the graph, fall back to a source entry before giving up — e.g. exports["."].development, publishConfig.exports, src/index.{ts,tsx,js}, or the source path implied by the files array (which in our case already lists src).
Reproduction
Self-contained dummy workspace. lib-dist points its entry at gitignored build output (the bug); lib-src points its entry at source (the control).
mkdir -p repro/packages/lib-dist/src repro/packages/lib-dist/dist \
repro/packages/lib-src/src repro/apps/app/src && cd repro
printf 'packages:\n - "packages/*"\n - "apps/*"\n' > pnpm-workspace.yaml
printf 'dist/\nnode_modules/\n' > .gitignore
printf '{ "name": "repro-root", "private": true, "type": "module" }\n' > package.json
cat > packages/lib-dist/package.json <<'JSON'
{ "name": "@demo/lib-dist", "version": "0.0.0", "type": "module",
"main": "./dist/index.js", "types": "./dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist", "src"] }
JSON
echo 'export function helperFromDistPkg(x: number): number { return x + 1; }' > packages/lib-dist/src/index.ts
echo 'export function helperFromDistPkg(x) { return x + 1; }' > packages/lib-dist/dist/index.js
cat > packages/lib-src/package.json <<'JSON'
{ "name": "@demo/lib-src", "version": "0.0.0", "type": "module",
"main": "./src/index.ts", "types": "./src/index.ts",
"exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts" } },
"files": ["src"] }
JSON
echo 'export function helperFromSrcPkg(x: number): number { return x + 2; }' > packages/lib-src/src/index.ts
cat > apps/app/package.json <<'JSON'
{ "name": "@demo/app", "private": true, "type": "module",
"dependencies": { "@demo/lib-dist": "workspace:*", "@demo/lib-src": "workspace:*" } }
JSON
cat > apps/app/src/main.ts <<'TS'
import { helperFromDistPkg } from '@demo/lib-dist';
import { helperFromSrcPkg } from '@demo/lib-src';
import { localHelper } from './local';
export function run(): number {
return helperFromDistPkg(1) + helperFromSrcPkg(2) + localHelper(3);
}
TS
echo 'export function localHelper(x: number): number { return x * 2; }' > apps/app/src/local.ts
git init -q . && git add -A && git -c user.email=r@r -c user.name=r commit -qm init
codebase-memory-mcp cli index_repository --repo-path "$PWD" --name repro --mode full
Indexing correctly excludes the gitignored build output:
"excluded":{"dirs":[".git","packages/lib-dist/dist"],"count":2}
IMPORTS edges — @demo/lib-dist is missing, the other two are present:
codebase-memory-mcp cli query_graph --project repro \
--query "MATCH (f:File)-[imp:IMPORTS]->(m) RETURN f.file_path, imp.local_name, m.qualified_name"
apps/app/src/main.ts helperFromSrcPkg repro.packages.lib-src.src.index
apps/app/src/main.ts localHelper repro.apps.app.src.local
total: 2
main.ts has three imports; only the two whose targets are indexable produce an edge.
CALLS edges — the cross-package call degrades to a 0.38 guess:
codebase-memory-mcp cli query_graph --project repro \
--query "MATCH (a)-[r:CALLS]->(b) RETURN a.qualified_name, r.callee, b.qualified_name, r.strategy, r.confidence"
repro.apps.app.src.main.run helperFromDistPkg repro.packages.lib-dist.src.helperFromDistPkg unique_name "0.38"
repro.apps.app.src.main.run helperFromSrcPkg repro.packages.lib-src.src.helperFromSrcPkg unique_name "0.75"
repro.apps.app.src.main.run localHelper repro.apps.app.src.local.localHelper lsp_ts_import "0.95"
Here the guess happens to land on the right symbol, because the dummy names are unique. In a real codebase they are not — that is #1355.
Secondary observation, reported as observed rather than diagnosed: in the control case (@demo/lib-src) the IMPORTS edge is created and points at repro.packages.lib-src.src.index, the very module that defines helperFromSrcPkg — yet the call still resolves via unique_name @ 0.75 rather than import_map @ 0.95. So a landed IMPORTS edge did not feed import_map here. That may be a second, separate gap, or an artifact of this repro's size; I have not reduced it further.
Project scale (if relevant)
Private TS pnpm monorepo: 321 files, 4,548 nodes / 9,822 edges, --mode full. 4 workspace packages (@cck/core, @cck/engine, @cck/tools, @cck/ecs-bitecs) consumed by 2 apps. nodeLinker: hoisted, moduleResolution: "Bundler", no tsconfig paths (workspace resolution only). Edge counts: CALLS 1,190, USAGE 2,841, IMPORTS 774 — the 774 are all relative specifiers.
Confirmations
Version
codebase-memory-mcp 0.10.8
Platform
Windows 11 Pro 26H2 (10.0.26200), x64
Install channel
GitHub release archive
Binary variant
standard
What happened, and what did you expect?
#180 (ES/TS module specifiers produce zero IMPORTS edges) was closed by #184, which builds a package map from
package.json. That map keys onmain/exports/types. In a source-first monorepo those fields point at build output (./dist/index.js) which is gitignored and therefore — correctly — not indexed. The specifier resolves to a path that has no node in the graph, the IMPORTS edge is silently dropped, and every cross-package call falls through to theunique_namefallback.So #180's mapping step now succeeds syntactically but lands on nothing. The end state for the user is the same one #180 described.
Observed on a private TS pnpm monorepo (4 workspace packages + 2 apps, 321 files, 4,548 nodes / 9,822 edges,
--mode full):apps/demo/assets/foundation/net/auth.tshas 14 imported bindings — 13 from@cck/core, 1 from'../catalog'. The graph holds exactly one IMPORTS edge for that file, the relative one:packages/core/package.jsondeclares"main": "./dist/index.js"and"exports": { ".": { "import": "./dist/index.js" } }.packages/core/dist/index.jsexists on disk but.gitignorehasdist/, so the indexer skips it:MATCH (f:File) WHERE f.file_path CONTAINS 'dist/'returns 0 rows. Meanwhilepackages/core/src/index.tsis in the graph as<project>.packages.core.src.index.The resulting
CALLSstrategy mix —unique_nameis doing the workimport_mapshould be doing:same_modulelsp_ts_local/_import/_method/_namespaceimport_mapqualified_suffixunique_namesuffix_match319 of 1,190
CALLSedges (27%) are resolved by name alone; 162 of those cross a package boundary. All 141import_mapedges come from relative imports; not one comes from a@cck/*specifier.Because
unique_namealso binds unresolvable names to unrelated same-named symbols (#1355), the two compound: the missing IMPORTS edges push cross-package calls into the guessing path, and the guessing path then invents layering violations. In our repo that surfaced aspackages/engine → apps/demoandpackages/core → apps/demoedges in the Graph UI that do not exist in the source at all.Expected: when a package specifier's declared entry point resolves to a path with no node in the graph, fall back to a source entry before giving up — e.g.
exports["."].development,publishConfig.exports,src/index.{ts,tsx,js}, or the source path implied by thefilesarray (which in our case already listssrc).Reproduction
Self-contained dummy workspace.
lib-distpoints its entry at gitignored build output (the bug);lib-srcpoints its entry at source (the control).mkdir -p repro/packages/lib-dist/src repro/packages/lib-dist/dist \ repro/packages/lib-src/src repro/apps/app/src && cd repro printf 'packages:\n - "packages/*"\n - "apps/*"\n' > pnpm-workspace.yaml printf 'dist/\nnode_modules/\n' > .gitignore printf '{ "name": "repro-root", "private": true, "type": "module" }\n' > package.json cat > packages/lib-dist/package.json <<'JSON' { "name": "@demo/lib-dist", "version": "0.0.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "files": ["dist", "src"] } JSON echo 'export function helperFromDistPkg(x: number): number { return x + 1; }' > packages/lib-dist/src/index.ts echo 'export function helperFromDistPkg(x) { return x + 1; }' > packages/lib-dist/dist/index.js cat > packages/lib-src/package.json <<'JSON' { "name": "@demo/lib-src", "version": "0.0.0", "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts" } }, "files": ["src"] } JSON echo 'export function helperFromSrcPkg(x: number): number { return x + 2; }' > packages/lib-src/src/index.ts cat > apps/app/package.json <<'JSON' { "name": "@demo/app", "private": true, "type": "module", "dependencies": { "@demo/lib-dist": "workspace:*", "@demo/lib-src": "workspace:*" } } JSON cat > apps/app/src/main.ts <<'TS' import { helperFromDistPkg } from '@demo/lib-dist'; import { helperFromSrcPkg } from '@demo/lib-src'; import { localHelper } from './local'; export function run(): number { return helperFromDistPkg(1) + helperFromSrcPkg(2) + localHelper(3); } TS echo 'export function localHelper(x: number): number { return x * 2; }' > apps/app/src/local.ts git init -q . && git add -A && git -c user.email=r@r -c user.name=r commit -qm init codebase-memory-mcp cli index_repository --repo-path "$PWD" --name repro --mode fullIndexing correctly excludes the gitignored build output:
IMPORTS edges —
@demo/lib-distis missing, the other two are present:main.tshas three imports; only the two whose targets are indexable produce an edge.CALLS edges — the cross-package call degrades to a 0.38 guess:
Here the guess happens to land on the right symbol, because the dummy names are unique. In a real codebase they are not — that is #1355.
Secondary observation, reported as observed rather than diagnosed: in the control case (
@demo/lib-src) the IMPORTS edge is created and points atrepro.packages.lib-src.src.index, the very module that defineshelperFromSrcPkg— yet the call still resolves viaunique_name@ 0.75 rather thanimport_map@ 0.95. So a landed IMPORTS edge did not feedimport_maphere. That may be a second, separate gap, or an artifact of this repro's size; I have not reduced it further.Project scale (if relevant)
Private TS pnpm monorepo: 321 files, 4,548 nodes / 9,822 edges,
--mode full. 4 workspace packages (@cck/core,@cck/engine,@cck/tools,@cck/ecs-bitecs) consumed by 2 apps.nodeLinker: hoisted,moduleResolution: "Bundler", notsconfigpaths(workspace resolution only). Edge counts:CALLS1,190,USAGE2,841,IMPORTS774 — the 774 are all relative specifiers.Confirmations
describe, RxJStake) and trace_path exposes no confidence/strategy #1355 — that issue is about what the fallback binds to, this one is about why the fallback is reached for every cross-package call in the first place.