@unlaxer/dve-toolkit 4.1.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.
@@ -0,0 +1,734 @@
1
+ #!/usr/bin/env node
2
+ // dve-tool — DVE CLI
3
+
4
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { buildGraph } from "../graph/builder.js";
7
+ import { traceDecision, impactOf, orphanGaps, search } from "../graph/query.js";
8
+ import { generateBundle } from "../context/bundle.js";
9
+ import { loadConfig, singleProjectConfig, resolveProjectDirs } from "../config.js";
10
+ import { startAPIServer } from "../server/api.js";
11
+ import { clusterBySupersedes } from "../graph/cluster.js";
12
+ import { detectDrift } from "../parser/drift-detector.js";
13
+ import { detectProjectState } from "../parser/state-detector.js";
14
+ import type { DVEGraph, MultiProjectGraph, Changelog, Gap } from "../graph/schema.js";
15
+
16
+ const CWD = process.cwd();
17
+ const CONFIG_PATH = path.join(CWD, "dve.config.json");
18
+ const config = loadConfig(CONFIG_PATH) ?? singleProjectConfig(CWD);
19
+ const DIST_DIR = path.resolve(config.outputDir.startsWith("/") ? config.outputDir : path.join(CWD, config.outputDir));
20
+
21
+ function annDir(projectPath?: string) {
22
+ return path.join(projectPath ?? CWD, "dve", "annotations");
23
+ }
24
+ function ctxDir(projectPath?: string) {
25
+ return path.join(projectPath ?? CWD, "dve", "contexts");
26
+ }
27
+
28
+ function loadGraph(projectName?: string): DVEGraph {
29
+ const filename = projectName ? `graph-${projectName}.json` : "graph.json";
30
+ const p = path.join(DIST_DIR, filename);
31
+ // Fallback to graph.json for single project
32
+ const fallback = path.join(DIST_DIR, "graph.json");
33
+ const target = existsSync(p) ? p : fallback;
34
+ if (!existsSync(target)) {
35
+ console.error("graph.json not found. Run `dve build` first.");
36
+ process.exit(1);
37
+ }
38
+ return JSON.parse(readFileSync(target, "utf-8"));
39
+ }
40
+
41
+ function buildChangelog(prev: DVEGraph | null, curr: DVEGraph): Changelog | null {
42
+ if (!prev) return null;
43
+ const prevIds = new Set(prev.nodes.map((n) => n.id));
44
+ const currIds = new Set(curr.nodes.map((n) => n.id));
45
+ const changelog: Changelog = {
46
+ since: prev.generated_at,
47
+ new_nodes: curr.nodes.filter((n) => !prevIds.has(n.id)).map((n) => n.id),
48
+ removed_nodes: prev.nodes.filter((n) => !currIds.has(n.id)).map((n) => n.id),
49
+ changed_statuses: [],
50
+ };
51
+ for (const node of curr.nodes) {
52
+ if (node.type === "decision") {
53
+ const prevNode = prev.nodes.find((n) => n.id === node.id);
54
+ if (prevNode && (prevNode.data as any).status !== (node.data as any).status) {
55
+ changelog.changed_statuses.push({
56
+ id: node.id, from: (prevNode.data as any).status, to: (node.data as any).status,
57
+ });
58
+ }
59
+ }
60
+ }
61
+ return changelog;
62
+ }
63
+
64
+ function printBuildReport(name: string, graph: DVEGraph, changelog: Changelog | null, elapsed: string) {
65
+ const s = graph.stats;
66
+ const unknownSev = graph.nodes.filter((n) => n.type === "gap" && (n.data as Gap).severity === "Unknown").length;
67
+ const noMarkers = graph.warnings.filter((w) => w.message.includes("No gap markers")).length;
68
+
69
+ console.log(`\n [${name}] (${elapsed}s):`);
70
+ console.log(` Sessions: ${s.sessions}${noMarkers ? ` (${s.sessions - noMarkers} with gaps, ${noMarkers} no markers)` : ""}`);
71
+ console.log(` Gaps: ${s.gaps}${unknownSev ? ` (${unknownSev} severity unknown)` : ""}`);
72
+ console.log(` Decisions: ${s.decisions}`);
73
+ if (s.specs) console.log(` Specs: ${s.specs}`);
74
+ console.log(` Annotations: ${s.annotations}`);
75
+ if (graph.warnings.length > 0) console.log(` Warnings: ${graph.warnings.length}`);
76
+ if (changelog && changelog.new_nodes.length > 0) {
77
+ console.log(` New: ${changelog.new_nodes.length} nodes`);
78
+ }
79
+ }
80
+
81
+ // ─── build ───
82
+
83
+ function build() {
84
+ const startAll = Date.now();
85
+ mkdirSync(DIST_DIR, { recursive: true });
86
+
87
+ const isMulti = config.projects.length > 1;
88
+ const multiGraph: MultiProjectGraph = {
89
+ version: "1.0.0",
90
+ generated_at: new Date().toISOString(),
91
+ projects: [],
92
+ };
93
+
94
+ console.log(`\nDVE build — ${config.projects.length} project(s)`);
95
+
96
+ for (const project of config.projects) {
97
+ const start = Date.now();
98
+ const dirs = resolveProjectDirs(project);
99
+ const graph = buildGraph({ ...dirs, enableGitLinker: true });
100
+
101
+ const graphFile = isMulti
102
+ ? path.join(DIST_DIR, `graph-${project.name}.json`)
103
+ : path.join(DIST_DIR, "graph.json");
104
+
105
+ // Changelog
106
+ const prev = existsSync(graphFile)
107
+ ? JSON.parse(readFileSync(graphFile, "utf-8")) as DVEGraph
108
+ : null;
109
+ const changelog = buildChangelog(prev, graph);
110
+
111
+ writeFileSync(graphFile, JSON.stringify(graph, null, 2));
112
+ if (changelog) {
113
+ const clFile = isMulti
114
+ ? path.join(DIST_DIR, `changelog-${project.name}.json`)
115
+ : path.join(DIST_DIR, "changelog.json");
116
+ writeFileSync(clFile, JSON.stringify(changelog, null, 2));
117
+ }
118
+
119
+ multiGraph.projects.push({ name: project.name, path: project.path, graph });
120
+ printBuildReport(project.name, graph, changelog, ((Date.now() - start) / 1000).toFixed(1));
121
+ }
122
+
123
+ // Write multi-project index
124
+ if (isMulti) {
125
+ const index = {
126
+ version: "1.0.0",
127
+ generated_at: multiGraph.generated_at,
128
+ projects: multiGraph.projects.map((p) => ({
129
+ name: p.name,
130
+ path: p.path,
131
+ graphFile: `graph-${p.name}.json`,
132
+ stats: p.graph.stats,
133
+ })),
134
+ };
135
+ writeFileSync(path.join(DIST_DIR, "projects.json"), JSON.stringify(index, null, 2));
136
+ // Also write combined graph.json for backwards compat (merge all)
137
+ const merged: DVEGraph = {
138
+ version: "1.0.0",
139
+ generated_at: multiGraph.generated_at,
140
+ stats: { sessions: 0, gaps: 0, decisions: 0, annotations: 0, specs: 0 },
141
+ nodes: [],
142
+ edges: [],
143
+ warnings: [],
144
+ glossary: [],
145
+ };
146
+ for (const p of multiGraph.projects) {
147
+ // Prefix node IDs with project name to avoid collisions
148
+ for (const node of p.graph.nodes) {
149
+ merged.nodes.push({ ...node, id: `${p.name}/${node.id}` });
150
+ }
151
+ for (const edge of p.graph.edges) {
152
+ merged.edges.push({ ...edge, source: `${p.name}/${edge.source}`, target: `${p.name}/${edge.target}` });
153
+ }
154
+ merged.warnings.push(...p.graph.warnings);
155
+ merged.stats.sessions += p.graph.stats.sessions;
156
+ merged.stats.gaps += p.graph.stats.gaps;
157
+ merged.stats.decisions += p.graph.stats.decisions;
158
+ merged.stats.annotations += p.graph.stats.annotations;
159
+ merged.stats.specs = (merged.stats.specs ?? 0) + (p.graph.stats.specs ?? 0);
160
+ // Merge glossary (deduplicate by term)
161
+ const existingTerms = new Set((merged.glossary ?? []).map((e: any) => e.term.toLowerCase()));
162
+ for (const entry of p.graph.glossary ?? []) {
163
+ if (!existingTerms.has(entry.term.toLowerCase())) {
164
+ (merged.glossary as any[]).push(entry);
165
+ existingTerms.add(entry.term.toLowerCase());
166
+ }
167
+ }
168
+ }
169
+ writeFileSync(path.join(DIST_DIR, "graph.json"), JSON.stringify(merged, null, 2));
170
+ }
171
+
172
+ const totalElapsed = ((Date.now() - startAll) / 1000).toFixed(1);
173
+ console.log(`\n Total: ${totalElapsed}s → ${DIST_DIR}`);
174
+
175
+ return isMulti ? null : multiGraph.projects[0]?.graph ?? null;
176
+ }
177
+
178
+ // ─── serve ───
179
+
180
+ async function serve(watch: boolean) {
181
+ const { execSync, spawn } = await import("child_process");
182
+ const appDir = path.join(CWD, "dve", "app");
183
+
184
+ if (!existsSync(path.join(DIST_DIR, "graph.json"))) {
185
+ console.log("graph.json not found, building...");
186
+ build();
187
+ }
188
+
189
+ if (!existsSync(path.join(appDir, "node_modules"))) {
190
+ console.log("Installing app dependencies...");
191
+ execSync("npm install", { cwd: appDir, stdio: "inherit" });
192
+ }
193
+
194
+ // Build app if dist/index.html doesn't exist
195
+ if (!existsSync(path.join(DIST_DIR, "index.html"))) {
196
+ console.log("Building Web UI...");
197
+ execSync("npx vite build", { cwd: appDir, stdio: "inherit" });
198
+ }
199
+
200
+ if (watch) {
201
+ console.log("\nWatching for changes...");
202
+ const chokidar = await import("chokidar");
203
+ const dirs: string[] = [];
204
+ for (const project of config.projects) {
205
+ const d = resolveProjectDirs(project);
206
+ dirs.push(d.sessionsDir, d.decisionsDir, d.annotationsDir);
207
+ }
208
+
209
+ let debounce: ReturnType<typeof setTimeout> | null = null;
210
+ const watcher = chokidar.watch(dirs, {
211
+ ignoreInitial: true,
212
+ ignored: /(^|[\/\\])\../,
213
+ });
214
+
215
+ watcher.on("all", (event: string, filePath: string) => {
216
+ if (!filePath.endsWith(".md")) return;
217
+ if (debounce) clearTimeout(debounce);
218
+ debounce = setTimeout(() => {
219
+ console.log(`\n [${event}] ${path.relative(CWD, filePath)}`);
220
+ build();
221
+ }, 500);
222
+ });
223
+ }
224
+
225
+ // Start API server (annotations, drift, coverage)
226
+ startAPIServer({
227
+ annotationsDir: annDir(),
228
+ distDir: DIST_DIR,
229
+ projectDirs: config.projects.map((p) => ({
230
+ name: p.name,
231
+ path: p.path,
232
+ decisionsDir: path.join(p.path, p.decisionsDir),
233
+ })),
234
+ }, 4174);
235
+
236
+ // Serve with vite preview
237
+ console.log(`\nStarting UI server...`);
238
+ const vite = spawn("npx", ["vite", "preview", "--host", "0.0.0.0", "--port", "4173"], {
239
+ cwd: appDir,
240
+ stdio: "inherit",
241
+ });
242
+ vite.on("close", (code) => process.exit(code ?? 0));
243
+ }
244
+
245
+ // ─── trace ───
246
+
247
+ function trace(ddId: string) {
248
+ const graph = loadGraph();
249
+ const chain = traceDecision(graph, ddId);
250
+
251
+ if (chain.length === 0) {
252
+ console.error(`${ddId} not found in graph.`);
253
+ process.exit(1);
254
+ }
255
+
256
+ console.log(`\nTrace: ${ddId}\n`);
257
+ for (const node of chain) {
258
+ const data = node.data as any;
259
+ switch (node.type) {
260
+ case "decision":
261
+ console.log(` DD ${node.id}: ${data.title} (${data.date}) [${data.status}]`);
262
+ break;
263
+ case "gap":
264
+ console.log(` ← Gap ${node.id}: ${data.summary} (${data.severity})`);
265
+ break;
266
+ case "session":
267
+ console.log(` ← Session: ${node.id} (${(data.characters ?? []).join(", ")})`);
268
+ break;
269
+ default:
270
+ console.log(` ← ${node.type}: ${node.id}`);
271
+ }
272
+ }
273
+ }
274
+
275
+ // ─── impact ───
276
+
277
+ function impact(nodeId: string) {
278
+ const graph = loadGraph();
279
+ const affected = impactOf(graph, nodeId);
280
+
281
+ if (affected.length === 0) {
282
+ console.log(`\nNo downstream impact from ${nodeId}.`);
283
+ return;
284
+ }
285
+
286
+ console.log(`\nImpact of ${nodeId} (${affected.length} affected):\n`);
287
+ for (const node of affected) {
288
+ const data = node.data as any;
289
+ const label = data.title ?? data.summary ?? data.theme ?? "";
290
+ console.log(` ${node.type.padEnd(10)} ${node.id}: ${label}`);
291
+ }
292
+ }
293
+
294
+ // ─── orphans ───
295
+
296
+ function showOrphans() {
297
+ const graph = loadGraph();
298
+ const orphans = orphanGaps(graph);
299
+
300
+ if (orphans.length === 0) {
301
+ console.log("\nNo orphan gaps. All gaps are linked to decisions.");
302
+ return;
303
+ }
304
+
305
+ console.log(`\nOrphan gaps (${orphans.length} — no decision linked):\n`);
306
+ for (const gap of orphans) {
307
+ const data = gap.data as Gap;
308
+ console.log(` ${gap.id}: ${data.summary} (${data.severity})`);
309
+ }
310
+ }
311
+
312
+ // ─── search ───
313
+
314
+ function doSearch(keyword: string) {
315
+ const graph = loadGraph();
316
+ const results = search(graph, keyword);
317
+
318
+ if (results.length === 0) {
319
+ console.log(`\nNo results for "${keyword}".`);
320
+ return;
321
+ }
322
+
323
+ console.log(`\nSearch "${keyword}" (${results.length} results):\n`);
324
+ for (const node of results) {
325
+ const data = node.data as any;
326
+ const label = data.title ?? data.theme ?? data.summary ?? data.body?.slice(0, 60) ?? "";
327
+ console.log(` ${node.type.padEnd(10)} ${node.id}: ${label}`);
328
+ }
329
+ }
330
+
331
+ // ─── annotate ───
332
+
333
+ function annotate(targetId: string, action: string, body: string) {
334
+ const ANN_DIR = annDir();
335
+ mkdirSync(ANN_DIR, { recursive: true });
336
+
337
+ const existing = existsSync(ANN_DIR)
338
+ ? readdirSync(ANN_DIR).filter((f) => f.endsWith(".md")).length
339
+ : 0;
340
+ const annNum = String(existing + 1).padStart(3, "0");
341
+ const slug = targetId.replace(/[^a-zA-Z0-9-]/g, "_");
342
+ const filename = `${annNum}-${slug}-${action}.md`;
343
+ const filePath = path.join(ANN_DIR, filename);
344
+
345
+ const content = `---
346
+ target: ${targetId}
347
+ action: ${action}
348
+ date: ${new Date().toISOString().split("T")[0]}
349
+ ---
350
+
351
+ ${body}
352
+ `;
353
+
354
+ writeFileSync(filePath, content);
355
+ console.log(`\nAnnotation created: ${filePath}`);
356
+ }
357
+
358
+ // ─── context ───
359
+
360
+ function context(originId: string, constraints: string[]) {
361
+ const graph = loadGraph();
362
+ const bundle = generateBundle({
363
+ graph,
364
+ originId,
365
+ constraints,
366
+ outputDir: ctxDir(),
367
+ });
368
+
369
+ console.log(`\nContextBundle generated.`);
370
+ console.log(` Action: ${bundle.suggested_action}`);
371
+ console.log(` Theme: ${bundle.summary.theme}`);
372
+ console.log(`\n--- prompt (copy to DGE) ---\n`);
373
+ console.log(bundle.prompt_template);
374
+ console.log(`\n---`);
375
+ }
376
+
377
+ // ─── Main ───
378
+
379
+ const [cmd, ...args] = process.argv.slice(2);
380
+
381
+ switch (cmd) {
382
+ case "build":
383
+ build();
384
+ break;
385
+ case "serve":
386
+ serve(args.includes("--watch") || args.includes("-w"));
387
+ break;
388
+ case "trace":
389
+ if (!args[0]) { console.error("Usage: dve trace <DD-id>"); process.exit(1); }
390
+ trace(args[0]);
391
+ break;
392
+ case "impact":
393
+ if (!args[0]) { console.error("Usage: dve impact <node-id>"); process.exit(1); }
394
+ impact(args[0]);
395
+ break;
396
+ case "orphans":
397
+ showOrphans();
398
+ break;
399
+ case "search":
400
+ if (!args[0]) { console.error("Usage: dve search <keyword>"); process.exit(1); }
401
+ doSearch(args.join(" "));
402
+ break;
403
+ case "annotate": {
404
+ const target = args[0];
405
+ const actionFlag = args.find((a) => a.startsWith("--action="))?.split("=")[1]
406
+ ?? args[args.indexOf("--action") + 1]
407
+ ?? "comment";
408
+ const bodyFlag = args.find((a) => a.startsWith("--body="))?.split("=")[1]
409
+ ?? args[args.indexOf("--body") + 1]
410
+ ?? "";
411
+ if (!target || !bodyFlag) {
412
+ console.error('Usage: dve annotate <target-id> --action <type> --body "text"');
413
+ process.exit(1);
414
+ }
415
+ annotate(target, actionFlag, bodyFlag);
416
+ break;
417
+ }
418
+ case "context": {
419
+ const origin = args[0];
420
+ const constraintArgs = args.filter((a) => a.startsWith("--constraint=")).map((a) => a.split("=")[1]);
421
+ if (!origin) { console.error("Usage: dve context <node-id> [--constraint=...]"); process.exit(1); }
422
+ context(origin, constraintArgs);
423
+ break;
424
+ }
425
+ case "status": {
426
+ const DRE_ICONS: Record<string, string> = {
427
+ FRESH: "\u{26AA}", INSTALLED: "\u{1F7E2}", CUSTOMIZED: "\u{1F7E1}",
428
+ OUTDATED: "\u{1F534}", UNKNOWN: "\u{2753}",
429
+ };
430
+
431
+ console.log(`\nDVE Project Status\n`);
432
+
433
+ for (const project of config.projects) {
434
+ const state = detectProjectState(project.name, project.path);
435
+ const dreIcon = DRE_ICONS[state.dre.installState] ?? "";
436
+ const wf = state.workflow;
437
+
438
+ console.log(`\u{250C}${"─".repeat(70)}`);
439
+ console.log(`\u{2502} ${state.projectName} ${dreIcon} DRE ${state.dre.installState}${state.dre.localVersion ? ` v${state.dre.localVersion}` : ""} Sessions:${state.dgeSessionCount} DDs:${state.ddCount}`);
440
+ console.log(`\u{2502}`);
441
+
442
+ // Workflow state machine
443
+ const flow = wf.phases.map((p) => {
444
+ const isActive = p.active;
445
+ const isPlugin = p.source === "plugin";
446
+ const label = isPlugin ? `${p.id} (${p.plugin})` : p.id;
447
+ if (isActive) return `[\u{25B6} ${label}]`;
448
+ return isPlugin ? `{${label}}` : label;
449
+ });
450
+ console.log(`\u{2502} ${flow.join(" → ")}`);
451
+
452
+ // Current state + sub-state
453
+ const subLabel = wf.subState ? ` > ${wf.subState}` : "";
454
+ console.log(`\u{2502} Current: ${wf.currentPhase}${subLabel} (${wf.currentSource})`);
455
+ if (wf.stack.length > 1) {
456
+ console.log(`\u{2502} Stack: ${wf.stack.join(" > ")}`);
457
+ }
458
+
459
+ // Plugin sub-states for current phase
460
+ for (const psm of wf.pluginSMs) {
461
+ if (psm.states.length > 0 && psm.phaseId === wf.currentPhase) {
462
+ const subFlow = psm.states.map((s) =>
463
+ s.active ? `[\u{25B6} ${s.id}]` : s.id
464
+ );
465
+ console.log(`\u{2502} Sub (${psm.plugin}): ${subFlow.join(" \u{2192} ")}`);
466
+ }
467
+ }
468
+
469
+ // Plugins
470
+ if (wf.plugins.length > 0) {
471
+ console.log(`\u{2502} Plugins: ${wf.plugins.map((p) => `${p.id}${p.version ? ` v${p.version}` : ""}`).join(", ")}`);
472
+ }
473
+
474
+ console.log(`\u{2514}${"─".repeat(70)}`);
475
+ }
476
+ break;
477
+ }
478
+ case "clusters": {
479
+ const graph = loadGraph();
480
+ const clusters = clusterBySupersedes(graph);
481
+ if (clusters.length === 0) {
482
+ console.log("\nNo clusters found.");
483
+ break;
484
+ }
485
+ console.log(`\nDecision clusters (${clusters.length}):\n`);
486
+ for (const c of clusters) {
487
+ console.log(` ${c.label} (${c.ddIds.length} DDs, ${c.gapCount} gaps)`);
488
+ for (const id of c.ddIds) {
489
+ const node = graph.nodes.find((n) => n.id === id);
490
+ console.log(` ${id}: ${(node?.data as any)?.title ?? ""}`);
491
+ }
492
+ }
493
+ break;
494
+ }
495
+ case "drift": {
496
+ const graph = loadGraph();
497
+ const ddNodes = graph.nodes.filter((n) => n.type === "decision");
498
+ const drifted = detectDrift(ddNodes, CWD);
499
+ if (drifted.length === 0) {
500
+ console.log("\nNo drift detected.");
501
+ break;
502
+ }
503
+ console.log(`\nPotential drift (${drifted.length} decisions):\n`);
504
+ for (const d of drifted) {
505
+ console.log(` ${d.ddId}: ${d.commitsSince} commits since ${d.ddDate}`);
506
+ console.log(` latest: ${d.latestCommit}`);
507
+ }
508
+ break;
509
+ }
510
+ case "scan": {
511
+ const scanDir = args[0] ? path.resolve(args[0]) : path.resolve(CWD, "..");
512
+ const maxDepth = parseInt(args.find((a) => a.startsWith("--depth="))?.split("=")[1] ?? "3", 10);
513
+ const autoRegister = args.includes("--register") || args.includes("-r");
514
+ const doAudit = args.includes("--audit") || args.includes("-a");
515
+
516
+ console.log(`\nScanning ${scanDir} (depth=${maxDepth})${doAudit ? " + audit" : ""}...\n`);
517
+
518
+ // Find git repos with DxE markers
519
+ const { readdirSync: rds, statSync: ss } = await import("node:fs");
520
+
521
+ interface ScanResult {
522
+ path: string;
523
+ name: string;
524
+ hasDGE: boolean;
525
+ hasDRE: boolean;
526
+ hasDVE: boolean;
527
+ hasDDE: boolean;
528
+ sessions: number;
529
+ decisions: number;
530
+ }
531
+
532
+ const results: ScanResult[] = [];
533
+
534
+ function scanRecursive(dir: string, depth: number) {
535
+ if (depth > maxDepth) return;
536
+ let entries: string[];
537
+ try { entries = rds(dir); } catch { return; }
538
+
539
+ // Check if this is a project root (has .git or package.json)
540
+ const isProject = entries.includes(".git") || entries.includes("package.json");
541
+ if (isProject) {
542
+ const hasDGE = existsSync(path.join(dir, "dge")) || existsSync(path.join(dir, ".claude", "skills", "dge-session.md"));
543
+ const hasDRE = existsSync(path.join(dir, ".claude", ".dre-version")) || existsSync(path.join(dir, "dre"));
544
+ const hasDVE = existsSync(path.join(dir, "dve")) || existsSync(path.join(dir, ".claude", "skills", "dve-build.md"));
545
+ const hasDDE = existsSync(path.join(dir, "dde"));
546
+
547
+ let sessions = 0;
548
+ let decisions = 0;
549
+ const sessDir = path.join(dir, "dge", "sessions");
550
+ const ddDir = path.join(dir, "dge", "decisions");
551
+ if (existsSync(sessDir)) {
552
+ try { sessions = rds(sessDir).filter((f: string) => f.endsWith(".md") && f !== "index.md").length; } catch {}
553
+ }
554
+ if (existsSync(ddDir)) {
555
+ try { decisions = rds(ddDir).filter((f: string) => f.endsWith(".md") && f !== "index.md").length; } catch {}
556
+ }
557
+
558
+ // Only include if has any DxE tooling
559
+ if (hasDGE || hasDRE || hasDVE || hasDDE || sessions > 0) {
560
+ results.push({
561
+ path: dir,
562
+ name: path.basename(dir),
563
+ hasDGE, hasDRE, hasDVE, hasDDE,
564
+ sessions, decisions,
565
+ });
566
+ }
567
+ }
568
+
569
+ // Recurse into subdirectories (skip common non-project dirs)
570
+ const skipDirs = new Set(["node_modules", ".git", "dist", "build", ".dre", ".claude", "dve", "dge", "dre", "dde"]);
571
+ for (const entry of entries) {
572
+ if (skipDirs.has(entry) || entry.startsWith(".")) continue;
573
+ const full = path.join(dir, entry);
574
+ try { if (ss(full).isDirectory()) scanRecursive(full, depth + 1); } catch {}
575
+ }
576
+ }
577
+
578
+ scanRecursive(scanDir, 0);
579
+
580
+ if (results.length === 0) {
581
+ console.log(" No DxE projects found.");
582
+ break;
583
+ }
584
+
585
+ console.log(`${"Project".padEnd(25)} ${"DGE".padEnd(5)} ${"DRE".padEnd(5)} ${"DVE".padEnd(5)} ${"DDE".padEnd(5)} ${"Sess".padEnd(6)} DDs`);
586
+ console.log("─".repeat(70));
587
+ for (const r of results) {
588
+ console.log(
589
+ `${r.name.padEnd(25)} ${(r.hasDGE ? "✅" : "—").padEnd(5)} ` +
590
+ `${(r.hasDRE ? "✅" : "—").padEnd(5)} ${(r.hasDVE ? "✅" : "—").padEnd(5)} ` +
591
+ `${(r.hasDDE ? "✅" : "—").padEnd(5)} ${String(r.sessions).padEnd(6)} ${r.decisions}`
592
+ );
593
+ }
594
+ console.log(`\n ${results.length} projects found.`);
595
+
596
+ // Auto-register
597
+ if (autoRegister) {
598
+ const newConfig = {
599
+ outputDir: config.outputDir.startsWith("/") ? config.outputDir : path.relative(CWD, path.resolve(CWD, config.outputDir)) || "dve/dist",
600
+ projects: results.map((r) => ({
601
+ name: r.name,
602
+ path: path.relative(CWD, r.path) || ".",
603
+ })),
604
+ };
605
+ writeFileSync(CONFIG_PATH, JSON.stringify(newConfig, null, 2) + "\n");
606
+ console.log(`\n Registered ${results.length} projects to ${CONFIG_PATH}`);
607
+ console.log(` Run: dve build`);
608
+ } else {
609
+ console.log(`\n Add --register (-r) to save to dve.config.json`);
610
+ }
611
+
612
+ // Audit: detect self-implementations that overlap with DxE toolkits
613
+ if (doAudit) {
614
+ console.log(`\n${"═".repeat(70)}`);
615
+ console.log(" AUDIT: Self-implementation overlap detection\n");
616
+
617
+ const CAPABILITIES: [string[], string][] = [
618
+ [["glossary-linker", "GlossaryLinker", "auto-link"], "DDE dde-link (npx dde-link --fix)"],
619
+ [["state-machine.yaml", "state_machine", "workflow-engine"], "DRE workflow engine (dre-engine init)"],
620
+ [["gap-extract", "gap_extract", "design-review"], "DGE session (dge-session skill)"],
621
+ [["decision-vis", "decision_vis"], "DVE (dve build + dve serve)"],
622
+ ];
623
+
624
+ let totalFindings = 0;
625
+ for (const r of results) {
626
+ let projFindings = 0;
627
+
628
+ for (const [patterns, toolkit] of CAPABILITIES) {
629
+ for (const pat of patterns) {
630
+ try {
631
+ const { execSync: ex } = await import("child_process");
632
+ const found = ex(
633
+ `find "${r.path}" -path "*/node_modules" -prune -o -path "*/.git" -prune -o -path "*/dge" -prune -o -path "*/dre" -prune -o -path "*/dve" -prune -o -path "*/dde" -prune -o -name "*${pat}*" -print 2>/dev/null | head -3`,
634
+ { encoding: "utf-8", timeout: 5000 }
635
+ ).trim();
636
+ if (found) {
637
+ if (projFindings === 0) console.log(` ${r.name}:`);
638
+ for (const f of found.split("\n")) {
639
+ console.log(` ⚠️ ${f.replace(r.path + "/", "")} → ${toolkit}`);
640
+ }
641
+ projFindings++;
642
+ }
643
+ } catch {}
644
+ }
645
+ }
646
+
647
+ // Version check
648
+ const dxeHome = path.resolve(import.meta.url.replace("file://", "").replace("/dist/cli/dve-tool.js", ""), "..", "..");
649
+ const versionChecks = [
650
+ ["DGE", path.join(r.path, "dge", "version.txt"), path.join(dxeHome, "dge", "kit", "version.txt")],
651
+ ["DRE", path.join(r.path, ".claude", ".dre-version"), path.join(dxeHome, "dre", "kit", "version.txt")],
652
+ ];
653
+ for (const [name, localV, kitV] of versionChecks) {
654
+ if (existsSync(localV) && existsSync(kitV)) {
655
+ const lv = readFileSync(localV, "utf-8").trim();
656
+ const kv = readFileSync(kitV, "utf-8").trim();
657
+ if (lv !== kv && lv && kv) {
658
+ if (projFindings === 0) console.log(` ${r.name}:`);
659
+ console.log(` 📦 ${name}: ${lv} → ${kv} available`);
660
+ projFindings++;
661
+ }
662
+ }
663
+ }
664
+
665
+ totalFindings += projFindings;
666
+ }
667
+
668
+ if (totalFindings === 0) {
669
+ console.log(" ✅ No duplicates or outdated toolkits found.");
670
+ } else {
671
+ console.log(`\n ${totalFindings} finding(s) across ${results.length} projects`);
672
+ }
673
+ }
674
+ break;
675
+ }
676
+ case "init": {
677
+ if (existsSync(CONFIG_PATH)) {
678
+ console.log(`dve.config.json already exists.`);
679
+ process.exit(0);
680
+ }
681
+ const initProjects = args.length > 0
682
+ ? args.map((p) => ({ name: path.basename(p), path: path.resolve(p) }))
683
+ : [{ name: path.basename(CWD), path: CWD }];
684
+ const initConfig = {
685
+ outputDir: "dve/dist",
686
+ projects: initProjects.map((p) => ({
687
+ name: p.name,
688
+ path: path.relative(CWD, p.path) || ".",
689
+ })),
690
+ };
691
+ writeFileSync(CONFIG_PATH, JSON.stringify(initConfig, null, 2) + "\n");
692
+ console.log(`\nCreated ${CONFIG_PATH}`);
693
+ console.log(` Projects: ${initProjects.map((p) => p.name).join(", ")}`);
694
+ console.log(`\nEdit to add more projects, then run: dve build`);
695
+ break;
696
+ }
697
+ case "projects": {
698
+ console.log(`\nDVE projects (${config.projects.length}):\n`);
699
+ for (const p of config.projects) {
700
+ console.log(` ${p.name.padEnd(20)} ${p.path}`);
701
+ }
702
+ break;
703
+ }
704
+ case "version":
705
+ console.log("DVE toolkit v4.0.0");
706
+ break;
707
+ default:
708
+ console.log(`
709
+ DVE — Decision Visualization Engine
710
+
711
+ Commands:
712
+ init [path...] Create dve.config.json (multi-project)
713
+ projects List configured projects
714
+ build Build graph.json from all projects
715
+ serve [--watch] Start web UI (with optional file watching)
716
+ trace <DD-id> Trace decision back to sessions/gaps
717
+ impact <node-id> Show downstream impact of a node
718
+ orphans Show gaps not linked to any decision
719
+ search <keyword> Search nodes by keyword
720
+ annotate <id> --action <type> --body "text"
721
+ Create annotation
722
+ context <id> [--constraint=...] Generate ContextBundle for DGE restart
723
+ status Show DRE state + dev phase per project
724
+ clusters Show decision clusters (supersedes chains)
725
+ drift Detect decisions that may have diverged
726
+ version Show version
727
+
728
+ Multi-project:
729
+ scan [dir] [--depth=N] [-r] Auto-discover DxE projects in directory
730
+ init [path...] Create dve.config.json manually
731
+ build → builds all projects
732
+ projects → list projects
733
+ `);
734
+ }