typegraph-mcp 0.9.37 → 0.9.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env npx tsx
2
+
3
+ import * as assert from "node:assert/strict";
4
+ import { execFileSync } from "node:child_process";
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+
9
+ function copyDir(src: string, dest: string): void {
10
+ fs.mkdirSync(dest, { recursive: true });
11
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
12
+ const srcPath = path.join(src, entry.name);
13
+ const destPath = path.join(dest, entry.name);
14
+ if (entry.isDirectory()) {
15
+ copyDir(srcPath, destPath);
16
+ } else {
17
+ fs.copyFileSync(srcPath, destPath);
18
+ }
19
+ }
20
+ }
21
+
22
+ function runTsx(
23
+ toolRoot: string,
24
+ args: string[],
25
+ cwd: string
26
+ ): string {
27
+ return execFileSync(path.join(toolRoot, "node_modules/.bin/tsx"), args, {
28
+ cwd,
29
+ encoding: "utf-8",
30
+ maxBuffer: 10 * 1024 * 1024,
31
+ env: process.env,
32
+ });
33
+ }
34
+
35
+ function assertIncludes(text: string, expected: string): void {
36
+ assert.ok(
37
+ text.includes(expected),
38
+ `Expected output to include:\n${expected}\n\nActual output:\n${text}`
39
+ );
40
+ }
41
+
42
+ async function main(): Promise<void> {
43
+ const repoRoot = import.meta.dirname;
44
+ const fixtureRoot = path.join(repoRoot, ".fixtures/install-oxlint");
45
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "typegraph-install-oxlint-"));
46
+ const projectRoot = path.join(tempRoot, "project");
47
+
48
+ copyDir(fixtureRoot, projectRoot);
49
+ fs.mkdirSync(path.join(projectRoot, "node_modules"), { recursive: true });
50
+ fs.symlinkSync(
51
+ path.join(repoRoot, "node_modules/typescript"),
52
+ path.join(projectRoot, "node_modules/typescript"),
53
+ "dir"
54
+ );
55
+
56
+ try {
57
+ const setupOutput = runTsx(repoRoot, [path.join(repoRoot, "cli.ts"), "setup", "--yes"], projectRoot);
58
+ const pluginRoot = path.join(projectRoot, "plugins/typegraph-mcp");
59
+
60
+ const tsconfig = fs.readFileSync(path.join(projectRoot, "tsconfig.json"), "utf-8");
61
+ const oxlint = fs.readFileSync(path.join(projectRoot, ".oxlintrc.json"), "utf-8");
62
+
63
+ assertIncludes(tsconfig, '"$schema": "http://json.schemastore.org/tsconfig"');
64
+ assertIncludes(tsconfig, '"exclude": ["plugins/**"]');
65
+ assertIncludes(oxlint, '"ignorePatterns": [');
66
+ assertIncludes(oxlint, '"plugins/**"');
67
+ assert.ok(fs.existsSync(path.join(pluginRoot, "cli.ts")), "Expected installed plugin CLI");
68
+
69
+ assertIncludes(setupOutput, 'Added "plugins/**" to tsconfig.json exclude');
70
+ assertIncludes(setupOutput, 'Added "plugins/**" to .oxlintrc.json ignorePatterns');
71
+ assertIncludes(setupOutput, "Oxlint ignores plugins/ (.oxlintrc.json)");
72
+
73
+ const checkOutput = runTsx(pluginRoot, [path.join(pluginRoot, "cli.ts"), "check"], projectRoot);
74
+ assertIncludes(checkOutput, "Oxlint ignores plugins/ (.oxlintrc.json)");
75
+ assert.ok(
76
+ !checkOutput.includes("Lint config check (no ESLint or Oxlint config found)"),
77
+ `Did not expect lint config detection to be skipped:\n${checkOutput}`
78
+ );
79
+
80
+ console.log("");
81
+ console.log("typegraph-mcp Install Oxlint Test");
82
+ console.log("=================================");
83
+ console.log(" ✓ tsconfig schema URL preserved during exclude patch");
84
+ console.log(" ✓ tsconfig exclude patch ignores unrelated plugins text");
85
+ console.log(" ✓ .oxlintrc.json patched with plugins ignore");
86
+ console.log(" ✓ installed plugin health check recognizes Oxlint config");
87
+ } finally {
88
+ fs.rmSync(tempRoot, { recursive: true, force: true });
89
+ }
90
+ }
91
+
92
+ main().catch((err) => {
93
+ console.error(err);
94
+ process.exit(1);
95
+ });
package/module-graph.ts CHANGED
@@ -216,7 +216,7 @@ function distToSource(resolvedPath: string, projectRoot: string): string {
216
216
  return resolvedPath;
217
217
  }
