codeblast 0.2.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/dist/bin.js ADDED
@@ -0,0 +1,2299 @@
1
+ #!/usr/bin/env node
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // src/db.ts
15
+ import { createRequire } from "node:module";
16
+ function openDatabase(file, opts = {}) {
17
+ if (isBun) {
18
+ const mod = require2("bun:sqlite");
19
+ return new mod.Database(file, { readonly: !!opts.readonly, create: true });
20
+ }
21
+ const mod = require2("node:sqlite");
22
+ return new mod.DatabaseSync(file, { readOnly: !!opts.readonly });
23
+ }
24
+ function transaction(db, fn) {
25
+ return (...args) => {
26
+ db.exec("BEGIN");
27
+ try {
28
+ fn(...args);
29
+ db.exec("COMMIT");
30
+ } catch (e) {
31
+ db.exec("ROLLBACK");
32
+ throw e;
33
+ }
34
+ };
35
+ }
36
+ var require2, isBun;
37
+ var init_db = __esm(() => {
38
+ require2 = createRequire(import.meta.url);
39
+ isBun = "Bun" in globalThis;
40
+ if (!isBun) {
41
+ const emit = process.emitWarning;
42
+ const filtered = (warning, ...rest) => {
43
+ const text = typeof warning === "string" ? warning : warning.message;
44
+ if (text.includes("SQLite is an experimental feature"))
45
+ return;
46
+ Reflect.apply(emit, process, [warning, ...rest]);
47
+ };
48
+ process.emitWarning = filtered;
49
+ }
50
+ });
51
+
52
+ // src/schema.ts
53
+ function openGraph(dbPath) {
54
+ const db = openDatabase(dbPath);
55
+ db.exec("PRAGMA journal_mode = WAL;");
56
+ db.exec(DDL);
57
+ try {
58
+ db.exec("ALTER TABLE nodes ADD COLUMN signature TEXT NOT NULL DEFAULT ''");
59
+ } catch {}
60
+ return db;
61
+ }
62
+ function invalidateFile(db, relPath) {
63
+ db.prepare("DELETE FROM nodes WHERE src_file = ?").run(relPath);
64
+ db.prepare("DELETE FROM edges WHERE src_file = ?").run(relPath);
65
+ db.prepare("DELETE FROM blind_spots WHERE src_file = ?").run(relPath);
66
+ db.prepare("DELETE FROM import_bindings WHERE src_file = ?").run(relPath);
67
+ db.prepare("DELETE FROM files WHERE path = ?").run(relPath);
68
+ }
69
+ var DDL = `
70
+ CREATE TABLE IF NOT EXISTS meta (
71
+ key TEXT PRIMARY KEY,
72
+ value TEXT NOT NULL
73
+ );
74
+ CREATE TABLE IF NOT EXISTS files (
75
+ path TEXT PRIMARY KEY, -- 相对仓库根
76
+ hash TEXT NOT NULL -- 内容 hash,增量判断
77
+ );
78
+ CREATE TABLE IF NOT EXISTS nodes (
79
+ id TEXT NOT NULL,
80
+ kind TEXT NOT NULL,
81
+ name TEXT NOT NULL,
82
+ file TEXT NOT NULL,
83
+ line INTEGER NOT NULL,
84
+ end_line INTEGER NOT NULL,
85
+ exported INTEGER NOT NULL DEFAULT 0,
86
+ signature TEXT NOT NULL DEFAULT '',
87
+ src_file TEXT NOT NULL,
88
+ PRIMARY KEY (id)
89
+ );
90
+ CREATE INDEX IF NOT EXISTS idx_nodes_src_file ON nodes(src_file);
91
+ CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);
92
+ CREATE TABLE IF NOT EXISTS edges (
93
+ src TEXT NOT NULL,
94
+ dst TEXT NOT NULL,
95
+ kind TEXT NOT NULL,
96
+ file TEXT NOT NULL,
97
+ line INTEGER NOT NULL,
98
+ confidence TEXT NOT NULL DEFAULT 'exact',
99
+ src_file TEXT NOT NULL,
100
+ PRIMARY KEY (src, dst, kind, file, line)
101
+ );
102
+ CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src);
103
+ CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst);
104
+ CREATE INDEX IF NOT EXISTS idx_edges_src_file ON edges(src_file);
105
+ CREATE TABLE IF NOT EXISTS blind_spots (
106
+ file TEXT NOT NULL,
107
+ line INTEGER NOT NULL,
108
+ reason TEXT NOT NULL,
109
+ src_file TEXT NOT NULL,
110
+ PRIMARY KEY (file, line, reason)
111
+ );
112
+ CREATE INDEX IF NOT EXISTS idx_blind_src_file ON blind_spots(src_file);
113
+ CREATE TABLE IF NOT EXISTS import_bindings (
114
+ importer TEXT NOT NULL, -- 引入方文件
115
+ imported TEXT NOT NULL, -- 被引入文件
116
+ names TEXT NOT NULL, -- 逗号分隔的具名绑定;空串+star=0 表示仅副作用 import
117
+ star INTEGER NOT NULL DEFAULT 0, -- 1 = namespace/星号/default 等无法枚举 → 禁止剪枝
118
+ src_file TEXT NOT NULL,
119
+ PRIMARY KEY (importer, imported)
120
+ );
121
+ CREATE INDEX IF NOT EXISTS idx_bindings_imported ON import_bindings(imported);
122
+ CREATE INDEX IF NOT EXISTS idx_bindings_src_file ON import_bindings(src_file);
123
+ `;
124
+ var init_schema = __esm(() => {
125
+ init_db();
126
+ });
127
+
128
+ // src/extract.ts
129
+ import ts from "typescript";
130
+ import path from "node:path";
131
+ import fs from "node:fs";
132
+ function injectWorkspacePaths(base, pkgs, rootDir) {
133
+ if (pkgs.size === 0)
134
+ return base;
135
+ const options = { ...base, baseUrl: base.baseUrl ?? rootDir, paths: { ...base.paths } };
136
+ for (const [name, dir] of pkgs) {
137
+ const rel = path.relative(options.baseUrl, dir) || ".";
138
+ options.paths[name] ??= [`${rel}/src/index.ts`, `${rel}/index.ts`];
139
+ options.paths[`${name}/*`] ??= [`${rel}/src/*`, `${rel}/*`];
140
+ }
141
+ return options;
142
+ }
143
+ function workspaceGlobs(repoRoot) {
144
+ try {
145
+ const pj = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"));
146
+ const ws = Array.isArray(pj.workspaces) ? pj.workspaces : pj.workspaces?.packages;
147
+ return Array.isArray(ws) ? ws.filter((w) => typeof w === "string" && !w.startsWith("!")) : [];
148
+ } catch {
149
+ return [];
150
+ }
151
+ }
152
+ function discoverWorkspacePackages(repoRoot) {
153
+ const out = new Map;
154
+ const tryAdd = (dir) => {
155
+ const pj = path.join(dir, "package.json");
156
+ if (!fs.existsSync(pj))
157
+ return;
158
+ try {
159
+ const parsed = JSON.parse(fs.readFileSync(pj, "utf8"));
160
+ if (parsed && typeof parsed === "object" && "name" in parsed && typeof parsed.name === "string") {
161
+ out.set(parsed.name, dir);
162
+ }
163
+ } catch {}
164
+ };
165
+ for (const g of workspaceGlobs(repoRoot)) {
166
+ const star = g.endsWith("/*");
167
+ const base = path.join(repoRoot, star ? g.slice(0, -2) : g);
168
+ if (!fs.existsSync(base))
169
+ continue;
170
+ if (!star) {
171
+ tryAdd(base);
172
+ continue;
173
+ }
174
+ for (const sub of fs.readdirSync(base, { withFileTypes: true }))
175
+ if (sub.isDirectory())
176
+ tryAdd(path.join(base, sub.name));
177
+ }
178
+ for (const entry of fs.readdirSync(repoRoot, { withFileTypes: true })) {
179
+ if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith("."))
180
+ continue;
181
+ const dir = path.join(repoRoot, entry.name);
182
+ tryAdd(dir);
183
+ for (const sub of ["packages", "apps", "libs"].includes(entry.name) ? fs.readdirSync(dir, { withFileTypes: true }) : []) {
184
+ if (sub.isDirectory())
185
+ tryAdd(path.join(dir, sub.name));
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+
191
+ class Extractor {
192
+ program;
193
+ checker;
194
+ parsedConfig;
195
+ programBuilt = false;
196
+ rootDir;
197
+ workspacePkgs = new Map;
198
+ implementers = new Map;
199
+ pendingBindings = new Map;
200
+ constructor(tsconfigPath, repoRoot) {
201
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
202
+ if (configFile.error)
203
+ throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, `
204
+ `));
205
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(tsconfigPath));
206
+ this.rootDir = repoRoot ?? path.dirname(path.resolve(tsconfigPath));
207
+ this.workspacePkgs = discoverWorkspacePackages(this.rootDir);
208
+ this.parsedConfig = parsed;
209
+ }
210
+ fileNames() {
211
+ return this.parsedConfig.fileNames.filter((f) => !f.includes("node_modules") && !f.endsWith(".d.ts"));
212
+ }
213
+ ensureProgram() {
214
+ if (this.programBuilt)
215
+ return;
216
+ const options = injectWorkspacePaths(this.parsedConfig.options, this.workspacePkgs, this.rootDir);
217
+ this.program = ts.createProgram({ rootNames: this.parsedConfig.fileNames, options });
218
+ this.checker = this.program.getTypeChecker();
219
+ this.programBuilt = true;
220
+ }
221
+ static forFiles(fileNames, baseTsconfigPath, repoRoot) {
222
+ const configFile = ts.readConfigFile(baseTsconfigPath, ts.sys.readFile);
223
+ const parsed = ts.parseJsonConfigFileContent(configFile.config ?? {}, ts.sys, path.dirname(baseTsconfigPath));
224
+ const ex = Object.create(Extractor.prototype);
225
+ ex.rootDir = repoRoot;
226
+ ex.workspacePkgs = discoverWorkspacePackages(repoRoot);
227
+ ex.implementers = new Map;
228
+ ex.pendingBindings = new Map;
229
+ ex.reentryCache = new Map;
230
+ ex.parsedConfig = { ...parsed, fileNames };
231
+ ex.programBuilt = false;
232
+ return ex;
233
+ }
234
+ sourceFiles() {
235
+ this.ensureProgram();
236
+ return this.program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes("node_modules"));
237
+ }
238
+ rel(fileName) {
239
+ return path.relative(this.rootDir, fileName);
240
+ }
241
+ collectImplementers() {
242
+ for (const sf of this.sourceFiles()) {
243
+ const visit = (node) => {
244
+ if (ts.isClassDeclaration(node) && node.heritageClauses) {
245
+ for (const clause of node.heritageClauses) {
246
+ for (const typeNode of clause.types) {
247
+ let sym = this.checker.getSymbolAtLocation(typeNode.expression);
248
+ if (sym && sym.flags & ts.SymbolFlags.Alias)
249
+ sym = this.checker.getAliasedSymbol(sym);
250
+ const decl = sym?.declarations?.[0];
251
+ if (!decl)
252
+ continue;
253
+ const parentId = this.nodeIdOfDecl(decl);
254
+ const classId = this.nodeIdOfDecl(node);
255
+ if (parentId && classId) {
256
+ const list = this.implementers.get(parentId) ?? [];
257
+ list.push(classId);
258
+ this.implementers.set(parentId, list);
259
+ }
260
+ }
261
+ }
262
+ }
263
+ ts.forEachChild(node, visit);
264
+ };
265
+ visit(sf);
266
+ }
267
+ }
268
+ extractFile(sf) {
269
+ const relPath = this.rel(sf.fileName);
270
+ const isTest = TEST_FILE_RE.test(relPath);
271
+ const nodes = [];
272
+ const edges = [];
273
+ const blindSpots = [];
274
+ this.pendingBindings.clear();
275
+ const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
276
+ const endLineOf = (node) => sf.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
277
+ nodes.push({
278
+ id: relPath,
279
+ kind: "file",
280
+ name: path.basename(relPath),
281
+ file: relPath,
282
+ line: 1,
283
+ end_line: endLineOf(sf),
284
+ exported: 0,
285
+ signature: "",
286
+ src_file: relPath
287
+ });
288
+ for (const stmt of sf.statements) {
289
+ if (ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt)) {
290
+ const spec = stmt.moduleSpecifier;
291
+ if (spec && ts.isStringLiteral(spec)) {
292
+ const resolved = this.resolveModule(spec.text, sf.fileName);
293
+ if (!resolved) {
294
+ for (const entry of this.externalReentry(spec.text, sf.fileName)) {
295
+ edges.push({
296
+ src: relPath,
297
+ dst: this.rel(entry),
298
+ kind: "imports",
299
+ file: relPath,
300
+ line: lineOf(stmt),
301
+ confidence: "conservative",
302
+ src_file: relPath
303
+ });
304
+ }
305
+ } else {
306
+ edges.push({
307
+ src: relPath,
308
+ dst: this.rel(resolved),
309
+ kind: "imports",
310
+ file: relPath,
311
+ line: lineOf(stmt),
312
+ confidence: "exact",
313
+ src_file: relPath
314
+ });
315
+ const dstRel = this.rel(resolved);
316
+ let names = [];
317
+ let star = false;
318
+ if (ts.isImportDeclaration(stmt)) {
319
+ const c = stmt.importClause;
320
+ if (!c)
321
+ star = true;
322
+ else {
323
+ if (c.name)
324
+ star = true;
325
+ if (c.namedBindings) {
326
+ if (ts.isNamespaceImport(c.namedBindings))
327
+ star = true;
328
+ else
329
+ for (const el of c.namedBindings.elements) {
330
+ const src = (el.propertyName ?? el.name).text;
331
+ if (src === "default")
332
+ star = true;
333
+ else
334
+ names.push(src);
335
+ }
336
+ }
337
+ }
338
+ } else {
339
+ if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
340
+ for (const el of stmt.exportClause.elements) {
341
+ if (el.propertyName && el.propertyName.text !== el.name.text)
342
+ star = true;
343
+ else if (el.name.text === "default")
344
+ star = true;
345
+ else
346
+ names.push(el.name.text);
347
+ }
348
+ } else
349
+ star = true;
350
+ }
351
+ const key = `${relPath}\x00${dstRel}`;
352
+ const prev = this.pendingBindings.get(key);
353
+ if (prev) {
354
+ prev.star = prev.star || star;
355
+ for (const n of names)
356
+ prev.names.add(n);
357
+ } else {
358
+ this.pendingBindings.set(key, { star, names: new Set(names) });
359
+ }
360
+ }
361
+ } else if (spec) {
362
+ blindSpots.push({ file: relPath, line: lineOf(stmt), reason: "non-literal module specifier", src_file: relPath });
363
+ }
364
+ }
365
+ }
366
+ const enclosing = [];
367
+ const declKindOf = (node) => {
368
+ if (ts.isFunctionDeclaration(node))
369
+ return isTest ? "test" : "function";
370
+ if (ts.isMethodDeclaration(node))
371
+ return "method";
372
+ if (ts.isClassDeclaration(node))
373
+ return "class";
374
+ if (ts.isInterfaceDeclaration(node))
375
+ return "interface";
376
+ if (ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node))
377
+ return "interface";
378
+ if (ts.isVariableDeclaration(node) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)))
379
+ return isTest ? "test" : "function";
380
+ if (ts.isVariableDeclaration(node) && ts.isVariableDeclarationList(node.parent) && ts.isVariableStatement(node.parent.parent) && ts.isSourceFile(node.parent.parent.parent) && this.isExported(node))
381
+ return "const";
382
+ return;
383
+ };
384
+ const norm = (s) => s.replace(/\s+/g, " ").trim().slice(0, 200);
385
+ const typeSignature = (node) => {
386
+ if (ts.isInterfaceDeclaration(node))
387
+ return norm(node.members.map((m) => m.getText(sf)).join(" "));
388
+ if (ts.isTypeAliasDeclaration(node))
389
+ return norm(node.type.getText(sf));
390
+ if (ts.isEnumDeclaration(node))
391
+ return norm(node.members.map((m) => m.getText(sf)).join(" "));
392
+ if (ts.isVariableDeclaration(node))
393
+ return norm(node.type ? node.type.getText(sf) : node.initializer?.getText(sf) ?? "");
394
+ return "";
395
+ };
396
+ const visit = (node) => {
397
+ const kind = declKindOf(node);
398
+ const id = kind ? this.nodeIdOfDecl(node) : undefined;
399
+ if (kind && id) {
400
+ const name = this.declName(node) ?? "<anonymous>";
401
+ let signature = "";
402
+ const fnLike = ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) ? node : ts.isVariableDeclaration(node) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) ? node.initializer : undefined;
403
+ if (fnLike)
404
+ signature = fnLike.parameters.map((p) => p.getText(sf)).join(", ").slice(0, 200);
405
+ else
406
+ signature = typeSignature(node);
407
+ const exported = ts.isMethodDeclaration(node) && ts.isClassDeclaration(node.parent) ? this.isExported(node.parent) && !(ts.getCombinedModifierFlags(node) & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) : this.isExported(node);
408
+ nodes.push({
409
+ id,
410
+ kind,
411
+ name,
412
+ file: relPath,
413
+ line: lineOf(node),
414
+ end_line: endLineOf(node),
415
+ exported: exported ? 1 : 0,
416
+ signature,
417
+ src_file: relPath
418
+ });
419
+ edges.push({
420
+ src: relPath,
421
+ dst: id,
422
+ kind: "contains",
423
+ file: relPath,
424
+ line: lineOf(node),
425
+ confidence: "exact",
426
+ src_file: relPath
427
+ });
428
+ if (ts.isClassDeclaration(node) && node.heritageClauses) {
429
+ for (const clause of node.heritageClauses) {
430
+ const ek = clause.token === ts.SyntaxKind.ImplementsKeyword ? "implements" : "extends";
431
+ for (const t of clause.types) {
432
+ let sym = this.checker.getSymbolAtLocation(t.expression);
433
+ if (sym && sym.flags & ts.SymbolFlags.Alias)
434
+ sym = this.checker.getAliasedSymbol(sym);
435
+ const decl = sym?.declarations?.[0];
436
+ const dst = decl ? this.nodeIdOfDecl(decl) : undefined;
437
+ if (dst)
438
+ edges.push({ src: id, dst, kind: ek, file: relPath, line: lineOf(clause), confidence: "exact", src_file: relPath });
439
+ }
440
+ }
441
+ }
442
+ }
443
+ if (kind && id)
444
+ enclosing.push(id);
445
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
446
+ const caller = [...enclosing].reverse().find(Boolean) ?? relPath;
447
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
448
+ const arg = node.arguments[0];
449
+ if (arg && ts.isStringLiteral(arg)) {
450
+ const resolved = this.resolveModule(arg.text, sf.fileName);
451
+ if (resolved) {
452
+ edges.push({
453
+ src: relPath,
454
+ dst: this.rel(resolved),
455
+ kind: "imports",
456
+ file: relPath,
457
+ line: lineOf(node),
458
+ confidence: "exact",
459
+ src_file: relPath
460
+ });
461
+ }
462
+ } else {
463
+ blindSpots.push({ file: relPath, line: lineOf(node), reason: `dynamic import: ${(arg?.getText() ?? "").slice(0, 80)}`, src_file: relPath });
464
+ }
465
+ } else {
466
+ this.resolveCall(node, caller, relPath, lineOf(node), edges, blindSpots);
467
+ }
468
+ }
469
+ ts.forEachChild(node, visit);
470
+ if (kind && id)
471
+ enclosing.pop();
472
+ };
473
+ visit(sf);
474
+ if (isTest) {
475
+ for (const e of edges) {
476
+ if (e.kind !== "calls")
477
+ continue;
478
+ const dstFile = e.dst.split("#")[0];
479
+ if (dstFile === e.dst || TEST_FILE_RE.test(dstFile))
480
+ continue;
481
+ edges.push({ ...e, kind: "tests" });
482
+ }
483
+ }
484
+ const bindings = [...this.pendingBindings.entries()].map(([key, v]) => {
485
+ const [importer, imported] = key.split("\x00");
486
+ return { importer, imported, names: [...v.names].join(","), star: v.star ? 1 : 0, src_file: relPath };
487
+ });
488
+ return { nodes, edges, blindSpots, bindings };
489
+ }
490
+ resolveCall(call, caller, relPath, line, edges, blindSpots) {
491
+ const expr = call.expression;
492
+ let sym = this.checker.getSymbolAtLocation(expr);
493
+ if (sym && sym.flags & ts.SymbolFlags.Alias)
494
+ sym = this.checker.getAliasedSymbol(sym);
495
+ const decl = sym?.valueDeclaration ?? sym?.declarations?.[0];
496
+ if (!decl) {
497
+ const structural = ts.isElementAccessExpression(expr) || ts.isPropertyAccessExpression(expr) && ["call", "apply", "bind"].includes(expr.name.text);
498
+ let leftmost = expr;
499
+ while (ts.isPropertyAccessExpression(leftmost) || ts.isCallExpression(leftmost) || ts.isNonNullExpression(leftmost)) {
500
+ leftmost = leftmost.expression;
501
+ }
502
+ const rootName = ts.isIdentifier(leftmost) ? leftmost.text : undefined;
503
+ const TEST_GLOBALS = {
504
+ describe: true,
505
+ it: true,
506
+ test: true,
507
+ expect: true,
508
+ expectTypeOf: true,
509
+ vi: true,
510
+ jest: true,
511
+ beforeEach: true,
512
+ afterEach: true,
513
+ beforeAll: true,
514
+ afterAll: true,
515
+ suite: true
516
+ };
517
+ const reason = structural ? `dynamic call: ${expr.getText().slice(0, 80)}` : rootName && TEST_GLOBALS[rootName] ? `test-global: ${rootName}` : `unresolved call: ${expr.getText().slice(0, 80)}`;
518
+ blindSpots.push({ file: relPath, line, reason, src_file: relPath });
519
+ return;
520
+ }
521
+ const declFile = decl.getSourceFile();
522
+ if (declFile.isDeclarationFile || declFile.fileName.includes("node_modules")) {
523
+ const SUBPROCESS_APIS = {
524
+ exec: true,
525
+ execSync: true,
526
+ execFile: true,
527
+ execFileSync: true,
528
+ spawn: true,
529
+ spawnSync: true,
530
+ fork: true
531
+ };
532
+ const calleeName = sym?.name ?? "";
533
+ if (SUBPROCESS_APIS[calleeName] && declFile.fileName.includes("child_process")) {
534
+ blindSpots.push({ file: relPath, line, reason: `subprocess spawn: ${calleeName}`, src_file: relPath });
535
+ }
536
+ return;
537
+ }
538
+ const dst = this.nodeIdOfDecl(decl);
539
+ if (!dst)
540
+ return;
541
+ if (ts.isVariableDeclaration(decl) && !(decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) && !(ts.isVariableDeclarationList(decl.parent) && ts.isVariableStatement(decl.parent.parent) && ts.isSourceFile(decl.parent.parent.parent) && this.isExported(decl)))
542
+ return;
543
+ const push = (target, confidence) => edges.push({ src: caller, dst: target, kind: "calls", file: relPath, line, confidence, src_file: relPath });
544
+ const isAbstractTarget = ts.isMethodSignature(decl) || ts.isMethodDeclaration(decl) && !!(ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Abstract) || ts.isInterfaceDeclaration(decl.parent ?? decl);
545
+ if (isAbstractTarget && ts.isMethodSignature(decl) && ts.isInterfaceDeclaration(decl.parent)) {
546
+ const ifaceId = this.nodeIdOfDecl(decl.parent);
547
+ const impls = ifaceId ? this.implementers.get(ifaceId) ?? [] : [];
548
+ push(dst, "exact");
549
+ const methodName = decl.name.getText();
550
+ for (const implClassId of impls) {
551
+ push(`${implClassId}.${methodName}`, "conservative");
552
+ }
553
+ if (impls.length === 0)
554
+ return;
555
+ return;
556
+ }
557
+ push(dst, "exact");
558
+ }
559
+ reentryCache = new Map;
560
+ externalReentry(specifier, fromFile) {
561
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("node:"))
562
+ return [];
563
+ const pkgName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
564
+ if (this.workspacePkgs.has(pkgName))
565
+ return [];
566
+ const hit = this.reentryCache.get(pkgName);
567
+ if (hit)
568
+ return hit;
569
+ const out = [];
570
+ const r = ts.resolveModuleName(`${pkgName}/package.json`, fromFile, { ...this.program.getCompilerOptions(), resolveJsonModule: true, moduleResolution: ts.ModuleResolutionKind.Bundler }, ts.sys);
571
+ const pjPath = r.resolvedModule?.resolvedFileName;
572
+ if (pjPath && pjPath.includes("node_modules")) {
573
+ try {
574
+ const pj = JSON.parse(fs.readFileSync(pjPath, "utf8"));
575
+ for (const dep of new Set([...Object.keys(pj.dependencies ?? {}), ...Object.keys(pj.peerDependencies ?? {})])) {
576
+ const dir = this.workspacePkgs.get(dep);
577
+ if (!dir)
578
+ continue;
579
+ const entry = [path.join(dir, "src", "index.ts"), path.join(dir, "index.ts"), path.join(dir, "src", "index.tsx")].find((f) => fs.existsSync(f));
580
+ if (entry)
581
+ out.push(entry);
582
+ }
583
+ } catch {}
584
+ }
585
+ this.reentryCache.set(pkgName, out);
586
+ return out;
587
+ }
588
+ resolveModule(specifier, fromFile) {
589
+ const r = ts.resolveModuleName(specifier, fromFile, this.program.getCompilerOptions(), ts.sys);
590
+ let resolved = r.resolvedModule?.resolvedFileName;
591
+ if (!resolved) {
592
+ const pkgName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
593
+ const pkgDir = this.workspacePkgs.get(pkgName);
594
+ if (!pkgDir)
595
+ return;
596
+ const sub = specifier.slice(pkgName.length).replace(/^\//, "");
597
+ for (const cand of [
598
+ path.join(pkgDir, sub || "index.ts"),
599
+ path.join(pkgDir, sub, "index.ts"),
600
+ path.join(pkgDir, "src", sub || "index.ts"),
601
+ path.join(pkgDir, "src", sub, "index.ts"),
602
+ path.join(pkgDir, sub + ".ts")
603
+ ]) {
604
+ if (fs.existsSync(cand))
605
+ return cand;
606
+ }
607
+ return;
608
+ }
609
+ if (resolved.includes("node_modules")) {
610
+ try {
611
+ resolved = fs.realpathSync(resolved);
612
+ } catch {
613
+ return;
614
+ }
615
+ if (resolved.includes("node_modules"))
616
+ return;
617
+ }
618
+ return path.relative(this.rootDir, resolved).startsWith("..") ? undefined : resolved;
619
+ }
620
+ nodeIdOfDecl(decl) {
621
+ const sf = decl.getSourceFile();
622
+ const relPath = this.rel(sf.fileName);
623
+ const name = this.declName(decl);
624
+ if (!name)
625
+ return;
626
+ if ((ts.isMethodDeclaration(decl) || ts.isMethodSignature(decl)) && decl.parent && (ts.isClassDeclaration(decl.parent) || ts.isInterfaceDeclaration(decl.parent))) {
627
+ const parentName = this.declName(decl.parent);
628
+ return `${relPath}#${parentName}.${name}`;
629
+ }
630
+ return `${relPath}#${name}`;
631
+ }
632
+ declName(decl) {
633
+ if ((ts.isFunctionDeclaration(decl) || ts.isClassDeclaration(decl) || ts.isInterfaceDeclaration(decl) || ts.isMethodDeclaration(decl) || ts.isMethodSignature(decl) || ts.isVariableDeclaration(decl) || ts.isTypeAliasDeclaration(decl) || ts.isEnumDeclaration(decl)) && decl.name && (ts.isIdentifier(decl.name) || ts.isStringLiteral(decl.name)))
634
+ return decl.name.text;
635
+ return;
636
+ }
637
+ isExported(node) {
638
+ return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
639
+ }
640
+ }
641
+ var TEST_FILE_RE;
642
+ var init_extract = __esm(() => {
643
+ TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$|__tests__\//;
644
+ });
645
+
646
+ // src/proc.ts
647
+ import { spawnSync as nodeSpawnSync } from "node:child_process";
648
+ function spawnSync(cmd, opts = {}) {
649
+ const p = nodeSpawnSync(cmd[0], cmd.slice(1), {
650
+ cwd: opts.cwd,
651
+ input: opts.input,
652
+ maxBuffer: opts.maxBuffer ?? 64 * 1024 * 1024,
653
+ encoding: "utf8"
654
+ });
655
+ if (p.error)
656
+ return { exitCode: 127, stdout: p.stdout ?? "", stderr: p.error.message };
657
+ return { exitCode: p.status ?? 1, stdout: p.stdout ?? "", stderr: p.stderr ?? "" };
658
+ }
659
+ function selfCommand(cmd, ...args) {
660
+ return [process.execPath, process.argv[1], cmd, ...args];
661
+ }
662
+ var init_proc = () => {};
663
+
664
+ // src/cli.ts
665
+ var exports_cli = {};
666
+ import path2 from "node:path";
667
+ import fs2 from "node:fs";
668
+ import { createHash } from "node:crypto";
669
+ function discoverTsconfigs(repoRoot) {
670
+ const found = [];
671
+ const rootConfig = path2.join(repoRoot, "tsconfig.json");
672
+ const SKIP = { node_modules: true, ".git": true, dist: true, build: true, coverage: true };
673
+ const walk = (dir, depth) => {
674
+ if (depth > 3)
675
+ return;
676
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
677
+ const p = path2.join(dir, entry.name);
678
+ if (entry.isDirectory() && !SKIP[entry.name] && !entry.name.startsWith("."))
679
+ walk(p, depth + 1);
680
+ else if (entry.name === "tsconfig.json" && p !== rootConfig)
681
+ found.push(p);
682
+ }
683
+ };
684
+ walk(repoRoot, 1);
685
+ if (found.length === 0 && fs2.existsSync(rootConfig))
686
+ found.push(rootConfig);
687
+ return found;
688
+ }
689
+ function indexProgram(extractor) {
690
+ extractor.collectImplementers();
691
+ for (const sf of extractor.sourceFiles()) {
692
+ const relPath = extractor.rel(sf.fileName);
693
+ if (relPath.startsWith("..") || seenFiles.has(relPath))
694
+ continue;
695
+ seenFiles.add(relPath);
696
+ const hash = createHash("sha1").update(sf.text).digest("hex");
697
+ const existing = getHash.get(relPath);
698
+ if (existing?.hash === hash) {
699
+ skipped++;
700
+ continue;
701
+ }
702
+ try {
703
+ const { nodes, edges, blindSpots, bindings } = extractor.extractFile(sf);
704
+ writeBatch(relPath, hash, nodes, edges, blindSpots, bindings);
705
+ indexed++;
706
+ nodeCount += nodes.length;
707
+ edgeCount += edges.length;
708
+ blindCount += blindSpots.length;
709
+ } catch (err) {
710
+ failures++;
711
+ seenFiles.delete(relPath);
712
+ console.error(`EXTRACT FAILED ${relPath}: ${err instanceof Error ? err.message : err}`);
713
+ }
714
+ }
715
+ }
716
+ function sweep(dir) {
717
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
718
+ if (entry.isDirectory()) {
719
+ if (!SKIP_DIRS[entry.name])
720
+ sweep(path2.join(dir, entry.name));
721
+ } else if (/\.[cm]?tsx?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
722
+ const abs = path2.join(dir, entry.name);
723
+ const rel = path2.relative(repoRoot, abs);
724
+ if (!seenFiles.has(rel))
725
+ orphans.push(abs);
726
+ }
727
+ }
728
+ }
729
+ var args, target, dbFlag, dbPath, repoRoot, tsconfigs, t0, db, insertNode, insertEdge, insertBlind, insertBinding, upsertFile, getHash, writeBatch, indexed = 0, skipped = 0, nodeCount = 0, edgeCount = 0, blindCount = 0, seenFiles, failures = 0, orphans, SKIP_DIRS, pyProbe, dt;
730
+ var init_cli = __esm(() => {
731
+ init_schema();
732
+ init_extract();
733
+ init_db();
734
+ init_proc();
735
+ args = process.argv.slice(2);
736
+ if (args.length === 0) {
737
+ console.error("usage: codeblast index <repo-root-or-tsconfig> [--db graph.db]");
738
+ process.exit(1);
739
+ }
740
+ target = path2.resolve(args[0]);
741
+ dbFlag = args.indexOf("--db");
742
+ dbPath = dbFlag >= 0 ? args[dbFlag + 1] : path2.join(process.cwd(), "graph.db");
743
+ if (target.endsWith(".json")) {
744
+ repoRoot = path2.dirname(target);
745
+ tsconfigs = [target];
746
+ } else {
747
+ repoRoot = target;
748
+ tsconfigs = discoverTsconfigs(target);
749
+ }
750
+ if (tsconfigs.length === 0) {
751
+ console.error(`no tsconfig.json found under: ${target}`);
752
+ console.error("proceeding: python-only ingestion");
753
+ }
754
+ console.error(`repo root: ${repoRoot}`);
755
+ console.error(`tsconfigs: ${tsconfigs.length}`);
756
+ t0 = performance.now();
757
+ db = openGraph(dbPath);
758
+ insertNode = db.prepare("INSERT OR REPLACE INTO nodes (id, kind, name, file, line, end_line, exported, signature, src_file) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
759
+ insertEdge = db.prepare("INSERT OR REPLACE INTO edges (src, dst, kind, file, line, confidence, src_file) VALUES (?, ?, ?, ?, ?, ?, ?)");
760
+ insertBlind = db.prepare("INSERT OR REPLACE INTO blind_spots (file, line, reason, src_file) VALUES (?, ?, ?, ?)");
761
+ insertBinding = db.prepare("INSERT OR REPLACE INTO import_bindings (importer, imported, names, star, src_file) VALUES (?, ?, ?, ?, ?)");
762
+ upsertFile = db.prepare("INSERT OR REPLACE INTO files (path, hash) VALUES (?, ?)");
763
+ getHash = db.prepare("SELECT hash FROM files WHERE path = ?");
764
+ writeBatch = transaction(db, (relPath, hash, nodes, edges, blind, bindings = []) => {
765
+ invalidateFile(db, relPath);
766
+ for (const n of nodes)
767
+ insertNode.run(n.id, n.kind, n.name, n.file, n.line, n.end_line, n.exported, n.signature ?? "", n.src_file);
768
+ for (const e of edges)
769
+ insertEdge.run(e.src, e.dst, e.kind, e.file, e.line, e.confidence, e.src_file);
770
+ for (const b of blind)
771
+ insertBlind.run(b.file, b.line, b.reason, b.src_file);
772
+ for (const ib of bindings)
773
+ insertBinding.run(ib.importer, ib.imported, ib.names, ib.star, ib.src_file);
774
+ upsertFile.run(relPath, hash);
775
+ });
776
+ seenFiles = new Set;
777
+ for (const tsconfigPath of tsconfigs) {
778
+ let extractor;
779
+ try {
780
+ extractor = new Extractor(tsconfigPath, repoRoot);
781
+ } catch (err) {
782
+ console.error(`skip ${path2.relative(repoRoot, tsconfigPath)}: ${err instanceof Error ? err.message.split(`
783
+ `)[0] : err}`);
784
+ continue;
785
+ }
786
+ const names = extractor.fileNames();
787
+ let anyChanged = names.length === 0;
788
+ for (const abs of names) {
789
+ const rel = path2.relative(repoRoot, abs);
790
+ if (rel.startsWith("..") || seenFiles.has(rel))
791
+ continue;
792
+ let text;
793
+ try {
794
+ text = fs2.readFileSync(abs, "utf8");
795
+ } catch {
796
+ anyChanged = true;
797
+ break;
798
+ }
799
+ const hash = createHash("sha1").update(text).digest("hex");
800
+ const existing = getHash.get(rel);
801
+ if (existing?.hash !== hash) {
802
+ anyChanged = true;
803
+ break;
804
+ }
805
+ }
806
+ if (!anyChanged) {
807
+ for (const abs of names) {
808
+ const rel = path2.relative(repoRoot, abs);
809
+ if (!rel.startsWith("..") && !seenFiles.has(rel)) {
810
+ seenFiles.add(rel);
811
+ skipped++;
812
+ }
813
+ }
814
+ continue;
815
+ }
816
+ indexProgram(extractor);
817
+ }
818
+ orphans = [];
819
+ SKIP_DIRS = { node_modules: true, ".git": true, dist: true, build: true, coverage: true, ".next": true };
820
+ sweep(repoRoot);
821
+ if (tsconfigs.length > 0 && orphans.length > 0) {
822
+ console.error(`orphan files (outside all tsconfigs): ${orphans.length}`);
823
+ let orphanChanged = false;
824
+ for (const abs of orphans) {
825
+ const rel = path2.relative(repoRoot, abs);
826
+ let text;
827
+ try {
828
+ text = fs2.readFileSync(abs, "utf8");
829
+ } catch {
830
+ orphanChanged = true;
831
+ break;
832
+ }
833
+ const hash = createHash("sha1").update(text).digest("hex");
834
+ const existing = getHash.get(rel);
835
+ if (existing?.hash !== hash) {
836
+ orphanChanged = true;
837
+ break;
838
+ }
839
+ }
840
+ if (orphanChanged) {
841
+ try {
842
+ indexProgram(Extractor.forFiles(orphans, tsconfigs[0], repoRoot));
843
+ } catch (err) {
844
+ failures++;
845
+ console.error(`ORPHAN PROGRAM FAILED (${orphans.length} files unindexed): ${err instanceof Error ? err.message : err}`);
846
+ }
847
+ } else {
848
+ for (const abs of orphans) {
849
+ seenFiles.add(path2.relative(repoRoot, abs));
850
+ skipped++;
851
+ }
852
+ }
853
+ }
854
+ pyProbe = spawnSync(["python3", path2.join(import.meta.dirname, "py_extract.py"), repoRoot]);
855
+ if (pyProbe.exitCode === 0) {
856
+ const payload = JSON.parse(pyProbe.stdout);
857
+ for (const f of payload.files) {
858
+ if (seenFiles.has(f.path))
859
+ continue;
860
+ seenFiles.add(f.path);
861
+ const existing = getHash.get(f.path);
862
+ if (existing?.hash === f.hash && f.hash !== "") {
863
+ skipped++;
864
+ continue;
865
+ }
866
+ writeBatch(f.path, f.hash, f.nodes, f.edges, f.blind_spots);
867
+ indexed++;
868
+ nodeCount += f.nodes.length;
869
+ edgeCount += f.edges.length;
870
+ blindCount += f.blind_spots.length;
871
+ }
872
+ const pyFiles = payload.files.length;
873
+ if (pyFiles > 0)
874
+ console.error(`python files ingested: ${pyFiles}`);
875
+ }
876
+ dt = ((performance.now() - t0) / 1000).toFixed(1);
877
+ console.log(JSON.stringify({
878
+ db: dbPath,
879
+ seconds: Number(dt),
880
+ tsconfigs: tsconfigs.length,
881
+ files_indexed: indexed,
882
+ files_skipped: skipped,
883
+ nodes: nodeCount,
884
+ edges: edgeCount,
885
+ blind_spots: blindCount,
886
+ failures
887
+ }, null, 2));
888
+ if (failures > 0) {
889
+ console.error(`
890
+ ${failures} extraction failure(s) — graph is INCOMPLETE. Exiting non-zero.`);
891
+ process.exit(2);
892
+ }
893
+ });
894
+
895
+ // src/impact.ts
896
+ function impact(db, targetId, maxNodes = 500) {
897
+ const targetRow = db.prepare("SELECT id, file FROM nodes WHERE id = ?").get(targetId);
898
+ if (!targetRow)
899
+ throw new Error(`node not found: ${targetId}`);
900
+ const targetName = targetId.includes("#") ? targetId.split("#").pop().split(".")[0] : null;
901
+ const bindingRows = (() => {
902
+ try {
903
+ return db.prepare("SELECT importer, imported, names, star FROM import_bindings").all();
904
+ } catch {
905
+ return [];
906
+ }
907
+ })();
908
+ const bindings = new Map;
909
+ for (const b of bindingRows) {
910
+ bindings.set(`${b.importer}\x00${b.imported}`, { star: b.star === 1, names: new Set(b.names ? b.names.split(",") : []) });
911
+ }
912
+ const incoming = db.prepare(`SELECT src, dst, kind, confidence, file, line FROM edges
913
+ WHERE kind IN (${IMPACT_EDGE_KINDS.map(() => "?").join(",")})`).all(...IMPACT_EDGE_KINDS);
914
+ const byDst = new Map;
915
+ for (const e of incoming) {
916
+ const list = byDst.get(e.dst) ?? [];
917
+ list.push(e);
918
+ byDst.set(e.dst, list);
919
+ }
920
+ const visited = new Map;
921
+ let frontier = [targetId];
922
+ visited.set(targetId, { hops: 0, confidence: "exact", via_file: targetRow.file, via_line: 0, fileLevel: false, namedMiss: false });
923
+ let truncated = false;
924
+ while (frontier.length > 0 && !truncated) {
925
+ const next = [];
926
+ for (const cur of frontier) {
927
+ const curInfo = visited.get(cur);
928
+ for (const e of byDst.get(cur) ?? []) {
929
+ if (visited.has(e.src))
930
+ continue;
931
+ let namedMiss = false;
932
+ if (e.kind === "imports" && targetName) {
933
+ const b = bindings.get(`${e.src}\x00${e.dst}`);
934
+ if (b && !b.star && !b.names.has(targetName))
935
+ namedMiss = true;
936
+ }
937
+ const conf = e.confidence === "conservative" || curInfo.confidence === "conservative" ? "conservative" : "exact";
938
+ const fileLevel = curInfo.fileLevel || e.kind === "imports" || e.kind === "contains";
939
+ const nm = curInfo.namedMiss || namedMiss;
940
+ visited.set(e.src, { hops: curInfo.hops + 1, confidence: conf, via_file: e.file, via_line: e.line, fileLevel, namedMiss: nm });
941
+ next.push(e.src);
942
+ if (visited.size > maxNodes) {
943
+ truncated = true;
944
+ break;
945
+ }
946
+ }
947
+ if (truncated)
948
+ break;
949
+ }
950
+ frontier = next;
951
+ }
952
+ visited.delete(targetId);
953
+ const getNode = db.prepare("SELECT id, name, kind, file, line FROM nodes WHERE id = ?");
954
+ const testEdges = db.prepare("SELECT DISTINCT src FROM edges WHERE kind = 'tests' AND dst = ?");
955
+ const items = [];
956
+ const affectedTests = new Set;
957
+ for (const [id, info] of visited) {
958
+ const n = getNode.get(id);
959
+ if (!n)
960
+ continue;
961
+ if (n.kind === "file" && n.file === targetRow.file)
962
+ continue;
963
+ const level = n.kind === "test" || TEST_FILE_RE2.test(n.file) ? "tests" : info.hops === 1 ? "direct" : "indirect";
964
+ if (level === "tests")
965
+ affectedTests.add(id);
966
+ items.push({
967
+ id: n.id,
968
+ name: n.name,
969
+ kind: n.kind,
970
+ file: n.file,
971
+ line: n.line,
972
+ level,
973
+ hops: info.hops,
974
+ confidence: info.confidence,
975
+ via_file: info.via_file,
976
+ via_line: info.via_line,
977
+ channel: info.fileLevel ? "file" : "call",
978
+ named_miss: info.namedMiss
979
+ });
980
+ }
981
+ for (const it of items) {
982
+ if (it.level === "tests")
983
+ continue;
984
+ for (const t of testEdges.all(it.id)) {
985
+ if (visited.has(t.src) || affectedTests.has(t.src))
986
+ continue;
987
+ const n = getNode.get(t.src);
988
+ if (!n)
989
+ continue;
990
+ affectedTests.add(t.src);
991
+ items.push({
992
+ id: n.id,
993
+ name: n.name,
994
+ kind: n.kind,
995
+ file: n.file,
996
+ line: n.line,
997
+ level: "tests",
998
+ hops: it.hops + 1,
999
+ confidence: it.confidence,
1000
+ via_file: n.file,
1001
+ via_line: n.line,
1002
+ channel: it.channel
1003
+ });
1004
+ }
1005
+ }
1006
+ for (const t of testEdges.all(targetId)) {
1007
+ if (affectedTests.has(t.src) || visited.has(t.src))
1008
+ continue;
1009
+ const n = getNode.get(t.src);
1010
+ if (!n)
1011
+ continue;
1012
+ items.push({
1013
+ id: n.id,
1014
+ name: n.name,
1015
+ kind: n.kind,
1016
+ file: n.file,
1017
+ line: n.line,
1018
+ level: "tests",
1019
+ hops: 1,
1020
+ confidence: "exact",
1021
+ via_file: n.file,
1022
+ via_line: n.line,
1023
+ channel: "call"
1024
+ });
1025
+ }
1026
+ const blindReachTests = db.prepare(`SELECT DISTINCT file FROM blind_spots
1027
+ WHERE (reason LIKE 'subprocess spawn%' OR reason LIKE 'dynamic import%')`).all();
1028
+ const includedFiles = new Set(items.filter((it) => it.level === "tests").map((it) => it.file));
1029
+ for (const { file } of blindReachTests) {
1030
+ if (!TEST_FILE_RE2.test(file) || includedFiles.has(file))
1031
+ continue;
1032
+ const bs = db.prepare("SELECT line, reason FROM blind_spots WHERE file = ? AND (reason LIKE 'subprocess spawn%' OR reason LIKE 'dynamic import%') LIMIT 1").get(file);
1033
+ includedFiles.add(file);
1034
+ items.push({
1035
+ id: file,
1036
+ name: `${file} (${bs.reason})`,
1037
+ kind: "file",
1038
+ file,
1039
+ line: bs.line,
1040
+ level: "tests",
1041
+ hops: 99,
1042
+ confidence: "conservative",
1043
+ via_file: file,
1044
+ via_line: bs.line,
1045
+ channel: "file"
1046
+ });
1047
+ }
1048
+ const blindCount = db.prepare("SELECT COUNT(*) c FROM blind_spots WHERE file = ? AND reason NOT LIKE 'test-global%'").get(targetRow.file).c;
1049
+ const coChange = db.prepare("SELECT dst, line, src_file FROM edges WHERE kind = 'co_change' AND src = ? ORDER BY line DESC LIMIT 10").all(targetRow.file).map((r) => ({ file: r.dst, co_commits: r.line, evidence: r.src_file }));
1050
+ items.sort((a, b) => a.hops - b.hops || a.id.localeCompare(b.id));
1051
+ return { target: targetId, items, truncated, blind_spot_count: blindCount, co_change_hints: coChange };
1052
+ }
1053
+ var IMPACT_EDGE_KINDS, TEST_FILE_RE2;
1054
+ var init_impact = __esm(() => {
1055
+ IMPACT_EDGE_KINDS = ["calls", "implements", "extends", "imports", "contains"];
1056
+ TEST_FILE_RE2 = /\.(test|spec)\.[cm]?[jt]sx?$|__tests__\/|(^|\/)tests?\/|(^|\/)test_[^/]*\.py$|_test\.py$|conftest\.py$/;
1057
+ });
1058
+
1059
+ // src/impact-cli.ts
1060
+ var exports_impact_cli = {};
1061
+ var dbPath2, query, maxFlag, maxNodes, db2, targetId, exact, t02, result, ms, byLevel, callItems, fileNamed, fileUnnamed, ordered;
1062
+ var init_impact_cli = __esm(() => {
1063
+ init_db();
1064
+ init_impact();
1065
+ [dbPath2, query] = process.argv.slice(2);
1066
+ if (!dbPath2 || !query) {
1067
+ console.error("usage: codeblast impact <graph.db> <node-id-or-name> [--max 500]");
1068
+ process.exit(1);
1069
+ }
1070
+ maxFlag = process.argv.indexOf("--max");
1071
+ maxNodes = maxFlag >= 0 ? Number(process.argv[maxFlag + 1]) : 500;
1072
+ db2 = openDatabase(dbPath2, { readonly: true });
1073
+ targetId = query;
1074
+ exact = db2.prepare("SELECT id FROM nodes WHERE id = ?").get(query);
1075
+ if (!exact) {
1076
+ const candidates = db2.prepare("SELECT id, kind, file, line FROM nodes WHERE name = ? AND kind != 'file' LIMIT 20").all(query);
1077
+ if (candidates.length === 0) {
1078
+ console.error(`no node matches: ${query}`);
1079
+ process.exit(1);
1080
+ }
1081
+ if (candidates.length > 1) {
1082
+ console.error(`ambiguous name, ${candidates.length} candidates:`);
1083
+ for (const c of candidates)
1084
+ console.error(` ${c.id} (${c.kind} @ ${c.file}:${c.line})`);
1085
+ process.exit(1);
1086
+ }
1087
+ targetId = candidates[0].id;
1088
+ }
1089
+ t02 = performance.now();
1090
+ result = impact(db2, targetId, maxNodes);
1091
+ ms = (performance.now() - t02).toFixed(0);
1092
+ if (process.argv.includes("--json")) {
1093
+ console.log(JSON.stringify(result));
1094
+ process.exit(0);
1095
+ }
1096
+ byLevel = { direct: 0, indirect: 0, tests: 0 };
1097
+ for (const it of result.items)
1098
+ byLevel[it.level]++;
1099
+ callItems = result.items.filter((it) => it.channel === "call");
1100
+ fileNamed = result.items.filter((it) => it.channel === "file" && !it.named_miss);
1101
+ fileUnnamed = result.items.filter((it) => it.channel === "file" && it.named_miss);
1102
+ console.log(`target: ${result.target}`);
1103
+ console.log(`impact: ${result.items.length} nodes (direct=${byLevel.direct} indirect=${byLevel.indirect} tests=${byLevel.tests})${result.truncated ? " [TRUNCATED — wide impact, run the full suite]" : ""}`);
1104
+ console.log(` ├─ call-graph reachable (high confidence): ${callItems.length}`);
1105
+ console.log(` ├─ reachable via named import: ${fileNamed.length}`);
1106
+ console.log(` └─ reachable via unnamed import (execution closure, conservative — do not skip): ${fileUnnamed.length}`);
1107
+ if (result.blind_spot_count > 0)
1108
+ console.log(`blind spots in target file: ${result.blind_spot_count} (impact may be underestimated)`);
1109
+ if (result.co_change_hints.length > 0) {
1110
+ console.log(`co-change hints (no static edge, but historically changed together):`);
1111
+ for (const h of result.co_change_hints)
1112
+ console.log(` ~ ${h.file} (${h.co_commits} co-commits, ${h.evidence})`);
1113
+ }
1114
+ console.log(`query: ${ms}ms
1115
+ `);
1116
+ ordered = [...callItems, ...fileNamed, ...fileUnnamed];
1117
+ for (const it of ordered.slice(0, 40)) {
1118
+ const conf = it.confidence === "conservative" ? " ~" : "";
1119
+ const ch = it.channel === "file" ? it.named_miss ? " ·closure" : " ·import" : "";
1120
+ console.log(` [${it.level}${conf}${ch}] ${it.id} (${it.kind}, ${it.hops} hop, via ${it.via_file}:${it.via_line})`);
1121
+ }
1122
+ if (ordered.length > 40)
1123
+ console.log(` ... and ${ordered.length - 40} more`);
1124
+ });
1125
+
1126
+ // src/graph-diff.ts
1127
+ function graphDiff(dbA, dbB) {
1128
+ const q = "SELECT id, kind, name, file, line, exported, COALESCE(signature,'') signature FROM nodes WHERE kind NOT IN ('file')";
1129
+ const nodesA = new Map(dbA.prepare(q).all().map((n) => [n.id, n]));
1130
+ const nodesB = new Map(dbB.prepare(q).all().map((n) => [n.id, n]));
1131
+ const rawAdded = [];
1132
+ const rawRemoved = [];
1133
+ for (const [id, n] of nodesB)
1134
+ if (!nodesA.has(id))
1135
+ rawAdded.push(n);
1136
+ for (const [id, n] of nodesA)
1137
+ if (!nodesB.has(id))
1138
+ rawRemoved.push(n);
1139
+ const visibilityChanged = [];
1140
+ for (const [id, b] of nodesB) {
1141
+ const a = nodesA.get(id);
1142
+ if (a && a.exported !== b.exported) {
1143
+ visibilityChanged.push({ id, name: b.name, kind: b.kind, file: b.file, line: b.line, nowExported: b.exported === 1 });
1144
+ }
1145
+ }
1146
+ const signatureChanged = [];
1147
+ for (const [id, b] of nodesB) {
1148
+ const a = nodesA.get(id);
1149
+ if (a && b.exported === 1 && a.signature !== b.signature && (a.signature || b.signature)) {
1150
+ signatureChanged.push({ id, name: b.name, kind: b.kind, file: b.file, line: b.line, from: a.signature, to: b.signature });
1151
+ }
1152
+ }
1153
+ const renamed = [];
1154
+ const usedAdded = new Set;
1155
+ const usedRemoved = new Set;
1156
+ for (const r of rawRemoved) {
1157
+ const candidate = rawAdded.find((a) => !usedAdded.has(a.id) && a.file === r.file && a.kind === r.kind && Math.abs(a.line - r.line) <= 30);
1158
+ if (candidate) {
1159
+ renamed.push({ from: r.name, to: candidate.name, file: r.file, kind: r.kind });
1160
+ usedAdded.add(candidate.id);
1161
+ usedRemoved.add(r.id);
1162
+ }
1163
+ }
1164
+ for (const r of rawRemoved) {
1165
+ if (usedRemoved.has(r.id))
1166
+ continue;
1167
+ const candidate = rawAdded.find((a) => !usedAdded.has(a.id) && a.name === r.name && a.kind === r.kind && Math.abs(a.line - r.line) <= 5);
1168
+ if (candidate) {
1169
+ renamed.push({ from: `${r.file}#${r.name}`, to: `${candidate.file}#${candidate.name}`, file: candidate.file, kind: r.kind });
1170
+ usedAdded.add(candidate.id);
1171
+ usedRemoved.add(r.id);
1172
+ }
1173
+ }
1174
+ const eq = `SELECT src, dst, kind, file, line FROM edges WHERE kind IN ${STRUCTURAL_EDGE_KINDS}`;
1175
+ const edgeKey = (e) => `${e.src}\x00${e.dst}\x00${e.kind}`;
1176
+ const edgesA = new Map(dbA.prepare(eq).all().map((e) => [edgeKey(e), e]));
1177
+ const edgesB = new Map(dbB.prepare(eq).all().map((e) => [edgeKey(e), e]));
1178
+ const edgesAdded = [];
1179
+ const edgesRemoved = [];
1180
+ for (const [k, e] of edgesB)
1181
+ if (!edgesA.has(k))
1182
+ edgesAdded.push(e);
1183
+ for (const [k, e] of edgesA)
1184
+ if (!edgesB.has(k))
1185
+ edgesRemoved.push(e);
1186
+ return {
1187
+ nodesAdded: rawAdded.filter((n) => !usedAdded.has(n.id)),
1188
+ nodesRemoved: rawRemoved.filter((n) => !usedRemoved.has(n.id)),
1189
+ renamed,
1190
+ edgesAdded,
1191
+ edgesRemoved,
1192
+ visibilityChanged,
1193
+ signatureChanged
1194
+ };
1195
+ }
1196
+ function foldToModules(diff) {
1197
+ const TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$|__tests__\/|(^|\/)tests?\/|(^|\/)test_[^/]*\.py$|_test\.py$|conftest\.py$/;
1198
+ const moduleOf = (file) => {
1199
+ if (TEST_RE.test(file))
1200
+ return "tests";
1201
+ const wsMatch = file.match(/^(?:packages|apps|libs)\/([^/]+)\//);
1202
+ if (wsMatch)
1203
+ return wsMatch[1];
1204
+ const ix = file.indexOf("/");
1205
+ return ix < 0 ? "(root)" : file.slice(0, ix);
1206
+ };
1207
+ const out = new Map;
1208
+ const bump = (m, key) => {
1209
+ const v = out.get(m) ?? { added: 0, removed: 0, renamed: 0, edgesIn: 0, edgesOut: 0 };
1210
+ v[key]++;
1211
+ out.set(m, v);
1212
+ };
1213
+ for (const n of diff.nodesAdded)
1214
+ bump(moduleOf(n.file), "added");
1215
+ for (const n of diff.nodesRemoved)
1216
+ bump(moduleOf(n.file), "removed");
1217
+ for (const r of diff.renamed)
1218
+ bump(moduleOf(r.file), "renamed");
1219
+ for (const e of diff.edgesAdded)
1220
+ bump(moduleOf(e.file), "edgesIn");
1221
+ for (const e of diff.edgesRemoved)
1222
+ bump(moduleOf(e.file), "edgesOut");
1223
+ return out;
1224
+ }
1225
+ var STRUCTURAL_EDGE_KINDS = "('calls','imports','implements','extends')";
1226
+
1227
+ // src/change-cli.ts
1228
+ var exports_change_cli = {};
1229
+ import fs3 from "node:fs";
1230
+ async function buildGraphAt(repo, ref, db) {
1231
+ const wt = `/tmp/codeblast-wt-${ref.replace(/[^\w]/g, "_")}`;
1232
+ const sh = (cmd) => {
1233
+ const p = spawnSync(cmd, { cwd: repo });
1234
+ if (p.exitCode !== 0)
1235
+ throw new Error(`${cmd.join(" ")}: ${p.stderr.slice(0, 300)}`);
1236
+ };
1237
+ spawnSync(["git", "worktree", "remove", "--force", wt], { cwd: repo });
1238
+ sh(["git", "worktree", "add", "--detach", wt, ref]);
1239
+ try {
1240
+ const p = spawnSync(selfCommand("index", wt, "--db", db));
1241
+ if (p.exitCode !== 0)
1242
+ throw new Error(p.stderr.slice(0, 500));
1243
+ } finally {
1244
+ spawnSync(["git", "worktree", "remove", "--force", wt], { cwd: repo });
1245
+ }
1246
+ }
1247
+ var args2, outFlag, outPath, jsonMode, dbA, dbB, header, diff, total, impactDetails, impactSummary, topChanged, folded, lines, doc;
1248
+ var init_change_cli = __esm(async () => {
1249
+ init_db();
1250
+ init_proc();
1251
+ init_impact();
1252
+ args2 = process.argv.slice(2);
1253
+ outFlag = args2.indexOf("--out");
1254
+ outPath = outFlag >= 0 ? args2[outFlag + 1] : undefined;
1255
+ jsonMode = args2.includes("--json");
1256
+ if (args2[0] === "--dbs") {
1257
+ dbA = openDatabase(args2[1], { readonly: true });
1258
+ dbB = openDatabase(args2[2], { readonly: true });
1259
+ header = `${args2[1]} → ${args2[2]}`;
1260
+ } else {
1261
+ const [repo, refA, refB] = args2;
1262
+ if (!repo || !refA || !refB) {
1263
+ console.error("usage: codeblast change <repo> <ref-a> <ref-b> [--out report.md] | --dbs <a.db> <b.db>");
1264
+ process.exit(1);
1265
+ }
1266
+ const pa = `/tmp/codeblast-diff-a.db`, pb = `/tmp/codeblast-diff-b.db`;
1267
+ for (const f of [pa, pb])
1268
+ for (const s of ["", "-wal", "-shm"])
1269
+ fs3.rmSync(f + s, { force: true });
1270
+ console.error(`building graph @ ${refA} ...`);
1271
+ await buildGraphAt(repo, refA, pa);
1272
+ console.error(`building graph @ ${refB} ...`);
1273
+ await buildGraphAt(repo, refB, pb);
1274
+ dbA = openDatabase(pa, { readonly: true });
1275
+ dbB = openDatabase(pb, { readonly: true });
1276
+ header = `${refA} → ${refB}`;
1277
+ }
1278
+ diff = graphDiff(dbA, dbB);
1279
+ total = diff.nodesAdded.length + diff.nodesRemoved.length + diff.renamed.length + diff.edgesAdded.length + diff.edgesRemoved.length;
1280
+ if (total === 0) {
1281
+ if (jsonMode) {
1282
+ console.log(JSON.stringify({ range: header, structural_changes: 0 }));
1283
+ process.exit(0);
1284
+ }
1285
+ console.log("无结构变化。");
1286
+ process.exit(0);
1287
+ }
1288
+ impactDetails = [];
1289
+ impactSummary = [];
1290
+ topChanged = [...diff.nodesAdded, ...diff.renamed.map((r) => ({ id: `${r.file}#${r.to}`, kind: r.kind, name: r.to, file: r.file, line: 0 }))].slice(0, 10);
1291
+ for (const n of topChanged) {
1292
+ try {
1293
+ const r = impact(dbB, n.id, 2000);
1294
+ const tests = r.items.filter((i) => i.level === "tests").length;
1295
+ impactSummary.push(`| ${n.name} | ${n.kind} | ${r.items.length}${r.truncated ? "+" : ""} | ${tests} |`);
1296
+ impactDetails.push({ symbol: n.name, kind: n.kind, impact_nodes: r.items.length, affected_tests: tests, truncated: r.truncated });
1297
+ } catch {}
1298
+ }
1299
+ folded = foldToModules(diff);
1300
+ if (jsonMode) {
1301
+ console.log(JSON.stringify({
1302
+ range: header,
1303
+ structural_changes: total,
1304
+ modules: Object.fromEntries(folded),
1305
+ nodes_added: diff.nodesAdded,
1306
+ nodes_removed: diff.nodesRemoved,
1307
+ renamed: diff.renamed,
1308
+ edges_added: diff.edgesAdded,
1309
+ edges_removed: diff.edgesRemoved,
1310
+ impact: impactDetails
1311
+ }));
1312
+ process.exit(0);
1313
+ }
1314
+ lines = [
1315
+ `# Change Map`,
1316
+ ``,
1317
+ `> ${header} · 结构变更 ${total} 项`,
1318
+ ``,
1319
+ `## 模块级变化`,
1320
+ ``,
1321
+ `| 模块 | +符号 | -符号 | 重命名 | +依赖 | -依赖 |`,
1322
+ `|---|---|---|---|---|---|`
1323
+ ];
1324
+ for (const [m, v] of [...folded.entries()].sort((a, b) => b[1].added + b[1].removed - (a[1].added + a[1].removed))) {
1325
+ lines.push(`| ${m} | ${v.added} | ${v.removed} | ${v.renamed} | ${v.edgesIn} | ${v.edgesOut} |`);
1326
+ }
1327
+ if (diff.renamed.length > 0) {
1328
+ lines.push(``, `## 重命名`, ``);
1329
+ for (const r of diff.renamed.slice(0, 20))
1330
+ lines.push(`- \`${r.from}\` → \`${r.to}\` (${r.kind}, ${r.file})`);
1331
+ }
1332
+ if (diff.nodesAdded.length > 0) {
1333
+ lines.push(``, `## 新增符号(前 20)`, ``);
1334
+ for (const n of diff.nodesAdded.slice(0, 20))
1335
+ lines.push(`- \`${n.name}\` (${n.kind}) ${n.file}:${n.line}`);
1336
+ }
1337
+ if (diff.nodesRemoved.length > 0) {
1338
+ lines.push(``, `## 删除符号(前 20)`, ``);
1339
+ for (const n of diff.nodesRemoved.slice(0, 20))
1340
+ lines.push(`- \`${n.name}\` (${n.kind}) ${n.file}`);
1341
+ }
1342
+ if (diff.edgesAdded.length > 0) {
1343
+ lines.push(``, `## 新增依赖(前 15)`, ``);
1344
+ for (const e of diff.edgesAdded.slice(0, 15))
1345
+ lines.push(`- ${e.kind}: \`${e.src}\` → \`${e.dst}\` (${e.file}:${e.line})`);
1346
+ }
1347
+ if (diff.edgesRemoved.length > 0) {
1348
+ lines.push(``, `## 删除依赖(前 15)`, ``);
1349
+ for (const e of diff.edgesRemoved.slice(0, 15))
1350
+ lines.push(`- ${e.kind}: \`${e.src}\` → \`${e.dst}\``);
1351
+ }
1352
+ if (impactSummary.length > 0) {
1353
+ lines.push(``, `## 变更符号的影响半径`, ``, `| 符号 | 类型 | 影响节点 | 受影响测试 |`, `|---|---|---|---|`, ...impactSummary);
1354
+ }
1355
+ doc = lines.join(`
1356
+ `) + `
1357
+ `;
1358
+ if (outPath) {
1359
+ fs3.writeFileSync(outPath, doc);
1360
+ console.error(`written: ${outPath}`);
1361
+ } else {
1362
+ console.log(doc);
1363
+ }
1364
+ });
1365
+
1366
+ // src/overlay.ts
1367
+ import fs4 from "node:fs";
1368
+ async function loadOverlay(path) {
1369
+ if (!fs4.existsSync(path))
1370
+ return { modules: {} };
1371
+ return JSON.parse(fs4.readFileSync(path, "utf8"));
1372
+ }
1373
+ function applyOverlay(rawModules, overlay) {
1374
+ const out = new Map;
1375
+ for (const m of rawModules) {
1376
+ const o = overlay.modules[m] ?? {};
1377
+ let effective = m;
1378
+ for (let i = 0;i < 5; i++) {
1379
+ const next = overlay.modules[effective]?.mergeInto;
1380
+ if (!next || next === effective)
1381
+ break;
1382
+ effective = next;
1383
+ }
1384
+ const display = overlay.modules[effective]?.name ?? effective;
1385
+ out.set(m, { display, effective, hidden: o.hidden ?? overlay.modules[effective]?.hidden ?? false });
1386
+ }
1387
+ return out;
1388
+ }
1389
+ var init_overlay = () => {};
1390
+
1391
+ // src/archmap-html.ts
1392
+ var exports_archmap_html = {};
1393
+ import fs5 from "node:fs";
1394
+ import dagre from "@dagrejs/dagre";
1395
+ async function layoutGraph(nodes, edges) {
1396
+ const g = new dagre.graphlib.Graph;
1397
+ g.setGraph({ rankdir: "TB", nodesep: 36, ranksep: 70, marginx: 24, marginy: 24 });
1398
+ g.setDefaultEdgeLabel(() => ({}));
1399
+ for (const n of nodes) {
1400
+ g.setNode(n.id, { width: Math.max(150, n.label.length * 8.5 + 56, (n.meta ?? "").length * 6.8 + 40), height: 58 });
1401
+ }
1402
+ for (const e of edges) {
1403
+ if (e.src !== e.dst)
1404
+ g.setEdge(e.src, e.dst);
1405
+ }
1406
+ dagre.layout(g);
1407
+ const gd = g.graph();
1408
+ return {
1409
+ width: gd.width ?? 900,
1410
+ height: gd.height ?? 600,
1411
+ nodes: g.nodes().map((id) => {
1412
+ const n = g.node(id);
1413
+ return { id, x: n.x - n.width / 2, y: n.y - n.height / 2, w: n.width, h: n.height };
1414
+ }),
1415
+ edges: edges.filter((e) => e.src !== e.dst).map((e) => ({
1416
+ src: e.src,
1417
+ dst: e.dst,
1418
+ points: g.edge(e.src, e.dst)?.points ?? []
1419
+ }))
1420
+ };
1421
+ }
1422
+ var CLIENT_JS, dbPath3, outFlag2, outPath2, overlayFlag, overlayPath, repoFlag, repoUrl, impactFlag, impactTarget, diffFlag, diffBase, db3, overlay, TEST_RE, moduleOf = (file) => {
1423
+ if (TEST_RE.test(file))
1424
+ return "tests";
1425
+ const ix = file.indexOf("/");
1426
+ return ix < 0 ? "(root)" : file.slice(0, ix);
1427
+ }, files, blindRows, blindMap, symRows, symsByFile, fileInfos, importRows, rawModuleNames, ovMap, effModule = (m) => ovMap.get(m)?.effective ?? m, hiddenModules, modules, modEdges, cycles, layouts, impactOverlay = null, diffOverlay = null, data, html;
1428
+ var init_archmap_html = __esm(async () => {
1429
+ init_db();
1430
+ init_overlay();
1431
+ init_impact();
1432
+ CLIENT_JS = fs5.readFileSync(new URL("./archmap-client.js", import.meta.url), "utf8");
1433
+ if (CLIENT_JS.includes("</script>"))
1434
+ throw new Error("archmap-client.js must not contain </script>");
1435
+ [dbPath3] = process.argv.slice(2);
1436
+ outFlag2 = process.argv.indexOf("--out");
1437
+ outPath2 = outFlag2 >= 0 ? process.argv[outFlag2 + 1] : "arch.html";
1438
+ overlayFlag = process.argv.indexOf("--overlay");
1439
+ overlayPath = overlayFlag >= 0 ? process.argv[overlayFlag + 1] : undefined;
1440
+ repoFlag = process.argv.indexOf("--repo-url");
1441
+ repoUrl = repoFlag >= 0 ? process.argv[repoFlag + 1] : undefined;
1442
+ impactFlag = process.argv.indexOf("--impact");
1443
+ impactTarget = impactFlag >= 0 ? process.argv[impactFlag + 1] : undefined;
1444
+ diffFlag = process.argv.indexOf("--diff");
1445
+ diffBase = diffFlag >= 0 ? process.argv[diffFlag + 1] : undefined;
1446
+ if (!dbPath3) {
1447
+ console.error("usage: codeblast archmap <graph.db> --out arch.html");
1448
+ process.exit(1);
1449
+ }
1450
+ db3 = openDatabase(dbPath3, { readonly: true });
1451
+ overlay = overlayPath ? await loadOverlay(overlayPath) : { modules: {} };
1452
+ TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$|__tests__\/|(^|\/)tests?\/|(^|\/)test_[^/]*\.py$|_test\.py$|conftest\.py$/;
1453
+ files = db3.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
1454
+ blindRows = db3.prepare(`SELECT file,
1455
+ SUM(CASE WHEN reason LIKE 'unresolved call%' THEN 0 ELSE 1 END) dyn,
1456
+ SUM(CASE WHEN reason LIKE 'unresolved call%' THEN 1 ELSE 0 END) unres
1457
+ FROM blind_spots WHERE reason NOT LIKE 'test-global%' GROUP BY file`).all();
1458
+ blindMap = new Map(blindRows.map((b) => [b.file, { dyn: b.dyn, unres: b.unres }]));
1459
+ symRows = db3.prepare("SELECT file, name, kind, line FROM nodes WHERE kind NOT IN ('file','module') ORDER BY file, line").all();
1460
+ symsByFile = new Map;
1461
+ for (const s of symRows) {
1462
+ const list = symsByFile.get(s.file) ?? [];
1463
+ list.push({ name: s.name, kind: s.kind, line: s.line });
1464
+ symsByFile.set(s.file, list);
1465
+ }
1466
+ fileInfos = files.map((f) => ({
1467
+ path: f.file,
1468
+ module: moduleOf(f.file),
1469
+ blind: blindMap.get(f.file) ?? { dyn: 0, unres: 0 },
1470
+ symbols: symsByFile.get(f.file) ?? []
1471
+ }));
1472
+ importRows = db3.prepare("SELECT src, dst, line FROM edges WHERE kind = 'imports'").all();
1473
+ rawModuleNames = [...new Set(fileInfos.map((f) => f.module))];
1474
+ ovMap = applyOverlay(rawModuleNames, overlay);
1475
+ for (const fi of fileInfos)
1476
+ fi.module = effModule(fi.module);
1477
+ hiddenModules = new Set([...ovMap.values()].filter((v) => v.hidden).map((v) => v.effective));
1478
+ fileInfos = fileInfos.filter((f) => !hiddenModules.has(f.module));
1479
+ modules = new Map;
1480
+ for (const fi of fileInfos) {
1481
+ const m = modules.get(fi.module) ?? { files: 0, blind: { dyn: 0, unres: 0 } };
1482
+ m.files++;
1483
+ m.blind.dyn += fi.blind.dyn;
1484
+ m.blind.unres += fi.blind.unres;
1485
+ modules.set(fi.module, m);
1486
+ }
1487
+ modEdges = new Map;
1488
+ for (const e of importRows) {
1489
+ const ms = moduleOf(e.src);
1490
+ const md = moduleOf(e.dst);
1491
+ if (ms !== md)
1492
+ modEdges.set(`${ms}→${md}`, (modEdges.get(`${ms}→${md}`) ?? 0) + 1);
1493
+ }
1494
+ cycles = new Set;
1495
+ for (const key of modEdges.keys()) {
1496
+ const [a, b] = key.split("→");
1497
+ if (modEdges.has(`${b}→${a}`)) {
1498
+ cycles.add(key);
1499
+ cycles.add(`${b}→${a}`);
1500
+ }
1501
+ }
1502
+ layouts = {};
1503
+ layouts["__modules__"] = await layoutGraph([...modules.keys()].map((m) => {
1504
+ const o = overlay.modules[m];
1505
+ const b = modules.get(m).blind;
1506
+ const metaParts = [`${modules.get(m).files} files`];
1507
+ if (b.dyn > 0)
1508
+ metaParts.push(`${b.dyn} dyn`);
1509
+ if (b.unres > 0)
1510
+ metaParts.push(`${b.unres} unres`);
1511
+ return { id: m, label: o?.name ?? m, meta: metaParts.join(" · ") };
1512
+ }), [...modEdges.keys()].map((key) => {
1513
+ const [src, dst] = key.split("→");
1514
+ return { src, dst };
1515
+ }));
1516
+ for (const [mod] of modules) {
1517
+ const fs = fileInfos.filter((f) => f.module === mod);
1518
+ if (fs.length === 0 || fs.length > 80)
1519
+ continue;
1520
+ const inSet = new Set(fs.map((f) => f.path));
1521
+ const agg = new Map;
1522
+ for (const e of importRows) {
1523
+ if (inSet.has(e.src) && inSet.has(e.dst) && e.src !== e.dst) {
1524
+ agg.set(`${e.src}→${e.dst}`, { src: e.src, dst: e.dst });
1525
+ }
1526
+ }
1527
+ layouts[mod] = await layoutGraph(fs.map((f) => ({
1528
+ id: f.path,
1529
+ label: f.path.split("/").pop() ?? f.path,
1530
+ meta: `${f.symbols.length} symbols`
1531
+ })), [...agg.values()]);
1532
+ }
1533
+ if (diffBase) {
1534
+ const dbBase = openDatabase(diffBase, { readonly: true });
1535
+ const d = graphDiff(dbBase, db3);
1536
+ const fileStates = {};
1537
+ const moduleCounts = {};
1538
+ const bump = (file, key) => {
1539
+ const m = moduleOf(file);
1540
+ const mc = moduleCounts[m] ?? { added: 0, removed: 0, changed: 0, renamed: 0 };
1541
+ mc[key]++;
1542
+ moduleCounts[m] = mc;
1543
+ };
1544
+ for (const n of d.nodesAdded) {
1545
+ fileStates[n.file] ??= "added";
1546
+ bump(n.file, "added");
1547
+ }
1548
+ for (const n of d.nodesRemoved) {
1549
+ fileStates[n.file] = fileStates[n.file] === "added" ? "changed" : fileStates[n.file] ?? "removed";
1550
+ bump(n.file, "removed");
1551
+ }
1552
+ for (const r of d.renamed) {
1553
+ fileStates[r.file] = "changed";
1554
+ bump(r.file, "renamed");
1555
+ }
1556
+ for (const s of d.signatureChanged) {
1557
+ fileStates[s.file] = "changed";
1558
+ bump(s.file, "changed");
1559
+ }
1560
+ for (const v of d.visibilityChanged) {
1561
+ fileStates[v.file] = "changed";
1562
+ bump(v.file, "changed");
1563
+ }
1564
+ const total = d.nodesAdded.length + d.nodesRemoved.length + d.renamed.length + d.signatureChanged.length + d.visibilityChanged.length;
1565
+ diffOverlay = { fileStates, moduleCounts, summary: `符号 +${d.nodesAdded.length} −${d.nodesRemoved.length} ↻${d.renamed.length} · 签名/可见性 ${d.signatureChanged.length + d.visibilityChanged.length} · 共 ${total} 项` };
1566
+ dbBase.close();
1567
+ }
1568
+ if (impactTarget) {
1569
+ let targetId = impactTarget;
1570
+ const exact = db3.prepare("SELECT id, file FROM nodes WHERE id = ?").get(impactTarget);
1571
+ let targetFile = exact?.file ?? "";
1572
+ if (!exact) {
1573
+ const cands = db3.prepare("SELECT id, file FROM nodes WHERE name = ? AND kind != 'file' LIMIT 2").all(impactTarget);
1574
+ if (cands.length !== 1) {
1575
+ console.error(`--impact: ${cands.length === 0 ? "no match" : "ambiguous"}: ${impactTarget}`);
1576
+ process.exit(1);
1577
+ }
1578
+ targetId = cands[0].id;
1579
+ targetFile = cands[0].file;
1580
+ }
1581
+ const r = impact(db3, targetId, 1e5);
1582
+ const fileLevels = {};
1583
+ const moduleCounts = {};
1584
+ const RANK = { direct: 3, tests: 2, indirect: 1 };
1585
+ for (const it of r.items) {
1586
+ const prev = fileLevels[it.file];
1587
+ if (!prev || RANK[it.level] > RANK[prev])
1588
+ fileLevels[it.file] = it.level;
1589
+ const m = moduleOf(it.file);
1590
+ const mc = moduleCounts[m] ?? { direct: 0, indirect: 0, tests: 0 };
1591
+ mc[it.level]++;
1592
+ moduleCounts[m] = mc;
1593
+ }
1594
+ impactOverlay = { target: targetId, targetFile, fileLevels, moduleCounts };
1595
+ }
1596
+ data = {
1597
+ generated: new Date().toISOString().slice(0, 16).replace("T", " "),
1598
+ layouts,
1599
+ impact: impactOverlay,
1600
+ diff: diffOverlay,
1601
+ repoUrl: repoUrl ?? null,
1602
+ modules: [...modules.entries()].map(([name, v]) => ({ name, ...v })).sort((a, b) => b.files - a.files),
1603
+ moduleMeta: Object.fromEntries([...modules.keys()].map((m) => {
1604
+ const o = overlay.modules[m];
1605
+ return [m, { display: o?.name ?? m, desc: o?.desc ?? "" }];
1606
+ })),
1607
+ modEdges: [...modEdges.entries()].map(([key, w]) => {
1608
+ const [src, dst] = key.split("→");
1609
+ return { src, dst, w, cyclic: cycles.has(key) };
1610
+ }),
1611
+ files: fileInfos,
1612
+ fileEdges: importRows
1613
+ };
1614
+ html = `<!DOCTYPE html>
1615
+ <html lang="zh">
1616
+ <head>
1617
+ <meta charset="utf-8">
1618
+ <title>codeblast · Architecture Map</title>
1619
+ <style>
1620
+ :root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#e6edf3; --dim:#8b949e;
1621
+ --accent:#58a6ff; --warn:#f85149; --ok:#3fb950; }
1622
+ * { box-sizing: border-box; margin: 0; }
1623
+ body { background: var(--bg); color: var(--fg); font: 14px/1.5 -apple-system, "Segoe UI", sans-serif; display: flex; height: 100vh; }
1624
+ #graph { background:
1625
+ radial-gradient(ellipse 80% 60% at 50% -10%, #1f6feb14, transparent),
1626
+ radial-gradient(#30363d55 1px, transparent 1px);
1627
+ background-size: auto, 26px 26px; }
1628
+ #graph { flex: 1; position: relative; overflow: hidden; }
1629
+ svg { width: 100%; height: 100%; cursor: grab; }
1630
+ #side { width: 380px; border-left: 1px solid var(--border); background: var(--panel); overflow-y: auto; padding: 16px; }
1631
+ h1 { font-size: 15px; padding: 12px 16px; border-bottom: 1px solid var(--border); background: var(--panel); }
1632
+ h1 small { color: var(--dim); font-weight: normal; margin-left: 8px; }
1633
+ .node rect { fill: url(#nodeFill); stroke: var(--c, var(--accent)); stroke-width: 1.4; rx: 10;
1634
+ cursor: pointer; filter: drop-shadow(0 2px 6px #010409aa); transition: filter .15s; }
1635
+ .node:hover rect { filter: drop-shadow(0 0 8px var(--c, var(--accent))) drop-shadow(0 2px 6px #010409aa); }
1636
+ .node .accentbar { fill: var(--c, var(--accent)); rx: 2; pointer-events: none; }
1637
+ .node.test { --c: var(--ok); }
1638
+ .node.cyc { --c: var(--warn); }
1639
+ .node.imp-direct { --c: #f85149; } .node.imp-direct rect { stroke-width: 2.2; }
1640
+ .node.imp-tests { --c: #d29922; }
1641
+ .node.imp-indirect { --c: #8957e5; }
1642
+ .node.imp-target rect { stroke: #f85149; stroke-width: 3; filter: drop-shadow(0 0 12px #f8514988); }
1643
+ .node.chg-added { --c: var(--ok); } .node.chg-added rect { stroke-width: 2.2; }
1644
+ .node.chg-removed { --c: #6e40c9; opacity: 0.65; }
1645
+ .node.chg-changed { --c: #d29922; } .node.chg-changed rect { stroke-width: 2.2; }
1646
+ #impactbar { padding: 8px 16px; background: #f8514915; border-bottom: 1px solid #f8514944;
1647
+ font-size: 13px; display: flex; gap: 18px; align-items: center; }
1648
+ #impactbar .sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }
1649
+ .node text { fill: var(--fg); font-size: 13.5px; font-weight: 600; pointer-events: none; }
1650
+ .node .meta { fill: var(--dim); font-size: 11px; font-weight: 400; font-family: ui-monospace, Menlo, monospace; }
1651
+ .edge { stroke: #58a6ff55; fill: none; marker-end: url(#arrow); }
1652
+ .edge.mid { stroke-width: 1.8; stroke: #58a6ff77; }
1653
+ .edge.heavy { stroke-width: 2.6; stroke: #58a6ffaa; }
1654
+ .edge.cyclic { stroke: var(--warn); stroke-dasharray: 5 3; stroke-width: 2;
1655
+ animation: cycflow 1.2s linear infinite; }
1656
+ @keyframes cycflow { to { stroke-dashoffset: -16; } }
1657
+ .edge.hi { stroke: #d29922 !important; stroke-width: 2.6; stroke-opacity: 1; }
1658
+ .edge.dim { stroke-opacity: 0.1; }
1659
+ .edge.faint { stroke-opacity: 0.06; }
1660
+ .node.dimn { opacity: 0.3; }
1661
+ #graph { position: relative; }
1662
+ #toolbar { position: absolute; top: 12px; right: 12px; }
1663
+ #toolbar button { background: var(--panel); color: var(--fg); border: 1px solid var(--border);
1664
+ border-radius: 8px; padding: 6px 14px; cursor: pointer; font-size: 13px; }
1665
+ #toolbar button:hover { border-color: var(--accent); }
1666
+ #toolbar button.on { border-color: var(--accent); background: #1f6feb33; }
1667
+ body.editing .node rect { cursor: text; }
1668
+ body.editing .node.sel rect { stroke: #d29922; stroke-width: 3; }
1669
+ .node.hidden-mod { opacity: 0.35; }
1670
+ .node.hidden-mod rect { stroke-dasharray: 4 3; }
1671
+ #editpanel { border-top: 1px solid var(--border); padding: 12px 0; margin-top: 12px; }
1672
+ #editpanel input { width: 100%; background: #0d1117; color: var(--fg); border: 1px solid var(--border);
1673
+ border-radius: 6px; padding: 6px 8px; font-size: 13px; margin: 4px 0 8px; }
1674
+ #editpanel .row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
1675
+ #editpanel button { flex: 1; background: var(--panel); color: var(--fg); border: 1px solid var(--border);
1676
+ border-radius: 6px; padding: 6px 8px; cursor: pointer; font-size: 12.5px; }
1677
+ #editpanel button:hover { border-color: var(--accent); }
1678
+ #editpanel button.primary { background: #1f6feb33; border-color: var(--accent); }
1679
+ #editpanel pre { background: #0d1117; border: 1px solid var(--border); border-radius: 6px;
1680
+ padding: 8px; font-size: 11px; max-height: 180px; overflow: auto; white-space: pre-wrap; }
1681
+ #editpanel .tip { color: var(--dim); font-size: 12px; }
1682
+ .edge-label { fill: var(--dim); font-size: 10px; }
1683
+ .crumb { color: var(--accent); cursor: pointer; }
1684
+ #side h2 { font-size: 14px; margin: 8px 0; }
1685
+ #side .item { padding: 6px 8px; border-radius: 6px; cursor: pointer; display: flex; justify-content: space-between; }
1686
+ #side .item:hover { background: #21262d; }
1687
+ #side .kind { color: var(--dim); font-size: 12px; }
1688
+ #side .blind { color: var(--warn); font-size: 12px; }
1689
+ .hint { color: var(--dim); font-size: 12px; margin-top: 12px; }
1690
+ .cyclic-banner { background: #f8514922; border: 1px solid var(--warn); border-radius: 6px; padding: 8px 10px; margin-bottom: 10px; font-size: 13px; }
1691
+ </style>
1692
+ </head>
1693
+ <body>
1694
+ <div style="display:flex;flex-direction:column;flex:1">
1695
+ <h1 id="title">Architecture Map <small id="crumbs"></small></h1>
1696
+ ${data.impact ? `<div id="impactbar"><strong>Impact: ${data.impact.target.split("#").pop()}</strong>
1697
+ <span><span class="sw" style="background:#f85149"></span>直接影响</span>
1698
+ <span><span class="sw" style="background:#d29922"></span>受影响测试</span>
1699
+ <span><span class="sw" style="background:#8957e5"></span>传递影响</span>
1700
+ <span style="color:var(--dim)">未着色 = 不受影响</span></div>` : ""}
1701
+ ${data.diff ? `<div id="impactbar" style="background:#3fb95012;border-color:#3fb95044"><strong>Change Map</strong>
1702
+ <span>${data.diff.summary}</span>
1703
+ <span><span class="sw" style="background:#3fb950"></span>新增</span>
1704
+ <span><span class="sw" style="background:#d29922"></span>修改</span>
1705
+ <span><span class="sw" style="background:#6e40c9"></span>删除</span></div>` : ""}
1706
+ <div id="graph"><svg id="svg"><defs>
1707
+ <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
1708
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#58a6ff88"/></marker>
1709
+ <linearGradient id="nodeFill" x1="0" y1="0" x2="0" y2="1">
1710
+ <stop offset="0" stop-color="#1c2430"/><stop offset="1" stop-color="#151b23"/>
1711
+ </linearGradient>
1712
+ </defs><g id="viewport"></g></svg>
1713
+ <div id="toolbar"><button id="editbtn" onclick="toggleEdit()">✎ 编辑模块</button> <button onclick="toggleFit()">适配 / 100%</button> <button onclick="exportPNG()">导出 PNG</button></div></div>
1714
+ </div>
1715
+ <div id="side"><h2>概览</h2><div id="detail"></div>
1716
+ <p class="hint">点击模块下钻到文件层;点击文件查看符号。红色虚线 = 循环依赖。滚轮缩放,拖拽平移。</p>
1717
+ </div>
1718
+ <script>
1719
+ const DATA = ${JSON.stringify(data)};
1720
+ ${CLIENT_JS}</script>
1721
+ </body>
1722
+ </html>`;
1723
+ fs5.writeFileSync(outPath2, html);
1724
+ console.error(`written: ${outPath2} (${(html.length / 1024).toFixed(0)}KB)`);
1725
+ });
1726
+
1727
+ // src/archmap.ts
1728
+ var exports_archmap = {};
1729
+ import fs6 from "node:fs";
1730
+ var dbPath4, outFlag3, outPath3, db4, TEST_RE2, moduleOf2 = (file) => {
1731
+ if (TEST_RE2.test(file))
1732
+ return "tests";
1733
+ const ix = file.indexOf("/");
1734
+ return ix < 0 ? "(root)" : file.slice(0, ix);
1735
+ }, files2, fileCount, imports, weight, blinds, blindByModule, alias, seq = 0, idOf = (m) => {
1736
+ let a = alias.get(m);
1737
+ if (!a) {
1738
+ a = `M${seq++}`;
1739
+ alias.set(m, a);
1740
+ }
1741
+ return a;
1742
+ }, lines2, sorted, sortedEdges, summaryTable, outDeg, inDeg, doc2;
1743
+ var init_archmap = __esm(() => {
1744
+ init_db();
1745
+ [dbPath4] = process.argv.slice(2);
1746
+ if (!dbPath4) {
1747
+ console.error("usage: codeblast mermaid <graph.db> [--out file.md]");
1748
+ process.exit(1);
1749
+ }
1750
+ outFlag3 = process.argv.indexOf("--out");
1751
+ outPath3 = outFlag3 >= 0 ? process.argv[outFlag3 + 1] : undefined;
1752
+ db4 = openDatabase(dbPath4, { readonly: true });
1753
+ TEST_RE2 = /\.(test|spec)\.[cm]?[jt]sx?$|__tests__\/|(^|\/)tests?\/|(^|\/)test_[^/]*\.py$|_test\.py$|conftest\.py$/;
1754
+ files2 = db4.prepare("SELECT id, file FROM nodes WHERE kind = 'file'").all();
1755
+ fileCount = new Map;
1756
+ for (const f of files2) {
1757
+ const m = moduleOf2(f.file);
1758
+ fileCount.set(m, (fileCount.get(m) ?? 0) + 1);
1759
+ }
1760
+ imports = db4.prepare("SELECT src, dst FROM edges WHERE kind = 'imports'").all();
1761
+ weight = new Map;
1762
+ for (const e of imports) {
1763
+ const ms = moduleOf2(e.src);
1764
+ const md = moduleOf2(e.dst);
1765
+ if (ms === md)
1766
+ continue;
1767
+ const key = `${ms}\x00${md}`;
1768
+ weight.set(key, (weight.get(key) ?? 0) + 1);
1769
+ }
1770
+ blinds = db4.prepare(`SELECT file,
1771
+ SUM(CASE WHEN reason LIKE 'dynamic%' OR reason LIKE 'subprocess%' OR reason LIKE 'star import%' OR reason LIKE 'unresolved self%' OR reason LIKE 'attribute call%' THEN 1 ELSE 0 END) dyn,
1772
+ SUM(CASE WHEN reason LIKE 'unresolved call%' THEN 1 ELSE 0 END) unres
1773
+ FROM blind_spots WHERE reason NOT LIKE 'test-global%' GROUP BY file`).all();
1774
+ blindByModule = new Map;
1775
+ for (const b of blinds) {
1776
+ const m = moduleOf2(b.file);
1777
+ const cur = blindByModule.get(m) ?? { dyn: 0, unres: 0 };
1778
+ cur.dyn += b.dyn;
1779
+ cur.unres += b.unres;
1780
+ blindByModule.set(m, cur);
1781
+ }
1782
+ alias = new Map;
1783
+ lines2 = ["```mermaid", "flowchart TD"];
1784
+ sorted = [...fileCount.entries()].sort((a, b) => b[1] - a[1]);
1785
+ for (const [m, count] of sorted) {
1786
+ const blind = blindByModule.get(m) ?? { dyn: 0, unres: 0 };
1787
+ const parts = [`${count} files`];
1788
+ if (blind.dyn > 0)
1789
+ parts.push(`${blind.dyn} dyn`);
1790
+ if (blind.unres > 0)
1791
+ parts.push(`${blind.unres} unres`);
1792
+ const label = `${m}<br/>${parts.join(" · ")}`;
1793
+ const shape = m === "tests" ? `${idOf(m)}[/"${label}"/]` : `${idOf(m)}["${label}"]`;
1794
+ lines2.push(` ${shape}`);
1795
+ }
1796
+ sortedEdges = [...weight.entries()].sort((a, b) => b[1] - a[1]);
1797
+ for (const [key, w] of sortedEdges) {
1798
+ const [ms, md] = key.split("\x00");
1799
+ lines2.push(` ${idOf(ms)} -->|${w}| ${idOf(md)}`);
1800
+ }
1801
+ lines2.push("```");
1802
+ summaryTable = [
1803
+ "",
1804
+ "| 模块 | 文件数 | 动态调用盲区 | 未解析调用 | 出边依赖 | 入边被依赖 |",
1805
+ "|---|---|---|---|---|"
1806
+ ];
1807
+ outDeg = new Map;
1808
+ inDeg = new Map;
1809
+ for (const [key, w] of weight) {
1810
+ const [ms, md] = key.split("\x00");
1811
+ outDeg.set(ms, (outDeg.get(ms) ?? 0) + w);
1812
+ inDeg.set(md, (inDeg.get(md) ?? 0) + w);
1813
+ }
1814
+ for (const [m, count] of sorted) {
1815
+ summaryTable.push(`| ${m} | ${count} | ${blindByModule.get(m)?.dyn ?? 0} | ${blindByModule.get(m)?.unres ?? 0} | ${outDeg.get(m) ?? 0} | ${inDeg.get(m) ?? 0} |`);
1816
+ }
1817
+ doc2 = [
1818
+ `# Architecture Map`,
1819
+ "",
1820
+ `> codeblast M3-v0(目录级折叠)· 节点=模块(含文件数/盲区数),边=import 依赖(数字=强度)`,
1821
+ `> 口径: **dyn** = 结构性动态调用(eval/动态import/子进程/属性链),静态原理性不可达;`,
1822
+ `> **unres** = 调用目标解析失败(多为缺依赖或复杂表达式),可能因环境不全而虚高。`,
1823
+ "",
1824
+ ...lines2,
1825
+ ...summaryTable,
1826
+ ""
1827
+ ].join(`
1828
+ `);
1829
+ if (outPath3) {
1830
+ fs6.writeFileSync(outPath3, doc2);
1831
+ console.error(`written: ${outPath3}`);
1832
+ } else {
1833
+ console.log(doc2);
1834
+ }
1835
+ });
1836
+
1837
+ // src/cochange.ts
1838
+ var exports_cochange = {};
1839
+ var repo, dbPath5, cFlag, N_COMMITS, MIN_CO = 3, MIN_RATIO = 0.5, MAX_FILES_PER_COMMIT = 20, db5, known, log, commits, fileFreq, pairFreq, staticPairs, insert, emitted = 0, samples, writeAll;
1840
+ var init_cochange = __esm(() => {
1841
+ init_db();
1842
+ init_proc();
1843
+ [repo, dbPath5] = process.argv.slice(2);
1844
+ if (!repo || !dbPath5) {
1845
+ console.error("usage: codeblast cochange <repo> <graph.db> [--commits 500]");
1846
+ process.exit(1);
1847
+ }
1848
+ cFlag = process.argv.indexOf("--commits");
1849
+ N_COMMITS = cFlag >= 0 ? Number(process.argv[cFlag + 1]) : 500;
1850
+ db5 = openDatabase(dbPath5);
1851
+ known = new Set(db5.prepare("SELECT path FROM files").all().map((r) => r.path));
1852
+ log = spawnSync(["git", "log", "--no-merges", `-${N_COMMITS}`, "--name-only", "--format=%x01%h"], { cwd: repo, maxBuffer: 64 * 1024 * 1024 });
1853
+ if (log.exitCode !== 0) {
1854
+ console.error(log.stderr.slice(0, 300));
1855
+ process.exit(1);
1856
+ }
1857
+ commits = [];
1858
+ for (const block of log.stdout.split("\x01")) {
1859
+ if (!block.trim())
1860
+ continue;
1861
+ const lines = block.trim().split(`
1862
+ `);
1863
+ const sha = lines[0].trim();
1864
+ const files = lines.slice(1).map((l) => l.trim()).filter((f) => f && known.has(f));
1865
+ if (files.length >= 2 && files.length <= MAX_FILES_PER_COMMIT)
1866
+ commits.push({ sha, files });
1867
+ }
1868
+ fileFreq = new Map;
1869
+ pairFreq = new Map;
1870
+ for (const c of commits) {
1871
+ for (const f of c.files)
1872
+ fileFreq.set(f, (fileFreq.get(f) ?? 0) + 1);
1873
+ const sorted = [...c.files].sort();
1874
+ for (let i = 0;i < sorted.length; i++) {
1875
+ for (let j = i + 1;j < sorted.length; j++) {
1876
+ const key = `${sorted[i]}\x00${sorted[j]}`;
1877
+ const e = pairFreq.get(key) ?? { count: 0, lastSha: c.sha };
1878
+ e.count++;
1879
+ e.lastSha = c.sha;
1880
+ pairFreq.set(key, e);
1881
+ }
1882
+ }
1883
+ }
1884
+ staticPairs = new Set;
1885
+ for (const r of db5.prepare("SELECT DISTINCT src, dst FROM edges WHERE kind != 'co_change'").all()) {
1886
+ const fa = r.src.split("#")[0];
1887
+ const fb = r.dst.split("#")[0];
1888
+ if (fa === fb)
1889
+ continue;
1890
+ staticPairs.add([fa, fb].sort().join("\x00"));
1891
+ }
1892
+ db5.prepare("DELETE FROM edges WHERE kind = 'co_change'").run();
1893
+ insert = db5.prepare("INSERT OR REPLACE INTO edges (src, dst, kind, file, line, confidence, src_file) VALUES (?, ?, 'co_change', ?, ?, 'conservative', ?)");
1894
+ samples = [];
1895
+ writeAll = transaction(db5, () => {
1896
+ for (const [key, e] of pairFreq) {
1897
+ if (e.count < MIN_CO)
1898
+ continue;
1899
+ const [a, b] = key.split("\x00");
1900
+ if (staticPairs.has(key))
1901
+ continue;
1902
+ const ratio = Math.max(e.count / fileFreq.get(a), e.count / fileFreq.get(b));
1903
+ if (ratio < MIN_RATIO)
1904
+ continue;
1905
+ insert.run(a, b, a, e.count, `git:${e.lastSha}`);
1906
+ insert.run(b, a, b, e.count, `git:${e.lastSha}`);
1907
+ emitted += 2;
1908
+ if (samples.length < 10)
1909
+ samples.push(`${a} <-> ${b} (${e.count}x, ${(ratio * 100).toFixed(0)}%, ${e.lastSha})`);
1910
+ }
1911
+ });
1912
+ writeAll();
1913
+ console.log(JSON.stringify({ commits_scanned: commits.length, pairs_considered: pairFreq.size, edges_emitted: emitted }, null, 2));
1914
+ for (const s of samples)
1915
+ console.error(" " + s);
1916
+ });
1917
+
1918
+ // src/pr-silence.ts
1919
+ function structuralTotal(diff) {
1920
+ return diff.nodesAdded.length + diff.nodesRemoved.length + diff.renamed.length + diff.edgesAdded.length + diff.edgesRemoved.length + diff.visibilityChanged.length + diff.signatureChanged.length;
1921
+ }
1922
+ function bodySignalCount(bodyChanged, diffLineCount, hasCallImpact) {
1923
+ let n = 0;
1924
+ for (const fn of bodyChanged) {
1925
+ if (hasCallImpact(fn))
1926
+ n++;
1927
+ else if (!AUX_RE.test(fn.file) && diffLineCount >= BIG_DIFF_LINES)
1928
+ n++;
1929
+ }
1930
+ return n;
1931
+ }
1932
+ function coreNamedCount(diff, prodNodesAdded) {
1933
+ return diff.edgesAdded.filter((e) => !AUX_RE.test(e.file)).length + prodNodesAdded.filter((n) => !AUX_RE.test(n.file)).length + diff.renamed.filter((r) => !AUX_RE.test(r.file)).length + diff.visibilityChanged.filter((v) => !AUX_RE.test(v.file)).length + diff.signatureChanged.filter((s) => !AUX_RE.test(s.file)).length;
1934
+ }
1935
+ var TEST_RE3, AUX_RE, BIG_DIFF_LINES = 40;
1936
+ var init_pr_silence = __esm(() => {
1937
+ TEST_RE3 = /\.(test|spec)\.[cm]?[jt]sx?$|__tests__\/|(^|\/)tests?\//;
1938
+ AUX_RE = /^(www|docs|examples)\//;
1939
+ });
1940
+
1941
+ // src/pr-comment.ts
1942
+ var exports_pr_comment = {};
1943
+ import fs7 from "node:fs";
1944
+ async function buildGraphAt2(ref, db) {
1945
+ const shaProc = spawnSync(["git", "rev-parse", ref], { cwd: repo2 });
1946
+ const sha = shaProc.exitCode === 0 ? shaProc.stdout.trim() : null;
1947
+ const cache = sha ? `/tmp/codeblast-cache-${sha}.db` : null;
1948
+ if (cache && fs7.existsSync(cache)) {
1949
+ fs7.copyFileSync(cache, db);
1950
+ return;
1951
+ }
1952
+ const wt = `/tmp/codeblast-pr-${ref.slice(0, 12)}`;
1953
+ spawnSync(["git", "worktree", "remove", "--force", wt], { cwd: repo2 });
1954
+ const add = spawnSync(["git", "worktree", "add", "--detach", wt, ref], { cwd: repo2 });
1955
+ if (add.exitCode !== 0)
1956
+ throw new Error(add.stderr.slice(0, 300));
1957
+ try {
1958
+ const p = spawnSync(selfCommand("index", wt, "--db", db));
1959
+ if (p.exitCode !== 0)
1960
+ throw new Error(p.stderr.slice(0, 500));
1961
+ const ck = openDatabase(db);
1962
+ ck.exec("PRAGMA wal_checkpoint(TRUNCATE);");
1963
+ ck.close();
1964
+ if (cache)
1965
+ fs7.copyFileSync(db, cache);
1966
+ } finally {
1967
+ spawnSync(["git", "worktree", "remove", "--force", wt], { cwd: repo2 });
1968
+ }
1969
+ }
1970
+ var args3, repo2, baseSha, headSha, urlFlag, repoUrl2, dbPathA = "/tmp/codeblast-pr-base.db", dbPathB = "/tmp/codeblast-pr-head.db", dbA2, dbB2, diff2, total2, structuralIds, bodyChanged, link = (file, line) => repoUrl2 ? `[${file}:${line}](${repoUrl2}/blob/${headSha}/${file}#L${line})` : `${file}:${line}`, lines3, folded2, sigView = (s) => {
1971
+ let d = 0;
1972
+ while (d < s.from.length && d < s.to.length && s.from[d] === s.to[d])
1973
+ d++;
1974
+ const ctx = Math.max(0, d - 15);
1975
+ const clip = (t) => (ctx > 0 ? "…" : "") + t.slice(ctx, ctx + 70).replace(/`/g, "'") + (t.length > ctx + 70 ? "…" : "");
1976
+ const wrap = s.kind === "interface" || s.kind === "const" ? (t) => t : (t) => `(${t})`;
1977
+ const what = s.kind === "interface" ? "成员变化" : s.kind === "const" ? "类型变化" : "";
1978
+ return `- \`${s.name}\`${what ? ` ${what}` : ""}: \`${wrap(clip(s.from))}\` → \`${wrap(clip(s.to))}\` (${link(s.file, s.line)})`;
1979
+ }, apiSig, testSig, uncovered, impactRows, prodNodesAdded, diffLineCount, bodySignal, coreNamed;
1980
+ var init_pr_comment = __esm(async () => {
1981
+ init_db();
1982
+ init_proc();
1983
+ init_impact();
1984
+ init_pr_silence();
1985
+ args3 = process.argv.slice(2);
1986
+ [repo2, baseSha, headSha] = args3;
1987
+ if (!repo2 || !baseSha || !headSha) {
1988
+ console.error("usage: codeblast pr-comment <repo> <base-sha> <head-sha> [--repo-url <url>]");
1989
+ process.exit(1);
1990
+ }
1991
+ urlFlag = args3.indexOf("--repo-url");
1992
+ repoUrl2 = urlFlag >= 0 ? args3[urlFlag + 1] : undefined;
1993
+ for (const f of [dbPathA, dbPathB])
1994
+ for (const s of ["", "-wal", "-shm"])
1995
+ fs7.rmSync(f + s, { force: true });
1996
+ await buildGraphAt2(baseSha, dbPathA);
1997
+ await buildGraphAt2(headSha, dbPathB);
1998
+ dbA2 = openDatabase(dbPathA, { readonly: true });
1999
+ dbB2 = openDatabase(dbPathB, { readonly: true });
2000
+ diff2 = graphDiff(dbA2, dbB2);
2001
+ total2 = structuralTotal(diff2);
2002
+ structuralIds = new Set([...diff2.nodesAdded.map((n) => n.id), ...diff2.renamed.map((r) => `${r.file}#${r.to}`)]);
2003
+ bodyChanged = [];
2004
+ {
2005
+ const diffOut = spawnSync(["git", "diff", "--unified=0", baseSha, headSha, "--", "*.ts", "*.tsx"], { cwd: repo2, maxBuffer: 64 * 1024 * 1024 }).stdout;
2006
+ const findFn = dbB2.prepare("SELECT id, name, kind, file, line FROM nodes WHERE file = ? AND kind IN ('function','method') AND line <= ? AND end_line >= ? ORDER BY (end_line - line) ASC LIMIT 1");
2007
+ let curFile = "";
2008
+ const seen = new Set;
2009
+ for (const ln of diffOut.split(`
2010
+ `)) {
2011
+ const fm = ln.match(/^\+\+\+ b\/(.+)$/);
2012
+ if (fm) {
2013
+ curFile = fm[1];
2014
+ continue;
2015
+ }
2016
+ const hm = ln.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
2017
+ if (!hm || !curFile || TEST_RE3.test(curFile))
2018
+ continue;
2019
+ const start = Number(hm[1]);
2020
+ const count = hm[2] === undefined ? 1 : Number(hm[2]);
2021
+ for (const probe of [start, start + Math.max(0, count - 1)]) {
2022
+ const fn = findFn.get(curFile, probe, probe);
2023
+ if (fn && !structuralIds.has(fn.id) && !seen.has(fn.id)) {
2024
+ seen.add(fn.id);
2025
+ bodyChanged.push(fn);
2026
+ }
2027
+ }
2028
+ }
2029
+ }
2030
+ if (total2 === 0 && bodyChanged.length === 0)
2031
+ process.exit(0);
2032
+ lines3 = [
2033
+ `## \uD83E\uDDED codeblast · 结构变更分析`,
2034
+ ``,
2035
+ `**${total2} 项结构变更**(符号 +${diff2.nodesAdded.length} −${diff2.nodesRemoved.length} ↻${diff2.renamed.length},依赖边 +${diff2.edgesAdded.length} −${diff2.edgesRemoved.length})`,
2036
+ ``
2037
+ ];
2038
+ folded2 = foldToModules(diff2);
2039
+ if (folded2.size > 1) {
2040
+ lines3.push(`| 模块 | 符号变化 | 依赖变化 |`, `|---|---|---|`);
2041
+ for (const [m, v] of folded2) {
2042
+ lines3.push(`| ${m} | +${v.added} −${v.removed} ↻${v.renamed} | +${v.edgesIn} −${v.edgesOut} |`);
2043
+ }
2044
+ lines3.push(``);
2045
+ }
2046
+ if (diff2.renamed.length > 0) {
2047
+ lines3.push(`### 重命名`, ``);
2048
+ for (const r of diff2.renamed.slice(0, 10)) {
2049
+ lines3.push(`- \`${r.from}\` → \`${r.to}\` (${r.kind})`);
2050
+ }
2051
+ if (diff2.renamed.length > 10)
2052
+ lines3.push(`- …及另外 ${diff2.renamed.length - 10} 项`);
2053
+ lines3.push(``);
2054
+ }
2055
+ if (diff2.visibilityChanged.length > 0) {
2056
+ lines3.push(`### 可见性变化(公共 API 面)`, ``);
2057
+ for (const v of diff2.visibilityChanged.slice(0, 10)) {
2058
+ lines3.push(`- \`${v.name}\` (${v.kind}) ${v.nowExported ? "转为导出" : "**不再导出**"} (${link(v.file, v.line)})`);
2059
+ }
2060
+ lines3.push(``);
2061
+ }
2062
+ apiSig = diff2.signatureChanged.filter((s) => !TEST_RE3.test(s.file));
2063
+ testSig = diff2.signatureChanged.filter((s) => TEST_RE3.test(s.file));
2064
+ if (apiSig.length > 0) {
2065
+ lines3.push(`### 签名变更(公共 API 面)`, ``, ...apiSig.slice(0, 10).map(sigView));
2066
+ if (apiSig.length > 10)
2067
+ lines3.push(`- …及另外 ${apiSig.length - 10} 项`);
2068
+ lines3.push(``);
2069
+ }
2070
+ if (testSig.length > 0) {
2071
+ lines3.push(`### 测试助手签名变更`, ``, ...testSig.slice(0, 5).map(sigView), ``);
2072
+ }
2073
+ if (diff2.edgesAdded.length > 0) {
2074
+ lines3.push(`### 新增依赖`, ``);
2075
+ for (const e of diff2.edgesAdded.slice(0, 10)) {
2076
+ const srcName = e.src.split("#").pop();
2077
+ const dstName = e.dst.split("#").pop();
2078
+ lines3.push(`- \`${srcName}\` → \`${dstName}\` (${link(e.file, e.line)})`);
2079
+ }
2080
+ if (diff2.edgesAdded.length > 10)
2081
+ lines3.push(`- …及另外 ${diff2.edgesAdded.length - 10} 条`);
2082
+ lines3.push(``);
2083
+ }
2084
+ uncovered = [];
2085
+ impactRows = [];
2086
+ prodNodesAdded = diff2.nodesAdded.filter((n) => !TEST_RE3.test(n.file));
2087
+ for (const n of prodNodesAdded.slice(0, 15)) {
2088
+ try {
2089
+ const r = impact(dbB2, n.id, 2000);
2090
+ const callItems = r.items.filter((i) => i.channel === "call");
2091
+ const tests = callItems.filter((i) => i.level === "tests").length;
2092
+ impactRows.push(`| \`${n.name}\` | ${n.kind} | ${callItems.length} | ${tests} | ${link(n.file, n.line)} |`);
2093
+ const blind = r.blind_spot_count > 0;
2094
+ if (tests === 0 && n.kind !== "interface" && n.kind !== "const") {
2095
+ uncovered.push(`- \`${n.name}\` (${link(n.file, n.line)})${blind ? " — 所在文件含动态调用,覆盖可能未被静态识别" : ""}`);
2096
+ }
2097
+ } catch {}
2098
+ }
2099
+ if (impactRows.length > 0) {
2100
+ lines3.push(`### 新增符号的影响半径(仅调用链可达,不含 import 粗粒度)`, ``, `| 符号 | 类型 | 调用链影响 | 受影响测试 | 位置 |`, `|---|---|---|---|---|`, ...impactRows, ``);
2101
+ }
2102
+ if (uncovered.length > 0) {
2103
+ lines3.push(`### ⚠️ 无测试覆盖的新增符号`, ``, ...uncovered, ``);
2104
+ }
2105
+ diffLineCount = spawnSync(["git", "diff", "--numstat", baseSha, headSha], { cwd: repo2, maxBuffer: 16 * 1024 * 1024 }).stdout.split(`
2106
+ `).reduce((sum, l) => {
2107
+ const m = l.match(/^(\d+)\t(\d+)\t/);
2108
+ return sum + (m ? Number(m[1]) + Number(m[2]) : 0);
2109
+ }, 0);
2110
+ bodySignal = bodySignalCount(bodyChanged, diffLineCount, (fn) => {
2111
+ try {
2112
+ return impact(dbB2, fn.id, 2000).items.some((i) => i.channel === "call");
2113
+ } catch {
2114
+ return false;
2115
+ }
2116
+ });
2117
+ coreNamed = coreNamedCount(diff2, prodNodesAdded);
2118
+ if (coreNamed + bodySignal === 0)
2119
+ process.exit(0);
2120
+ if (bodyChanged.length > 0) {
2121
+ const rows = [];
2122
+ for (const fn of bodyChanged.slice(0, 12)) {
2123
+ try {
2124
+ const r = impact(dbB2, fn.id, 2000);
2125
+ const callItems = r.items.filter((i) => i.channel === "call");
2126
+ const tests = callItems.filter((i) => i.level === "tests").length;
2127
+ rows.push(`| \`${fn.name}\` | ${callItems.length} | ${tests} | ${link(fn.file, fn.line)} |`);
2128
+ } catch {}
2129
+ }
2130
+ if (rows.length > 0) {
2131
+ lines3.push(`### 函数体内改动(结构未变,行为可能变)`, ``, `| 函数 | 调用链影响 | 受影响测试 | 位置 |`, `|---|---|---|---|`, ...rows, ``);
2132
+ }
2133
+ if (bodyChanged.length > 12)
2134
+ lines3.push(`…及另外 ${bodyChanged.length - 12} 个函数`, ``);
2135
+ }
2136
+ if (total2 === 0 && bodyChanged.length > 0) {
2137
+ lines3[2] = `**无结构变更**,但有 ${bodyChanged.length} 个函数体内改动(见下)`;
2138
+ }
2139
+ lines3.push(`<sub>由 [codeblast](https://github.com/alloevil/codeblast) 生成 · 每条结论基于静态分析,含证据链接 · 动态调用盲区不在本报告内 · 评论不准?[30 秒反馈](https://github.com/alloevil/codeblast/issues/new?template=bot-feedback.yml&title=${encodeURIComponent(`[feedback] ${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`)})</sub>`);
2140
+ console.log(lines3.join(`
2141
+ `));
2142
+ });
2143
+
2144
+ // src/demo.ts
2145
+ var exports_demo = {};
2146
+ import fs8 from "node:fs";
2147
+ import path3 from "node:path";
2148
+ var repo3, db6 = "/tmp/codeblast-demo.db", out = "/tmp/codeblast-demo-arch.html", run = (label, args) => {
2149
+ console.log(`
2150
+ \x1B[36m▸ ${label}\x1B[0m`);
2151
+ console.log(` $ codeblast ${args.slice(2).join(" ")}`);
2152
+ const p = spawnSync(args);
2153
+ if (p.exitCode !== 0) {
2154
+ console.error(p.stderr.slice(-800) || p.stdout.slice(-800));
2155
+ console.error(`
2156
+ \x1B[31m✗ step failed: ${label}\x1B[0m`);
2157
+ process.exit(p.exitCode);
2158
+ }
2159
+ process.stdout.write(p.stdout.split(`
2160
+ `).slice(0, 14).map((l) => " " + l).join(`
2161
+ `) + `
2162
+ `);
2163
+ return p.stdout;
2164
+ }, conn, pick;
2165
+ var init_demo = __esm(() => {
2166
+ init_db();
2167
+ init_proc();
2168
+ repo3 = path3.resolve(process.argv[2] ?? path3.join(import.meta.dirname, ".."));
2169
+ console.log(`codeblast demo — target: ${repo3}`);
2170
+ for (const s of ["", "-wal", "-shm"])
2171
+ fs8.rmSync(db6 + s, { force: true });
2172
+ run("1/4 build graph", selfCommand("index", repo3, "--db", db6));
2173
+ run("2/4 incremental rerun (should skip everything)", selfCommand("index", repo3, "--db", db6));
2174
+ conn = openDatabase(db6, { readonly: true });
2175
+ pick = conn.prepare(`SELECT n.id, COUNT(DISTINCT e.src) c FROM nodes n
2176
+ JOIN edges e ON e.dst = n.id AND e.kind = 'calls'
2177
+ WHERE n.kind IN ('function','method','class') AND n.exported = 1
2178
+ GROUP BY n.id ORDER BY c DESC LIMIT 1`).get();
2179
+ conn.close();
2180
+ if (pick) {
2181
+ run(`3/4 impact of the most-called export (${pick.id.split("#").pop()}, ${pick.c} callers)`, selfCommand("impact", db6, pick.id));
2182
+ } else {
2183
+ console.log(`
2184
+ ▸ 3/4 impact — skipped: no exported symbol with callers in this repo`);
2185
+ }
2186
+ run("4/4 interactive architecture map", selfCommand("archmap", db6, "--out", out));
2187
+ console.log(`
2188
+ \x1B[32m✓ demo complete\x1B[0m
2189
+ graph: ${db6}
2190
+ map: ${out} ← open this in a browser
2191
+ next: codeblast impact ${db6} "<symbol>"
2192
+ codeblast change <repo> HEAD~1 HEAD
2193
+ live demos: https://alloevil.github.io/codeblast/`);
2194
+ });
2195
+
2196
+ // src/name-modules.ts
2197
+ var exports_name_modules = {};
2198
+ import fs9 from "node:fs";
2199
+ var dbPath6, overlayFlag2, overlayPath2, db7, TEST_RE4, files3, byModule, symStmt, evidence, prompt, cmd;
2200
+ var init_name_modules = __esm(async () => {
2201
+ init_db();
2202
+ init_proc();
2203
+ init_overlay();
2204
+ [dbPath6] = process.argv.slice(2);
2205
+ overlayFlag2 = process.argv.indexOf("--overlay");
2206
+ overlayPath2 = overlayFlag2 >= 0 ? process.argv[overlayFlag2 + 1] : "codeblast.overlay.json";
2207
+ if (!dbPath6) {
2208
+ console.error("usage: codeblast name-modules <graph.db> --overlay codeblast.overlay.json");
2209
+ process.exit(1);
2210
+ }
2211
+ db7 = openDatabase(dbPath6, { readonly: true });
2212
+ TEST_RE4 = /\.(test|spec)\.[cm]?[jt]sx?$|__tests__\/|(^|\/)tests?\/|(^|\/)test_[^/]*\.py$|_test\.py$|conftest\.py$/;
2213
+ files3 = db7.prepare("SELECT file FROM nodes WHERE kind='file'").all();
2214
+ byModule = new Map;
2215
+ for (const { file } of files3) {
2216
+ const m = TEST_RE4.test(file) ? "tests" : file.includes("/") ? file.slice(0, file.indexOf("/")) : "(root)";
2217
+ const list = byModule.get(m) ?? [];
2218
+ list.push(file);
2219
+ byModule.set(m, list);
2220
+ }
2221
+ symStmt = db7.prepare("SELECT DISTINCT name FROM nodes WHERE exported=1 AND kind IN ('function','class','method') AND file LIKE ? LIMIT 15");
2222
+ evidence = {};
2223
+ for (const [mod, fs] of byModule) {
2224
+ const prefix = mod === "(root)" ? "" : mod + "/";
2225
+ const exports = mod === "(root)" ? [] : symStmt.all(prefix + "%").map((r) => r.name);
2226
+ evidence[mod] = { files: fs.slice(0, 15).map((f) => f.replace(prefix, "")), exports };
2227
+ }
2228
+ prompt = `你是代码架构分析器。基于每个模块的文件清单和导出符号,给出中文人话名(≤8字)和一句话职责(≤25字)。
2229
+ 只输出 JSON,格式:{"<模块目录名>": {"name": "<人话名>", "desc": "<职责>"}}
2230
+ 不要编造证据里看不到的功能。证据:
2231
+ ${JSON.stringify(evidence, null, 1)}`;
2232
+ cmd = process.env.LLM_CMD;
2233
+ if (!cmd) {
2234
+ const applyFlag = process.argv.indexOf("--apply");
2235
+ if (applyFlag >= 0) {
2236
+ const namesJson = JSON.parse(fs9.readFileSync(process.argv[applyFlag + 1], "utf8"));
2237
+ const overlay = await loadOverlay(overlayPath2);
2238
+ for (const [mod, v] of Object.entries(namesJson)) {
2239
+ const existing = overlay.modules[mod];
2240
+ if (existing?.name)
2241
+ continue;
2242
+ overlay.modules[mod] = { ...existing, name: `${v.name}`, ...v.desc ? {} : {} };
2243
+ overlay.modules[mod].desc = v.desc;
2244
+ }
2245
+ fs9.writeFileSync(overlayPath2, JSON.stringify(overlay, null, 2));
2246
+ console.error(`overlay written: ${overlayPath2}`);
2247
+ } else {
2248
+ console.log(prompt);
2249
+ }
2250
+ } else {
2251
+ const proc = spawnSync(["sh", "-c", cmd], { input: prompt });
2252
+ const raw = proc.stdout.trim();
2253
+ const jsonStart = raw.indexOf("{");
2254
+ const namesJson = JSON.parse(raw.slice(jsonStart));
2255
+ const overlay = await loadOverlay(overlayPath2);
2256
+ for (const [mod, v] of Object.entries(namesJson)) {
2257
+ if (overlay.modules[mod]?.name)
2258
+ continue;
2259
+ overlay.modules[mod] = { ...overlay.modules[mod], name: v.name };
2260
+ overlay.modules[mod].desc = v.desc;
2261
+ }
2262
+ fs9.writeFileSync(overlayPath2, JSON.stringify(overlay, null, 2));
2263
+ console.error(`overlay written: ${overlayPath2}`);
2264
+ }
2265
+ });
2266
+
2267
+ // src/bin.ts
2268
+ var ROUTES = {
2269
+ index: () => Promise.resolve().then(() => (init_cli(), exports_cli)),
2270
+ impact: () => Promise.resolve().then(() => (init_impact_cli(), exports_impact_cli)),
2271
+ change: () => init_change_cli().then(() => exports_change_cli),
2272
+ archmap: () => init_archmap_html().then(() => exports_archmap_html),
2273
+ mermaid: () => Promise.resolve().then(() => (init_archmap(), exports_archmap)),
2274
+ cochange: () => Promise.resolve().then(() => (init_cochange(), exports_cochange)),
2275
+ "pr-comment": () => init_pr_comment().then(() => exports_pr_comment),
2276
+ demo: () => Promise.resolve().then(() => (init_demo(), exports_demo)),
2277
+ "name-modules": () => init_name_modules().then(() => exports_name_modules)
2278
+ };
2279
+ var [cmd2, ...rest] = process.argv.slice(2);
2280
+ if (!cmd2 || cmd2 === "--help" || cmd2 === "-h" || !ROUTES[cmd2]) {
2281
+ console.log(`codeblast — deterministic code graph: architecture, change & impact maps
2282
+
2283
+ usage: codeblast <command> [args]
2284
+
2285
+ index <repo> [--db graph.db] build/update the graph (incremental)
2286
+ impact <graph.db> <symbol> [--json] blast radius of a change
2287
+ change <repo> <ref-a> <ref-b> [--json] structural diff between two refs
2288
+ archmap <graph.db> --out arch.html interactive architecture map
2289
+ [--impact <sym>] [--diff <base.db>] ...with impact / change overlay
2290
+ mermaid <graph.db> module map as mermaid
2291
+ cochange <repo> <graph.db> mine git history coupling
2292
+ pr-comment <repo> <base-sha> <head-sha> PR review comment (silent if no change)
2293
+ demo [repo] build + query + map in one shot
2294
+
2295
+ docs: https://github.com/alloevil/codeblast · demos: https://alloevil.github.io/codeblast/`);
2296
+ process.exit(cmd2 && !ROUTES[cmd2] ? 1 : 0);
2297
+ }
2298
+ process.argv.splice(2, 1);
2299
+ await ROUTES[cmd2]();