@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/README.md +26 -3
- package/docs/MIGRATION.md +107 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +242 -18
- package/scripts/engine.mjs +1730 -721
- package/src/ast/extract.ts +180 -142
- package/src/coupling.ts +0 -0
- package/src/delta.ts +417 -0
- package/src/derived.ts +17 -0
- package/src/engine-cli.ts +145 -24
- package/src/engine.ts +31 -4
- package/src/extract/code.ts +4 -1
- package/src/graph.ts +0 -0
- package/src/mcp.ts +469 -212
- package/src/pool.ts +271 -0
- package/src/preload.ts +159 -0
- package/src/render/scip.ts +80 -20
- package/src/render/symbols-json.ts +3 -2
- package/src/scan.ts +142 -17
- package/src/traverse.ts +164 -0
- package/src/types.ts +1 -1
- package/src/viz.ts +91 -1
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[]): {
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
519
|
-
fileCount:
|
|
520
|
-
languages:
|
|
521
|
-
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 } =
|
|
624
|
+
const { graph } = readArtifacts();
|
|
526
625
|
emit(renderGraphJson(graph), flags.out);
|
|
527
626
|
} else if (cmd === "symbols") {
|
|
528
|
-
const { symbols } =
|
|
627
|
+
const { symbols } = readArtifacts();
|
|
529
628
|
emit(renderSymbolsJson(symbols), flags.out);
|
|
530
629
|
} else if (cmd === "scip") {
|
|
531
|
-
const scan =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 } =
|
|
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 } =
|
|
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 =
|
|
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(
|
|
854
|
+
emit(JSON.stringify(findDeadCode(readScan()), null, 2) + "\n", flags.out);
|
|
756
855
|
} else if (cmd === "complexity") {
|
|
757
|
-
const scan =
|
|
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 =
|
|
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 } =
|
|
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`).
|
package/src/extract/code.ts
CHANGED
|
@@ -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
|
-
|
|
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
|