@maxgfr/codeindex 2.17.1 → 2.18.0

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.
package/src/engine-cli.ts CHANGED
@@ -4,12 +4,14 @@ import { SCHEMA_VERSION, EXTRACTOR_VERSION, type FileRecord } from "./types.js";
4
4
  import { ENGINE_VERSION } from "./types.js";
5
5
  import { ensureGrammars, grammarKeysForExts, resolveGrammarsTier, sharedGrammarsCacheDir } from "./ast/loader.js";
6
6
  import { resolveGrammarsPullTarget, pullGrammars } from "./ast/grammars-pull.js";
7
- import { buildIndexArtifacts, buildArtifactsFromScan, type BuildIndexOptions } from "./pipeline.js";
7
+ import { buildIndexArtifacts, buildArtifactsFromScan, type BuildIndexOptions, type IndexArtifacts } from "./pipeline.js";
8
8
  import { sha1 } from "./hash.js";
9
9
  import { renderGraphJson } from "./render/graph-json.js";
10
10
  import { renderSymbolsJson } from "./render/symbols-json.js";
11
11
  import { renderScip } from "./render/scip.js";
12
- import { scanRepo } from "./scan.js";
12
+ import { scanRepo, scanSummary, type RepoScan } from "./scan.js";
13
+ import { scanRepoParallel } from "./pool.js";
14
+ import { preloadSession, INDEX_DIR } from "./preload.js";
13
15
  import { walk, type WalkResult } from "./walk.js";
14
16
  import { buildCallerIndex } from "./callers.js";
15
17
  import { detectWorkspaces } from "./workspaces.js";
@@ -20,6 +22,8 @@ import { renderRepoMap } from "./repomap.js";
20
22
  import { findDeadCode } from "./deadcode.js";
21
23
  import { symbolComplexity, riskHotspots } from "./complexity.js";
22
24
  import { renderMermaid } from "./viz.js";
25
+ import { impactOf, neighborsOf } from "./traverse.js";
26
+ import { deltaFor, formatDeltaPanel } from "./delta.js";
23
27
  import { searchIndex } from "./bm25.js";
24
28
  import { checkRules, parseRules } from "./rules.js";
25
29
  import { EMBED_VERSION, resolveEmbedModelDir, loadEmbedModel, parseEmbedModel, resolveEmbedPullUrl, fetchEmbedModel } from "./embed/model.js";
@@ -83,6 +87,13 @@ Commands:
83
87
  complexity Cyclomatic-complexity estimates, most-complex first. Pass a file
84
88
  positional for one file; omit for the repo-wide top
85
89
  risk Complexity × git-churn ranking (JSON; --since <ref> to bound)
90
+ delta Review panel for the git diff: changed files -> enclosing symbols ->
91
+ blast radius -> risk score with explained reasons
92
+ (--base <ref> | --staged, --depth <n>, --json)
93
+ impact Reverse dependency closure of a file or module: everything that
94
+ transitively imports/uses/calls it (--depth <n>; JSON)
95
+ neighbors Graph neighbours of a file or module, both directions
96
+ (--depth <n>, --kind import,call,use,doc-link,mention; JSON)
86
97
  mermaid Mermaid diagram of the module graph; pass a module positional to
87
98
  focus on one neighborhood
88
99
  rewrite Map an expensive tree-wide search onto its indexed equivalent:
@@ -97,7 +108,10 @@ Commands:
97
108
  check_rules, the memory quartet and the three symbolic-edit
98
109
  writes). Flags: --repo <dir> pins ONE repository so the per-tool
99
110
  repo argument becomes optional (an explicit per-call repo still
100
- wins); --server-name <name> overrides the announced serverInfo
111
+ wins); --server-name <name> overrides the announced serverInfo;
112
+ --max-response-bytes <n> caps a single tool response (default 1e6;
113
+ a response under the cap is byte-identical, one over it is
114
+ replaced by an actionable notice instead of an unusable blob)
101
115
  version Print the engine version
102
116
 
