@maxgfr/codeindex 2.17.0 → 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,10 +108,14 @@ 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
- Flags:
117
+ Flags (accepted before OR after the subcommand: '--repo X scan' and
118
+ 'scan --repo X' are equivalent):
104
119
  --repo <dir> Repo root (default: cwd)
105
120
  --out <file> Write output to a file instead of stdout (\`scip\`: --out -
106
121
  writes the binary index to stdout)
@@ -116,6 +131,16 @@ Flags:
116
131
  --max-bytes <n> Skip files above this size (default 1 MiB)
117
132
  --max-calls <n> Per-file call-site cap for extraction (default 512)
118
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
119
144
  --config <file> Rules config for \`rules\` (JSON: [{name, from, to, …}])
120
145
  --limit <n> Max results for \`search\` (default 20)
121
146
  --no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
@@ -143,6 +168,9 @@ interface CliFlags {
143
168
  maxBytes?: number;
144
169
  maxCalls?: number;
145
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
146
174
  since?: string;
147
175
  ignoreCase?: boolean;
148
176
  maxHits?: number;
@@ -154,6 +182,11 @@ interface CliFlags {
154
182
  recall?: boolean; // callers: recall-oriented binding
155
183
  run?: boolean; // `embed serve`: actually run the docker command (default: print)
156
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
157
190
  positional?: string; // e.g. the grep pattern or search query
158
191
  }
159
192
 
@@ -189,6 +222,15 @@ function parseFlags(args: string[]): CliFlags {
189
222
  else if (a === "--max-hits") flags.maxHits = num();
190
223
  else if (a === "--budget-tokens") flags.budgetTokens = num();
191
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
+ }
192
234
  else if (a === "--since") flags.since = next();
193
235
  else if (a === "--config") flags.config = resolve(next());
194
236
  else if (a === "--limit") flags.limit = num();
@@ -196,6 +238,11 @@ function parseFlags(args: string[]): CliFlags {
196
238
  else if (a === "--semantic") flags.semantic = true;
197
239
  else if (a === "--recall") flags.recall = true;
198
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;
199
246
  else if (!a.startsWith("--") && flags.positional === undefined) flags.positional = a;
200
247
  else throw new Error(`unknown flag: ${a}`);
201
248
  }
@@ -237,9 +284,14 @@ const SCANLESS_COMMANDS = new Set(["grep", "churn", "coupling", "workspaces", "g
237
284
  // dispatch site). `--repo` is resolved to an absolute path and must exist: a
238
285
  // server pinned to a typo'd directory would otherwise answer every tool call
239
286
  // with the same confusing per-call error instead of failing at startup.
240
- 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
+ } {
241
292
  let defaultRepo: string | undefined;
242
293
  let name: string | undefined;
294
+ let maxResponseBytes: number | undefined;
243
295
  for (let i = 0; i < argv.length; i++) {
244
296
  const a = argv[i];
245
297
  if (a === "--repo") {
@@ -250,15 +302,76 @@ export function parseMcpFlags(argv: string[]): { defaultRepo?: string; serverInf
250
302
  const v = argv[++i];
251
303
  if (!v) throw new Error("--server-name requires a value");
252
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;
253
310
  } else {
254
311
  throw new Error(`unknown flag for \`mcp\`: ${a}`);
255
312
  }
256
313
  }
