@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/README.md +26 -3
- package/docs/MIGRATION.md +107 -0
- package/package.json +1 -1
- package/scripts/engine.d.mts +243 -19
- package/scripts/engine.mjs +1768 -723
- 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 +201 -26
- 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,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[]): {
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
465
|
-
fileCount:
|
|
466
|
-
languages:
|
|
467
|
-
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 } =
|
|
624
|
+
const { graph } = readArtifacts();
|
|
472
625
|
emit(renderGraphJson(graph), flags.out);
|
|
473
626
|
} else if (cmd === "symbols") {
|
|
474
|
-
const { symbols } =
|
|
627
|
+
const { symbols } = readArtifacts();
|
|
475
628
|
emit(renderSymbolsJson(symbols), flags.out);
|
|
476
629
|
} else if (cmd === "scip") {
|
|
477
|
-
const scan =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 } =
|
|
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 } =
|
|
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 =
|
|
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(
|
|
854
|
+
emit(JSON.stringify(findDeadCode(readScan()), null, 2) + "\n", flags.out);
|
|
702
855
|
} else if (cmd === "complexity") {
|
|
703
|
-
const scan =
|
|
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 =
|
|
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 } =
|
|
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`).
|
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
|