218
218
 
219
- function resolveImport(
219
+ export function resolveProjectImport(
220
220
  resolver: ResolverFactory,
221
221
  fromDir: string,
222
222
  specifier: string,
@@ -287,7 +287,7 @@ function buildForwardEdges(
287
287
  const fromDir = path.dirname(filePath);
288
288
 
289
289
  for (const raw of rawImports) {
290
- const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot);
290
+ const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
291
291
  if (target) {
292
292
  edges.push({
293
293
  target,
@@ -397,7 +397,7 @@ export function updateFile(
397
397
  const fromDir = path.dirname(filePath);
398
398
  const newEdges: ImportEdge[] = [];
399
399
  for (const raw of rawImports) {
400
- const target = resolveImport(resolver, fromDir, raw.specifier, projectRoot);
400
+ const target = resolveProjectImport(resolver, fromDir, raw.specifier, projectRoot);
401
401
  if (target) {
402
402
  newEdges.push({
403
403
  target,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typegraph-mcp",
3
- "version": "0.9.37",
3
+ "version": "0.9.39",
4
4
  "description": "Type-aware codebase navigation for AI coding agents — 14 MCP tools powered by tsserver + oxc",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "scripts": {
35
35
  "build": "tsup",
36
36
  "start": "tsx server.ts",
37
- "test": "tsx smoke-test.ts",
37
+ "test": "tsx smoke-test.ts && tsx export-surface-test.ts && tsx install-oxlint-test.ts",
38
38
  "check": "tsx check.ts"
39
39
  },
40
40
  "dependencies": {
package/server.ts CHANGED
@@ -17,9 +17,16 @@
17
17
 
18
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
19
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { parseSync } from "oxc-parser";
21
+ import type { ResolverFactory } from "oxc-resolver";
20
22
  import { z } from "zod";
21
23
  import { TsServerClient, type NavBarItem } from "./tsserver-client.js";
22
- import { buildGraph, startWatcher, type ModuleGraph } from "./module-graph.js";
24
+ import {
25
+ buildGraph,
26
+ resolveProjectImport,
27
+ startWatcher,
28
+ type ModuleGraph,
29
+ } from "./module-graph.js";
23
30
  import {
24
31
  dependencyTree,
25
32
  dependents,
@@ -44,6 +51,7 @@ const client = new TsServerClient(projectRoot, tsconfigPath);
44
51
 
45
52
  // Module graph — initialized in main(), used by graph tools
46
53
  let moduleGraph: ModuleGraph;
54
+ let moduleResolver: ResolverFactory;
47
55
 
48
56
  const mcpServer = new McpServer({
49
57
  name: "typegraph",
@@ -167,6 +175,347 @@ async function resolveParams(params: {
167
175
  return { error: "Either line+column or symbol must be provided" };
168
176
  }
169
177
 
178
+ type ModuleExportRecord = {
179
+ symbol: string;
180
+ kind: string;
181
+ line: number;
182
+ type: string | null;
183
+ exportKind: "value" | "type";
184
+ isTypeOnly: boolean;
185
+ isNamespace: boolean;
186
+ source: "local" | "re-export" | "star-re-export";
187
+ from: string | null;
188
+ definedIn: string;
189
+ definedLine: number | null;
190
+ };
191
+
192
+ type StaticExportEntry = ReturnType<typeof parseSync>["module"]["staticExports"][number]["entries"][number];
193
+
194
+ const exportKinds = new Set([
195
+ "function",
196
+ "const",
197
+ "class",
198
+ "interface",
199
+ "type",
200
+ "enum",
201
+ "var",
202
+ "let",
203
+ "method",
204
+ ]);
205
+
206
+ function exportPriority(source: ModuleExportRecord["source"]): number {
207
+ switch (source) {
208
+ case "local":
209
+ return 3;
210
+ case "re-export":
211
+ return 2;
212
+ case "star-re-export":
213
+ return 1;
214
+ }
215
+ }
216
+
217
+ function exportKey(item: Pick<ModuleExportRecord, "symbol" | "exportKind">): string {
218
+ return `${item.symbol}:${item.exportKind}`;
219
+ }
220
+
221
+ function sameExportOrigin(a: ModuleExportRecord, b: ModuleExportRecord): boolean {
222
+ return (
223
+ a.symbol === b.symbol &&
224
+ a.exportKind === b.exportKind &&
225
+ a.from === b.from &&
226
+ a.definedIn === b.definedIn &&
227
+ a.definedLine === b.definedLine
228
+ );
229
+ }
230
+
231
+ function kindImpliesTypeOnly(kind: string): boolean {
232
+ return kind === "type" || kind === "interface";
233
+ }
234
+
235
+ function normalizeExportKindLabel(
236
+ kind: string,
237
+ exportKind: ModuleExportRecord["exportKind"]
238
+ ): string {
239
+ if (exportKind === "type" && !kindImpliesTypeOnly(kind)) {
240
+ return "type";
241
+ }
242
+ return kind;
243
+ }
244
+
245
+ function upsertExport(
246
+ map: Map<string, ModuleExportRecord>,
247
+ conflicts: Set<string>,
248
+ nextExport: ModuleExportRecord
249
+ ): void {
250
+ const key = exportKey(nextExport);
251
+ if (conflicts.has(key)) {
252
+ if (nextExport.source === "star-re-export") return;
253
+ conflicts.delete(key);
254
+ map.set(key, nextExport);
255
+ return;
256
+ }
257
+
258
+ const existing = map.get(key);
259
+ if (
260
+ existing &&
261
+ existing.source === "star-re-export" &&
262
+ nextExport.source === "star-re-export" &&
263
+ !sameExportOrigin(existing, nextExport)
264
+ ) {
265
+ map.delete(key);
266
+ conflicts.add(key);
267
+ return;
268
+ }
269
+
270
+ if (!existing || exportPriority(nextExport.source) > exportPriority(existing.source)) {
271
+ map.set(key, nextExport);
272
+ }
273
+ }
274
+
275
+ function offsetToLineColumn(source: string, offset: number | null | undefined): {
276
+ line: number;
277
+ column: number;
278
+ } {
279
+ const safeOffset = Math.max(0, Math.min(offset ?? 0, source.length));
280
+ const prefix = source.slice(0, safeOffset);
281
+ const lines = prefix.split("\n");
282
+ return {
283
+ line: lines.length,
284
+ column: (lines.at(-1)?.length ?? 0) + 1,
285
+ };
286
+ }
287
+
288
+ function normalizeExistingPath(filePath: string): string {
289
+ const resolved = path.resolve(filePath);
290
+ try {
291
+ return fs.realpathSync.native(resolved);
292
+ } catch {
293
+ return resolved;
294
+ }
295
+ }
296
+
297
+ const normalizedProjectRoot = normalizeExistingPath(projectRoot);
298
+
299
+ function projectPath(file: string): string {
300
+ return path.isAbsolute(file) ? relPath(file) : file;
301
+ }
302
+
303
+ function exportSymbol(entry: StaticExportEntry): string | null {
304
+ if (entry.exportName.kind === "Default") return "default";
305
+ return entry.exportName.name ?? entry.localName.name ?? entry.importName.name;
306
+ }
307
+
308
+ function exportLookupOffset(entry: StaticExportEntry): number | null | undefined {
309
+ if ((entry as { moduleRequest?: { value: string } }).moduleRequest) {
310
+ return entry.importName.start ?? entry.exportName.start ?? entry.start;
311
+ }
312
+ if (entry.exportName.kind === "Default") {
313
+ return entry.localName.start ?? entry.exportName.start ?? entry.start;
314
+ }
315
+ return entry.exportName.start ?? entry.localName.start ?? entry.start;
316
+ }
317
+
318
+ async function resolveExportMetadata(
319
+ file: string,
320
+ line: number,
321
+ column: number,
322
+ fallbackKind: string
323
+ ): Promise<{
324
+ kind: string;
325
+ type: string | null;
326
+ definedIn: string;
327
+ definedLine: number | null;
328
+ }> {
329
+ const defs = await client.definition(file, line, column);
330
+ const def = defs[0] ?? null;
331
+
332
+ let info = await client.quickinfo(file, line, column);
333
+ if ((!info || info.kind === "alias") && def) {
334
+ info = (await client.quickinfo(def.file, def.start.line, def.start.offset)) ?? info;
335
+ }
336
+
337
+ return {
338
+ kind: info?.kind ?? fallbackKind,
339
+ type: info?.displayString ?? null,
340
+ definedIn: projectPath(def?.file ?? file),
341
+ definedLine: def?.start.line ?? null,
342
+ };
343
+ }
344
+
345
+ async function getModuleExports(
346
+ file: string,
347
+ visited = new Set<string>()
348
+ ): Promise<ModuleExportRecord[]> {
349
+ const relFile = path.isAbsolute(file) ? relPath(file) : file;
350
+ const absFile = normalizeExistingPath(client.resolvePath(relFile));
351
+ if (visited.has(absFile)) return [];
352
+
353
+ const nextVisited = new Set(visited);
354
+ nextVisited.add(absFile);
355
+
356
+ const exportMap = new Map<string, ModuleExportRecord>();
357
+ const conflictingStarExports = new Set<string>();
358
+
359
+ let source: string;
360
+ try {
361
+ source = fs.readFileSync(absFile, "utf-8");
362
+ } catch {
363
+ return [...exportMap.values()];
364
+ }
365
+
366
+ let parsed: ReturnType<typeof parseSync>;
367
+ try {
368
+ parsed = parseSync(absFile, source);
369
+ } catch {
370
+ return [...exportMap.values()];
371
+ }
372
+
373
+ for (const exp of parsed.module.staticExports) {
374
+ for (const entry of exp.entries) {
375
+ const moduleRequest = (entry as { moduleRequest?: { value: string } }).moduleRequest;
376
+ if (!moduleRequest) continue;
377
+
378
+ const targetFile = resolveProjectImport(
379
+ moduleResolver,
380
+ path.dirname(absFile),
381
+ moduleRequest.value,
382
+ projectRoot
383
+ );
384
+
385
+ const exportLoc = offsetToLineColumn(
386
+ source,
387
+ entry.exportName.start ?? entry.localName.start ?? entry.importName.start ?? entry.start
388
+ );
389
+ const importKind = entry.importName.kind as string;
390
+ const exportKind = entry.exportName.kind as string;
391
+
392
+ if (importKind === "AllButDefault" && exportKind === "None") {
393
+ if (!targetFile) continue;
394
+ const nestedExports = await getModuleExports(targetFile, nextVisited);
395
+ for (const nested of nestedExports) {
396
+ if (nested.symbol === "default") continue;
397
+ const starExportKind: ModuleExportRecord["exportKind"] = entry.isType
398
+ ? "type"
399
+ : nested.exportKind;
400
+ upsertExport(exportMap, conflictingStarExports, {
401
+ ...nested,
402
+ line: exportLoc.line,
403
+ exportKind: starExportKind,
404
+ isTypeOnly: starExportKind === "type",
405
+ source: "star-re-export",
406
+ from: relPath(targetFile),
407
+ });
408
+ }
409
+ continue;
410
+ }
411
+
412
+ const symbol = exportSymbol(entry);
413
+ if (!symbol) continue;
414
+
415
+ const importedSymbol =
416
+ importKind === "Default"
417
+ ? "default"
418
+ : importKind === "Name"
419
+ ? entry.importName.name
420
+ : null;
421
+ const nestedMatch =
422
+ targetFile && importedSymbol
423
+ ? (await getModuleExports(targetFile, nextVisited)).find(
424
+ (item) => item.symbol === importedSymbol
425
+ ) ?? null
426
+ : null;
427
+
428
+ const lookupLoc = offsetToLineColumn(
429
+ source,
430
+ exportLookupOffset(entry)
431
+ );
432
+ const metadata = await resolveExportMetadata(
433
+ relFile,
434
+ lookupLoc.line,
435
+ lookupLoc.column,
436
+ importKind === "All" ? "namespace" : "alias"
437
+ );
438
+ const resolvedExportKind: ModuleExportRecord["exportKind"] =
439
+ entry.isType ||
440
+ nestedMatch?.exportKind === "type" ||
441
+ kindImpliesTypeOnly(nestedMatch?.kind ?? metadata.kind)
442
+ ? "type"
443
+ : "value";
444
+ const resolvedKind = normalizeExportKindLabel(
445
+ nestedMatch?.kind ?? metadata.kind,
446
+ resolvedExportKind
447
+ );
448
+
449
+ upsertExport(exportMap, conflictingStarExports, {
450
+ symbol,
451
+ kind: resolvedKind,
452
+ line: exportLoc.line,
453
+ type: nestedMatch?.type ?? metadata.type,
454
+ exportKind: resolvedExportKind,
455
+ isTypeOnly: resolvedExportKind === "type",
456
+ isNamespace: importKind === "All",
457
+ source: "re-export",
458
+ from: targetFile ? relPath(targetFile) : moduleRequest.value,
459
+ definedIn: nestedMatch?.definedIn ?? metadata.definedIn,
460
+ definedLine: nestedMatch?.definedLine ?? metadata.definedLine,
461
+ });
462
+ continue;
463
+ }
464
+
465
+ for (const entry of exp.entries) {
466
+ const moduleRequest = (entry as { moduleRequest?: { value: string } }).moduleRequest;
467
+ if (moduleRequest) continue;
468
+
469
+ const symbol = exportSymbol(entry);
470
+ if (!symbol) continue;
471
+
472
+ const exportLoc = offsetToLineColumn(
473
+ source,
474
+ entry.exportName.start ?? entry.localName.start ?? entry.start
475
+ );
476
+ const lookupLoc = offsetToLineColumn(source, exportLookupOffset(entry));
477
+ const metadata = await resolveExportMetadata(
478
+ relFile,
479
+ lookupLoc.line,
480
+ lookupLoc.column,
481
+ entry.isType ? "type" : "value"
482
+ );
483
+ const resolvedExportKind: ModuleExportRecord["exportKind"] =
484
+ entry.isType || kindImpliesTypeOnly(metadata.kind) ? "type" : "value";
485
+ const resolvedKind = normalizeExportKindLabel(metadata.kind, resolvedExportKind);
486
+
487
+ // Skip navbar/import alias noise — only keep actual exported declaration kinds.
488
+ if (
489
+ resolvedExportKind === "value" &&
490
+ symbol !== "default" &&
491
+ !exportKinds.has(resolvedKind) &&
492
+ resolvedKind !== "namespace" &&
493
+ resolvedKind !== "class"
494
+ ) {
495
+ continue;
496
+ }
497
+
498
+ upsertExport(exportMap, conflictingStarExports, {
499
+ symbol,
500
+ kind: resolvedKind,
501
+ line: exportLoc.line,
502
+ type: metadata.type,
503
+ exportKind: resolvedExportKind,
504
+ isTypeOnly: resolvedExportKind === "type",
505
+ isNamespace: false,
506
+ source: "local",
507
+ from: null,
508
+ definedIn: relFile,
509
+ definedLine: resolvedExportKind === "type" ? exportLoc.line : metadata.definedLine,
510
+ });
511
+ }
512
+ }
513
+
514
+ return [...exportMap.values()].sort(
515
+ (a, b) => a.line - b.line || a.symbol.localeCompare(b.symbol)
516
+ );
517
+ }
518
+
170
519
  // ─── Tool 1: ts_find_symbol ─────────────────────────────────────────────────
171
520
 
172
521
  mcpServer.tool(
@@ -499,68 +848,38 @@ mcpServer.tool(
499
848
 
500
849
  mcpServer.tool(
501
850
  "ts_module_exports",
502
- "List all exported symbols from a module with their resolved types. Gives an at-a-glance understanding of what a file provides.",
851
+ "List all exported symbols from a module with their resolved types, including re-exports when possible. Gives an at-a-glance understanding of what a file provides.",
503
852
  {
504
853
  file: z.string().describe("File to inspect"),
505
854
  },
506
855
  async ({ file }) => {
507
- const bar = await client.navbar(file);
508
- if (bar.length === 0) {
509
- return {
510
- content: [
511
- {
512
- type: "text" as const,
513
- text: JSON.stringify({ error: `No symbols found in ${file}` }),
514
- },
515
- ],
516
- };
517
- }
518
-
519
- // The top-level navbar item is the module itself — its children are exports
520
- const moduleItem = bar.find((item) => item.kind === "module");
521
- const topItems = moduleItem?.childItems ?? bar;
522
-
523
- // Filter to meaningful declarations (skip imports, local vars, etc.)
524
- const exportKinds = new Set([
525
- "function",
526
- "const",
527
- "class",
528
- "interface",
529
- "type",
530
- "enum",
531
- "var",
532
- "let",
533
- "method",
534
- ]);
535
- const candidates = topItems.filter((item) => exportKinds.has(item.kind));
536
-
537
- const exports: Array<{
538
- symbol: string;
539
- kind: string;
540
- line: number;
541
- type: string | null;
542
- }> = [];
543
-
544
- for (const item of candidates) {
545
- if (!item.spans[0]) continue;
546
- const span = item.spans[0];
547
-
548
- // Get type info for this symbol
549
- const info = await client.quickinfo(file, span.start.line, span.start.offset);
550
-
551
- exports.push({
552
- symbol: item.text,
553
- kind: item.kind,
554
- line: span.start.line,
555
- type: info?.displayString ?? null,
556
- });
557
- }
856
+ const exports = await getModuleExports(file);
857
+ const localCount = exports.filter((item) => item.source === "local").length;
858
+ const reExportCount = exports.length - localCount;
859
+ const typeOnlyCount = exports.filter((item) => item.isTypeOnly).length;
860
+ const valueCount = exports.length - typeOnlyCount;
861
+ const namespaceExportCount = exports.filter((item) => item.isNamespace).length;
862
+ const hasLocalRuntimeExports = exports.some(
863
+ (item) => item.source === "local" && !item.isTypeOnly
864
+ );
865
+ const isPrimarilyBarrel = exports.length > 0 && localCount < reExportCount;
558
866
 
559
867
  return {
560
868
  content: [
561
869
  {
562
870
  type: "text" as const,
563
- text: JSON.stringify({ file, exports, count: exports.length }),
871
+ text: JSON.stringify({
872
+ file,
873
+ exports,
874
+ count: exports.length,
875
+ localCount,
876
+ reExportCount,
877
+ typeOnlyCount,
878
+ valueCount,
879
+ namespaceExportCount,
880
+ hasLocalRuntimeExports,
881
+ isPrimarilyBarrel,
882
+ }),
564
883
  },
565
884
  ],
566
885
  };
@@ -571,7 +890,7 @@ mcpServer.tool(
571
890
 
572
891
  /** Convert an absolute path to project-relative */
573
892
  function relPath(absPath: string): string {
574
- return path.relative(projectRoot, absPath);
893
+ return path.relative(normalizedProjectRoot, normalizeExistingPath(absPath));
575
894
  }
576
895
 
577
896
  /** Convert a relative or absolute path to absolute */
@@ -812,6 +1131,7 @@ async function main() {
812
1131
  ]);
813
1132
 
814
1133
  moduleGraph = graphResult.graph;
1134
+ moduleResolver = graphResult.resolver;
815
1135
  startWatcher(projectRoot, moduleGraph, graphResult.resolver);
816
1136
 
817
1137
  const transport = new StdioServerTransport();
@@ -24,6 +24,11 @@ If you know the file:
24
24
  - Call `ts_module_exports` to see what the file provides
25
25
  - Call `ts_find_symbol` to locate a specific symbol within it
26
26
 
27
+ If `ts_module_exports` on a top-level `index.ts` is empty or mostly re-exports:
28
+ - Treat it as a barrel, not a dead end
29
+ - Pivot to a composition module such as an app entrypoint, router, handler, API module, or service composition root
30
+ - Use `ts_dependency_tree` on that composition module to get quick architectural context
31
+
27
32
  ### Step 2: Understand the Type
28
33
  Call `ts_type_info` on the entry point symbol. This gives you the full type signature and documentation without reading the entire file.
29
34
 
@@ -33,6 +38,8 @@ Call `ts_trace_chain` to follow the definition chain from the entry point to the
33
38
  ### Step 4: Explore the Neighborhood
34
39
  Call `ts_subgraph` with the key files discovered in step 3 to see the surrounding module structure. Use `direction: "both"` and `depth: 1` for immediate context.
35
40
 
41
+ For a fast system-level read, `ts_dependency_tree` on the composition module often gives a better first picture than reading a barrel file's exports.
42
+
36
43
  ### Step 5: Deep Dive Where Needed
37
44
  Only now, read specific files at the lines identified by the tools. You have precise coordinates — no need to read entire files.
38
45
 
@@ -54,6 +54,8 @@ Glob: **/index.ts, **/main.ts, **/entry*.ts, **/worker*.ts, **/server.ts, **/app
54
54
 
55
55
  Exclude `node_modules/` hits. The results are your starting nodes.
56
56
 
57
+ Prefer these composition modules over top-level barrel files for your first passes. Barrels often describe the public API surface, but entry points and composition roots reveal how the system is actually wired.
58
+
57
59
  ### 1b. Dependency tree from every entry point (depth 2)
58
60
 
59
61
  For each entry point, run:
@@ -121,6 +123,8 @@ Record for each:
121
123
  - **Export types** — classes, interfaces, types, constants, functions. A file that exports 8 interfaces is a contract definition. A file that exports 8 constants is a configuration. A file that exports a mix of class + error + test layer is a service module.
122
124
  - **Naming patterns** — do exports follow consistent naming (`*Service`, `*Error`, `*Test`, `*Live`)? Consistency across files is evidence of intentional patterns.
123
125
 
126
+ If a file is a barrel and `ts_module_exports` is sparse or uninformative, do not stop there. Pivot to the concrete composition modules it fronts and use `ts_dependency_tree` or `ts_module_exports` there instead.
127
+
124
128
  ### 2c. Module boundaries — how coupled are directories?
125
129
 
126
130
  Identify 3-5 directories that look like they should be self-contained modules (e.g., `services/billing/`, `providers/email/`, `middleware/`).
@@ -29,6 +29,8 @@ Use **ts_type_info** — returns the same info as hovering in VS Code. Includes
29
29
  ### "What are all the exports of this file?"
30
30
  Use **ts_module_exports** — lists all exported symbols with their resolved types.
31
31
 
32
+ If the file is a top-level barrel (`index.ts`, re-export hub), the result may be sparse or unhelpful for architecture discovery. For quick project insight, prefer composition modules such as entrypoints, routers, handler modules, service composition roots, or API modules that wire concrete behavior together.
33
+
32
34
  ### "Where is X used?"
33
35
  Use **ts_references** for all semantic references. Unlike grep, this returns only real code references, not string matches in comments or unrelated variables.
34
36
 
@@ -63,6 +65,7 @@ Use **ts_module_boundary** — analyzes incoming/outgoing edges, shared dependen
63
65
  3. **Combine tools for workflows.** Impact analysis = ts_blast_radius + ts_dependents. Refactor safety = ts_trace_chain + ts_import_cycles.
64
66
  4. **Graph queries are instant** (~0.1ms). Point queries are fast (~2-50ms). Don't hesitate to use them liberally.
65
67
  5. **First query may be slow** (~2s) as tsserver warms up. All subsequent queries are fast.
68
+ 6. **For fast architecture reads, start at composition modules, not barrels.** Barrels are useful for API shape, but entrypoints and composition roots tell you how the system is actually wired.
66
69
 
67
70
  ## Tool Reference
68
71