257
314
  if (defaultRepo && !existsSync(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
258
- return { defaultRepo, serverInfo: name ? { name } : undefined };
315
+ return { defaultRepo, serverInfo: name ? { name } : undefined, maxResponseBytes };
259
316
  }
260
317
 
261
- export async function runCli(argv: string[]): Promise<void> {
318
+ // Flags that consume the following argv element. Needed to hoist leading flags
319
+ // past the subcommand without mistaking a flag's VALUE for the command name
320
+ // (`--repo /x scan`: `/x` must not be read as the command).
321
+ const VALUE_FLAGS = new Set([
322
+ "--repo",
323
+ "--out",
324
+ "--project-root",
325
+ "--include",
326
+ "--exclude",
327
+ "--scope",
328
+ "--ignore-dir",
329
+ "--max-files",
330
+ "--max-bytes",
331
+ "--max-calls",
332
+ "--max-hits",
333
+ "--budget-tokens",
334
+ "--since",
335
+ "--config",
336
+ "--limit",
337
+ "--server-name",
338
+ "--workers",
339
+ "--index",
340
+ "--max-response-bytes",
341
+ ]);
342
+
343
+ // Accept global flags BEFORE the subcommand as well as after, so
344
+ // `codeindex --repo /x scan` and `codeindex scan --repo /x` agree. A strict
345
+ // subcommand-first parser reads the leading flag as the command name and fails
346
+ // with a baffling "unknown flag: scan".
347
+ //
348
+ // This is not only ergonomics. A host that wraps the CLI may splice a flag in
349
+ // right after the binary name — iterion's rewriter `inject_flag` does exactly
350
+ // that, turning `codeindex grep foo` into `codeindex --max-hits 40 grep foo` —
351
+ // and without hoisting that command cannot run at all.
352
+ //
353
+ // Returns argv unchanged when there is nothing to hoist, so `--help`,
354
+ // `--version` and a bare subcommand all keep their existing behaviour.
355
+ export function hoistLeadingFlags(argv: string[]): string[] {
356
+ const lead: string[] = [];
357
+ let i = 0;
358
+ while (i < argv.length) {
359
+ const a = argv[i];
360
+ if (a === undefined || !a.startsWith("-")) break;
361
+ lead.push(a);
362
+ i++;
363
+ if (VALUE_FLAGS.has(a) && i < argv.length) {
364
+ lead.push(argv[i] as string);
365
+ i++;
366
+ }
367
+ }
368
+ // No leading flags, or they were the whole line (`--help`, `--version`).
369
+ if (lead.length === 0 || i >= argv.length) return argv;
370
+ return [argv[i] as string, ...lead, ...argv.slice(i + 1)];
371
+ }
372
+
373
+ export async function runCli(rawArgv: string[]): Promise<void> {
374
+ const argv = hoistLeadingFlags(rawArgv);
262
375
  const [cmd, ...rest] = argv;
263
376
  if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
264
377
  process.stdout.write(HELP);
@@ -315,6 +428,38 @@ export async function runCli(argv: string[]): Promise<void> {
315
428
  await ensureGrammars(grammarKeysForExts(precomputedWalk.files.map((f) => f.ext)));
316
429
  }
317
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
+
318
463
  if (cmd === "index") {
319
464
  if (!flags.out) throw new Error("index needs --out <dir>");
320
465
  const outDir = flags.out;
@@ -356,7 +501,12 @@ export async function runCli(argv: string[]): Promise<void> {
356
501
  } catch {
357
502
  // no cache yet (or unreadable) — cold build
358
503
  }
359
- 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
+ });
360
510
  const modelDir = resolveEmbedModelDir(flags.repo);
361
511
  const model = modelDir ? loadEmbedModel(modelDir) : undefined;
362
512
 
@@ -458,23 +608,26 @@ export async function runCli(argv: string[]): Promise<void> {
458
608
  process.stderr.write(`codeindex: ${scan.files.length} files → ${outDir}/graph.json + symbols.json${embedNote}${scan.capped ? " (capped)" : ""}\n`);
459
609
  }
460
610
  } else if (cmd === "scan") {
461
- 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));
462
615
  const summary = {
463
616
  engineVersion: ENGINE_VERSION,
464
- commit: scan.commit,
465
- fileCount: scan.files.length,
466
- languages: scan.languages,
467
- capped: scan.capped,
617
+ commit: s.commit,
618
+ fileCount: s.fileCount,
619
+ languages: s.languages,
620
+ capped: s.capped,
468
621
  };
469
622
  emit(JSON.stringify(summary, null, 2) + "\n", flags.out);