103
117
  Flags (accepted before OR after the subcommand: '--repo X scan' and
@@ -117,6 +131,16 @@ Flags (accepted before OR after the subcommand: '--repo X scan' and
117
131
  --max-bytes <n> Skip files above this size (default 1 MiB)
118
132
  --max-calls <n> Per-file call-site cap for extraction (default 512)
119
133
  --no-ast Skip tree-sitter grammars even when present (regex tier)
134
+ --workers <n> \`index\`: extraction worker threads (default: cores-1,
135
+ capped at 8; 0 or 1 forces the single-threaded path).
136
+ Also settable with CODEINDEX_WORKERS. Artifacts are
137
+ byte-identical either way
138
+ --index <dir> Persisted index the READ commands reuse, relative to the
139
+ repo (default .codeindex — i.e. what \`index --out\` wrote
140
+ there). A fresh index turns the scan into a stat pass and,
141
+ when it still matches the worktree, skips the pipeline
142
+ entirely. Stale/absent/corrupt → a normal cold build
143
+ --no-index-cache Never reuse a persisted index; always build from scratch
120
144
  --config <file> Rules config for \`rules\` (JSON: [{name, from, to, …}])
121
145
  --limit <n> Max results for \`search\` (default 20)
122
146
  --no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
@@ -144,6 +168,9 @@ interface CliFlags {
144
168
  maxBytes?: number;
145
169
  maxCalls?: number;
146
170
  noAst: boolean;
171
+ workers?: number; // extraction worker threads (0/1 = sequential)
172
+ indexDir?: string; // persisted index to read (default .codeindex)
173
+ noIndexCache?: boolean; // never reuse a persisted index
147
174
  since?: string;
148
175
  ignoreCase?: boolean;
149
176
  maxHits?: number;
@@ -155,6 +182,11 @@ interface CliFlags {
155
182
  recall?: boolean; // callers: recall-oriented binding
156
183
  run?: boolean; // `embed serve`: actually run the docker command (default: print)
157
184
  projectRoot?: string; // scip: override Metadata.project_root
185
+ base?: string; // delta: branch/ref to diff against (default: the repo's default branch)
186
+ staged?: boolean; // delta: diff the index instead of the merge-base
187
+ depth?: number; // delta/impact/neighbors: traversal hops
188
+ kind?: string; // neighbors: comma-separated edge kinds to traverse
189
+ json?: boolean; // delta: emit JSON instead of the human panel
158
190
  positional?: string; // e.g. the grep pattern or search query
159
191
  }
160
192
 
@@ -190,6 +222,15 @@ function parseFlags(args: string[]): CliFlags {
190
222
  else if (a === "--max-hits") flags.maxHits = num();
191
223
  else if (a === "--budget-tokens") flags.budgetTokens = num();
192
224
  else if (a === "--no-ast") flags.noAst = true;
225
+ else if (a === "--index") flags.indexDir = next();
226
+ else if (a === "--no-index-cache") flags.noIndexCache = true;
227
+ else if (a === "--workers") {
228
+ // 0 is meaningful here (force sequential), so this cannot use num().
229
+ const raw = next();
230
+ const n = Number(raw);
231
+ if (!Number.isInteger(n) || n < 0) throw new Error(`--workers expects a non-negative integer, got "${raw}"`);
232
+ flags.workers = n;
233
+ }
193
234
  else if (a === "--since") flags.since = next();
194
235
  else if (a === "--config") flags.config = resolve(next());
195
236
  else if (a === "--limit") flags.limit = num();
@@ -197,6 +238,11 @@ function parseFlags(args: string[]): CliFlags {
197
238
  else if (a === "--semantic") flags.semantic = true;
198
239
  else if (a === "--recall") flags.recall = true;
199
240
  else if (a === "--run") flags.run = true;
241
+ else if (a === "--base") flags.base = next();
242
+ else if (a === "--staged") flags.staged = true;
243
+ else if (a === "--depth") flags.depth = num();
244
+ else if (a === "--kind") flags.kind = next();
245
+ else if (a === "--json") flags.json = true;
200
246
  else if (!a.startsWith("--") && flags.positional === undefined) flags.positional = a;
201
247
  else throw new Error(`unknown flag: ${a}`);
202
248
  }
@@ -238,9 +284,14 @@ const SCANLESS_COMMANDS = new Set(["grep", "churn", "coupling", "workspaces", "g
238
284
  // dispatch site). `--repo` is resolved to an absolute path and must exist: a
239
285
  // server pinned to a typo'd directory would otherwise answer every tool call
240
286
  // with the same confusing per-call error instead of failing at startup.
241
- export function parseMcpFlags(argv: string[]): { defaultRepo?: string; serverInfo?: { name?: string } } {
287
+ export function parseMcpFlags(argv: string[]): {
288
+ defaultRepo?: string;
289
+ serverInfo?: { name?: string };
290
+ maxResponseBytes?: number;
291
+ } {
242
292
  let defaultRepo: string | undefined;
243
293
  let name: string | undefined;
294
+ let maxResponseBytes: number | undefined;
244
295
  for (let i = 0; i < argv.length; i++) {
245
296
  const a = argv[i];
246
297
  if (a === "--repo") {
@@ -251,12 +302,17 @@ export function parseMcpFlags(argv: string[]): { defaultRepo?: string; serverInf
251
302
  const v = argv[++i];
252
303
  if (!v) throw new Error("--server-name requires a value");
253
304
  name = v;
305
+ } else if (a === "--max-response-bytes") {
306
+ const v = argv[++i];
307
+ const n = Number(v);
308
+ if (!v || !Number.isFinite(n) || n <= 0) throw new Error("--max-response-bytes requires a positive number");
309
+ maxResponseBytes = n;
254
310
  } else {
255
311
  throw new Error(`unknown flag for \`mcp\`: ${a}`);
256
312
  }
