@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.
- package/CHANGELOG.md +17 -0
- package/cli/dve-tool.ts +734 -0
- package/config.ts +64 -0
- package/context/bundle.ts +142 -0
- package/graph/builder.ts +254 -0
- package/graph/cluster.ts +105 -0
- package/graph/query.ts +82 -0
- package/graph/schema.ts +169 -0
- package/install.sh +78 -0
- package/package.json +29 -0
- package/parser/annotation-parser.ts +77 -0
- package/parser/decision-parser.ts +104 -0
- package/parser/drift-detector.ts +45 -0
- package/parser/git-linker.ts +62 -0
- package/parser/glossary-builder.ts +116 -0
- package/parser/session-parser.ts +213 -0
- package/parser/spec-parser.ts +65 -0
- package/parser/state-detector.ts +379 -0
- package/scripts/audit-duplicates.sh +101 -0
- package/scripts/discover-decisions.sh +129 -0
- package/scripts/recover-all.sh +150 -0
- package/scripts/recover-dialogues.sh +190 -0
- package/server/api.ts +297 -0
- package/server/slack.ts +217 -0
- package/skills/dve-annotate.md +26 -0
- package/skills/dve-build.md +15 -0
- package/skills/dve-context.md +22 -0
- package/skills/dve-serve.md +17 -0
- package/skills/dve-status.md +18 -0
- package/skills/dve-trace.md +16 -0
- package/tsconfig.json +15 -0
- package/update.sh +73 -0
- package/version.txt +1 -0
package/cli/dve-tool.ts
ADDED
|
@@ -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
|
+
}
|