470
623
  } else if (cmd === "graph") {
471
- const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
624
+ const { graph } = readArtifacts();
472
625
  emit(renderGraphJson(graph), flags.out);
473
626
  } else if (cmd === "symbols") {
474
- const { symbols } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
627
+ const { symbols } = readArtifacts();
475
628
  emit(renderSymbolsJson(symbols), flags.out);
476
629
  } else if (cmd === "scip") {
477
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
630
+ const scan = readScan();
478
631
  const bytes = renderScip(scan, { projectRoot: flags.projectRoot });
479
632
  const out = flags.out ?? resolve("index.scip");
480
633
  if (out === "-") process.stdout.write(Buffer.from(bytes));
@@ -483,14 +636,14 @@ export async function runCli(argv: string[]): Promise<void> {
483
636
  process.stderr.write(`codeindex: SCIP index → ${out} (${bytes.length} bytes)\n`);
484
637
  }
485
638
  } else if (cmd === "callers") {
486
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
639
+ const scan = readScan();
487
640
  const index = buildCallerIndex(scan, undefined, { recall: flags.recall });
488
641
  const obj: Record<string, unknown> = {};
489
642
  for (const [name, entry] of index) obj[name] = entry;
490
643
  emit(JSON.stringify(obj, null, 2) + "\n", flags.out);
491
644
  } else if (cmd === "search") {
492
645
  if (!flags.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
493
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
646
+ const scan = readScan();
494
647
  if (flags.semantic) {
495
648
  const endpoint = resolveEmbedEndpoint();
496
649
  const lexical = (): void => {
@@ -595,7 +748,7 @@ export async function runCli(argv: string[]): Promise<void> {
595
748
  }
596
749
  const model = loadEmbedModel(modelDir)!;
597
750
  mkdirSync(flags.out, { recursive: true });
598
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
751
+ const scan = readScan();
599
752
  const index = buildEmbeddingIndex(scan, model);
600
753
  writeFileSync(join(flags.out, "embeddings.bin"), serializeEmbeddings(index));
601
754
  process.stderr.write(`codeindex: ${index.records.length} embedding records → ${flags.out}/embeddings.bin (model ${model.modelId})\n`);
@@ -667,7 +820,7 @@ export async function runCli(argv: string[]): Promise<void> {
667
820
  } else if (cmd === "rules") {
668
821
  if (!flags.config) throw new Error("rules needs --config <codeindex.rules.json>");
669
822
  const rules = parseRules(JSON.parse(readFileSync(flags.config, "utf8")));
670
- const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
823
+ const { graph } = readArtifacts();
671
824
  const violations = checkRules(graph, rules);
672
825
  const errors = violations.filter((v) => v.severity === "error").length;
673
826
  emit(JSON.stringify({ errors, warnings: violations.length - errors, violations }, null, 2) + "\n", flags.out);
@@ -688,26 +841,48 @@ export async function runCli(argv: string[]): Promise<void> {
688
841
  for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k)!;
689
842
  emit(JSON.stringify({ ok, churn: sorted }, null, 2) + "\n", flags.out);
690
843
  } else if (cmd === "repomap") {
691
- const { scan, graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
844
+ const { scan, graph } = readArtifacts();
692
845
  emit(renderRepoMap(scan, graph, { budgetTokens: flags.budgetTokens }), flags.out);
693
846
  } else if (cmd === "hotspots") {
694
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
847
+ const scan = readScan();
695
848
  const { churn, ok } = gitChurn(flags.repo, { since: flags.since });
696
849
  emit(JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan, churn) }, null, 2) + "\n", flags.out);
697
850
  } else if (cmd === "coupling") {
698
851
  const { ok, couplings } = changeCoupling(flags.repo, { since: flags.since });
699
852
  emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags.out);
700
853
  } else if (cmd === "deadcode") {
701
- 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);
702
855
  } else if (cmd === "complexity") {
703
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
856
+ const scan = readScan();
704
857
  emit(JSON.stringify(symbolComplexity(scan, flags.positional), null, 2) + "\n", flags.out);
705
858
  } else if (cmd === "risk") {
706
- const scan = scanRepo(flags.repo, scanOptions(flags, precomputedWalk));
859
+ const scan = readScan();
707
860
  const { churn, ok } = gitChurn(flags.repo, { since: flags.since });
708
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);
709
884
  } else if (cmd === "mermaid") {
710
- const { graph } = buildIndexArtifacts(flags.repo, scanOptions(flags, precomputedWalk));
885
+ const { graph } = readArtifacts();
711
886
  emit(renderMermaid(graph, { module: flags.positional }), flags.out);
712
887
  } else if (cmd === "grep") {
713
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