257
313
  }
258
314
  if (defaultRepo && !existsSync(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
259
- return { defaultRepo, serverInfo: name ? { name } : undefined };
315
+ return { defaultRepo, serverInfo: name ? { name } : undefined, maxResponseBytes };
260
316
  }
261
317
 
262
318
  // Flags that consume the following argv element. Needed to hoist leading flags
@@ -279,6 +335,9 @@ const VALUE_FLAGS = new Set([
279
335
  "--config",
280
336
  "--limit",
281
337
  "--server-name",
338
+ "--workers",
339
+ "--index",
340
+ "--max-response-bytes",
282
341
  ]);
283
342
 
284
343
  // Accept global flags BEFORE the subcommand as well as after, so
@@ -369,6 +428,38 @@ export async function runCli(rawArgv: string[]): Promise<void> {
369
428
  await ensureGrammars(grammarKeysForExts(precomputedWalk.files.map((f) => f.ext)));
370
429
  }
371
430
 
431
+ // Read commands reuse a persisted index instead of rebuilding from scratch.
432
+ //
433
+ // Only `index` ever consulted .codeindex/; every read command (graph, symbols,
434
+ // scip, callers, search, repomap, hotspots, deadcode, complexity, risk,
435
+ // mermaid, rules) re-walked, re-read, re-hashed and re-EXTRACTED the whole
436
+ // repo on each invocation — `codeindex search` cost a full tree-sitter pass
437
+ // every time, with a fresh index sitting right next to it.
438
+ //
439
+ // cache.json turns the scan into a stat pass; when the freshness guard holds,
440
+ // graph.json/symbols.json come back without running the pipeline at all. Both
441
+ // degrade to today's cold path when the index is absent, stale or corrupt, so
442
+ // output is unchanged either way. Resolved lazily and at most once: a command
443
+ // uses either the scan or the artifacts, never both.
444
+ const indexDir = flags.indexDir ?? INDEX_DIR;
445
+ let preloadTried = false;
446
+ let preloaded: { scan: RepoScan; arts?: IndexArtifacts } | undefined;
447
+ const tryPreload = (): { scan: RepoScan; arts?: IndexArtifacts } | undefined => {
448
+ if (preloadTried) return preloaded;
449
+ preloadTried = true;
450
+ if (flags.noIndexCache) return undefined;
451
+ const p = preloadSession(flags.repo, scanOptions(flags, precomputedWalk), indexDir);
452
+ if (p) preloaded = { scan: p.scan, arts: p.arts };
453
+ return preloaded;
454
+ };
455
+ const readScan = (): RepoScan => tryPreload()?.scan ?? scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
456
+ const readArtifacts = (): IndexArtifacts => {
457
+ const p = tryPreload();
458
+ if (p?.arts) return p.arts;
459
+ if (p) return buildArtifactsFromScan(p.scan, scanOptions(flags, precomputedWalk));
460
+ return buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
461
+ };
462
+
372
463
  if (cmd === "index") {
373
464
  if (!flags.out) throw new Error("index needs --out <dir>");
374
465
  const outDir = flags.out;
@@ -410,7 +501,12 @@ export async function runCli(rawArgv: string[]): Promise<void> {
410
501
  } catch {
411
502
  // no cache yet (or unreadable) — cold build
412
503
  }
413
- const scan = scanRepo(flags.repo, { ...scanOptions(flags, precomputedWalk), cache, out: outDir });
504
+ const scan = await scanRepoParallel(flags.repo, {
505
+ ...scanOptions(flags, precomputedWalk),
506
+ cache,
507
+ out: outDir,
508
+ workers: flags.workers,
509
+ });
414
510
  const modelDir = resolveEmbedModelDir(flags.repo);
415
511
  const model = modelDir ? loadEmbedModel(modelDir) : undefined;
416
512
 
@@ -512,23 +608,26 @@ export async function runCli(rawArgv: string[]): Promise<void> {
512
608
  process.stderr.write(`codeindex: ${scan.files.length} files → ${outDir}/graph.json + symbols.json${embedNote}${scan.capped ? " (capped)" : ""}\n`);
513
609
  }
514
610
  } else if (cmd === "scan") {
515
- const { scan } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
611
+ // Summary-only: a file count and a language histogram need the walk and the
612
+ // path-based classifiers, never a read or a parse. Same numbers as before by
613
+ // construction — scanSummary and scanRepo share the keptFiles loop.
614
+ const s = scanSummary(flags.repo, scanOptions(flags, precomputedWalk));
516
615
  const summary = {
517
616
  engineVersion: ENGINE_VERSION,
518
- commit: scan.commit,
519
- fileCount: scan.files.length,
520
- languages: scan.languages,
521
- capped: scan.capped,
617
+ commit: s.commit,
618
+ fileCount: s.fileCount,
619
+ languages: s.languages,
620
+ capped: s.capped,
522
621
  };
523
622
  emit(JSON.stringify(summary, null, 2) + "\n", flags.out);
524
623
  } else if (cmd === "graph") {
525
- const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
624
+ const { graph } = readArtifacts();
526
625
  emit(renderGraphJson(graph), flags.out);
527
626
  } else if (cmd === "symbols") {
528
- const { symbols } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
627
+ const { symbols } = readArtifacts();
529
628
  emit(renderSymbolsJson(symbols), flags.out);
530
629
  } else if (cmd === "scip") {
531
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
630
+ const scan = readScan();
532
631
  const bytes = renderScip(scan, { projectRoot: flags.projectRoot });
533
632
  const out = flags.out ?? resolve("index.scip");
534
633
  if (out === "-") process.stdout.write(Buffer.from(bytes));
@@ -537,14 +636,14 @@ export async function runCli(rawArgv: string[]): Promise<void> {
537
636
  process.stderr.write(`codeindex: SCIP index → ${out} (${bytes.length} bytes)\n`);
538
637
  }
539
638
  } else if (cmd === "callers") {
540
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
639
+ const scan = readScan();
541
640
  const index = buildCallerIndex(scan, undefined, { recall: flags.recall });
542
641
  const obj: Record<string, unknown> = {};
543
642
  for (const [name, entry] of index) obj[name] = entry;
544
643
  emit(JSON.stringify(obj, null, 2) + "\n", flags.out);
545
644
  } else if (cmd === "search") {
546
645
  if (!flags.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
547
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
646
+ const scan = readScan();
548
647
  if (flags.semantic) {
549
648
  const endpoint = resolveEmbedEndpoint();
550
649
  const lexical = (): void => {
@@ -649,7 +748,7 @@ export async function runCli(rawArgv: string[]): Promise<void> {
649
748
  }
650
749
  const model = loadEmbedModel(modelDir)!;
651
750
  mkdirSync(flags.out, { recursive: true });
652
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
751
+ const scan = readScan();
653
752
  const index = buildEmbeddingIndex(scan, model);
654
753
  writeFileSync(join(flags.out, "embeddings.bin"), serializeEmbeddings(index));
655
754
  process.stderr.write(`codeindex: ${index.records.length} embedding records → ${flags.out}/embeddings.bin (model ${model.modelId})\n`);
@@ -721,7 +820,7 @@ export async function runCli(rawArgv: string[]): Promise<void> {
721
820
  } else if (cmd === "rules") {
722
821
  if (!flags.config) throw new Error("rules needs --config <codeindex.rules.json>");
723
822
  const rules = parseRules(JSON.parse(readFileSync(flags.config, "utf8")));
724
- const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
823
+ const { graph } = readArtifacts();
725
824
  const violations = checkRules(graph, rules);
726
825
  const errors = violations.filter((v) => v.severity === "error").length;
727
826
  emit(JSON.stringify({ errors, warnings: violations.length - errors, violations }, null, 2) + "\n", flags.out);
@@ -742,26 +841,48 @@ export async function runCli(rawArgv: string[]): Promise<void> {
742
841
  for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k)!;
743
842
  emit(JSON.stringify({ ok, churn: sorted }, null, 2) + "\n", flags.out);
744
843
  } else if (cmd === "repomap") {
745
- const { scan, graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
844
+ const { scan, graph } = readArtifacts();
746
845
  emit(renderRepoMap(scan, graph, { budgetTokens: flags.budgetTokens }), flags.out);
747
846
  } else if (cmd === "hotspots") {
748
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
847
+ const scan = readScan();
749
848
  const { churn, ok } = gitChurn(flags.repo, { since: flags.since });
750
849
  emit(JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan, churn) }, null, 2) + "\n", flags.out);
751
850
  } else if (cmd === "coupling") {
752
851
  const { ok, couplings } = changeCoupling(flags.repo, { since: flags.since });
753
852
  emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags.out);
754
853
  } else if (cmd === "deadcode") {
755
- emit(JSON.stringify(findDeadCode(scanRepo(flags.repo, scanOptions(flags, precomputedWalk))), null, 2) + "\n", flags.out);
854
+ emit(JSON.stringify(findDeadCode(readScan()), null, 2) + "\n", flags.out);
756
855
  } else if (cmd === "complexity") {
757
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
856
+ const scan = readScan();
758
857
  emit(JSON.stringify(symbolComplexity(scan, flags.positional), null, 2) + "\n", flags.out);
759
858
  } else if (cmd === "risk") {
760
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
859
+ const scan = readScan();
761
860
  const { churn, ok } = gitChurn(flags.repo, { since: flags.since });
762
861
  emit(JSON.stringify({ churnOk: ok, risks: riskHotspots(scan, churn) }, null, 2) + "\n", flags.out);
862
+ } else if (cmd === "delta") {
863
+ const { graph, symbols } = readArtifacts();
864
+ const res = deltaFor(flags.repo, graph, symbols, {
865
+ base: flags.base,
866
+ staged: flags.staged,
867
+ depth: flags.depth,
868
+ });
869
+ if ("error" in res) throw new Error(res.error);
870
+ emit(flags.json ? JSON.stringify(res, null, 2) + "\n" : formatDeltaPanel(res), flags.out);
871
+ } else if (cmd === "impact") {
872
+ if (!flags.positional) throw new Error("impact needs a target: cli.mjs impact <file|module> --repo <dir>");
873
+ const { graph } = readArtifacts();
874
+ const res = impactOf(graph, flags.positional, flags.depth ?? Infinity);
875
+ if (!res) throw new Error(`no such file or module in the index: ${flags.positional}`);
876
+ emit(JSON.stringify(res, null, 2) + "\n", flags.out);
877
+ } else if (cmd === "neighbors") {
878
+ if (!flags.positional) throw new Error("neighbors needs a target: cli.mjs neighbors <file|module> --repo <dir>");
879
+ const { graph } = readArtifacts();
880
+ const kinds = flags.kind ? new Set(flags.kind.split(",").map((k) => k.trim()).filter(Boolean)) : undefined;
881
+ const res = neighborsOf(graph, flags.positional, flags.depth ?? 1, kinds);
882
+ if (!res) throw new Error(`no such file or module in the index: ${flags.positional}`);
883
+ emit(JSON.stringify(res, null, 2) + "\n", flags.out);
763
884
  } else if (cmd === "mermaid") {
764
- const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
885
+ const { graph } = readArtifacts();
765
886
  emit(renderMermaid(graph, { module: flags.positional }), flags.out);
766
887
  } else if (cmd === "grep") {
767
888
  if (!flags.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
package/src/engine.ts CHANGED
@@ -28,8 +28,23 @@ export type {
28
28
  // Files tier: walk, read, filter, classify.
29
29
  export { walk, readText, DEFAULT_MAX_FILES } from "./walk.js";
30
30
  export type { WalkOptions, WalkedFile, WalkResult } from "./walk.js";
31
- export { scanRepo } from "./scan.js";
32
- export type { RepoScan, ScanOptions } from "./scan.js";
31
+ export { scanRepo, scanSummary } from "./scan.js";
32
+ export type { RepoScan, ScanOptions, ScanSummary, ExtractedRecord } from "./scan.js";
33
+ export { keptCodeFiles, buildCodeRecord } from "./scan.js";
34
+ // Reusing a persisted `.codeindex/` index instead of rebuilding it. The MCP
35
+ // server and every CLI read command go through this; a consumer that vendors
36
+ // the engine gets the same shortcut. Every function degrades to undefined
37
+ // (= "build it yourself") rather than throwing.
38
+ export { preloadSession, preloadArtifacts, readPersistedIndex, toCacheMap, INDEX_DIR } from "./preload.js";
39
+ export type { PersistedMeta, PersistedCacheEntry, PersistedCacheMap } from "./preload.js";
40
+ // Parallel extraction. scanRepoParallel is scanRepo with the code files
41
+ // extracted across worker_threads; it returns the same RepoScan, byte-for-byte,
42
+ // and degrades to the sequential path whenever workers are unavailable.
43
+ //
44
+ // `runExtractWorker` MUST stay exported: the worker bootstrap imports it BY NAME
45
+ // off this barrel, and a missing export is indistinguishable from "this is not
46
+ // the engine" — the pool would silently run sequential forever.
47
+ export { scanRepoParallel, extractInParallel, runExtractWorker, workerCount } from "./pool.js";
33
48
  export { compileGlobs } from "./glob.js";
34
49
  export { parseGitignore, isIgnored } from "./ignore.js";
35
50
  export type { IgnoreRule } from "./ignore.js";
@@ -177,8 +192,20 @@ export { findDeadCode } from "./deadcode.js";
177
192
  export type { DeadSymbol } from "./deadcode.js";
178
193
  export { symbolComplexity, riskHotspots, complexityOfSource } from "./complexity.js";
179
194
  export type { SymbolComplexity, RiskHotspot } from "./complexity.js";
180
- export { renderMermaid } from "./viz.js";
181
- export type { MermaidOptions } from "./viz.js";
195
+ export { renderMermaid, renderMermaidClustered } from "./viz.js";
196
+ export type { MermaidOptions, ClusteredMermaidOptions, ClusteredMermaidResult } from "./viz.js";
197
+
198
+ // Graph traversal: reverse-dependency closure ("what breaks if I change this")
199
+ // and bidirectional neighbourhood walks. Pure functions of a Graph, so a
200
+ // consumer holding a persisted graph.json answers both without a rescan.
201
+ export { impactOf, neighborsOf, reverseClosure, hubThreshold } from "./traverse.js";
202
+ export type { ImpactResult, ImpactedFile, NeighborResult, NeighborLink } from "./traverse.js";
203
+
204
+ // Diff review: git diff -> enclosing symbols -> blast radius -> risk-scored,
205
+ // reasons-first panel. `computeDelta` is the pure core (no git, no fs);
206
+ // `deltaFor` adds the git plumbing against a graph the caller supplies.
207
+ export { computeDelta, deltaFor, formatDeltaPanel, symbolsInHunks, RISK_WEIGHTS, DEFAULT_DELTA_DEPTH } from "./delta.js";
208
+ export type { DeltaOptions, DeltaResult, DeltaError, DeltaModule, DeltaChange, ChangedSymbol } from "./delta.js";
182
209
  export type { RepoMapOptions } from "./repomap.js";
183
210
 
184
211
  // MCP server over stdio (also reachable as `engine.mjs mcp`).
@@ -360,7 +360,10 @@ export function extractCode(rel: string, ext: string, content: string, opts: { m
360
360
  // extractors. Imports/pkg stay on the battle-tested regex path here — their
361
361
  // resolution is covered by resolve tests and the e2e ratchet; the new-language
362
362
  // AST importers land with their resolvers.
363
- const ast = extractAst(rel, ext, content, { maxCalls: opts.maxCallsPerFile });
363
+ // `imports: false` because of exactly that: `ast.refs` and `ast.pkg` were
364
+ // computed by a full extra tree traversal and then discarded right below, in
365
+ // favour of the regex results. Public `extractAst` still computes them.
366
+ const ast = extractAst(rel, ext, content, { maxCalls: opts.maxCallsPerFile, imports: false });
364
367
  const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
365
368
  // Add barrel re-exports the local def didn't already cover.
366
369
  const known = new Set(symbols.map((s) => s.name));
package/src/graph.ts CHANGED
Binary file