@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/server/api.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// DVE lightweight API server — annotation write + drift detection
|
|
2
|
+
// Runs alongside vite preview, not a separate process
|
|
3
|
+
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type { DVEGraph } from "../graph/schema.js";
|
|
9
|
+
import { detectProjectState } from "../parser/state-detector.js";
|
|
10
|
+
import { handleSlashCommand, handleEvent } from "./slack.js";
|
|
11
|
+
|
|
12
|
+
interface APIConfig {
|
|
13
|
+
annotationsDir: string;
|
|
14
|
+
distDir: string;
|
|
15
|
+
projectDirs: { name: string; path: string; decisionsDir: string }[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseBody(req: any): Promise<any> {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
let body = "";
|
|
21
|
+
req.on("data", (chunk: string) => (body += chunk));
|
|
22
|
+
req.on("end", () => {
|
|
23
|
+
try { resolve(JSON.parse(body)); }
|
|
24
|
+
catch { resolve({}); }
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cors(res: any) {
|
|
30
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
31
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
32
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function json(res: any, data: any, status = 200) {
|
|
36
|
+
cors(res);
|
|
37
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
38
|
+
res.end(JSON.stringify(data));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function startAPIServer(config: APIConfig, port = 4174) {
|
|
42
|
+
const server = createServer(async (req, res) => {
|
|
43
|
+
cors(res);
|
|
44
|
+
if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
|
|
45
|
+
|
|
46
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
47
|
+
|
|
48
|
+
// POST /api/annotations — create annotation
|
|
49
|
+
if (req.method === "POST" && url.pathname === "/api/annotations") {
|
|
50
|
+
const body = await parseBody(req);
|
|
51
|
+
const { target, action, author, body: text } = body;
|
|
52
|
+
if (!target || !text) {
|
|
53
|
+
return json(res, { error: "target and body required" }, 400);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mkdirSync(config.annotationsDir, { recursive: true });
|
|
57
|
+
const existing = existsSync(config.annotationsDir)
|
|
58
|
+
? readdirSync(config.annotationsDir).filter((f) => f.endsWith(".md")).length
|
|
59
|
+
: 0;
|
|
60
|
+
const annNum = String(existing + 1).padStart(3, "0");
|
|
61
|
+
const slug = target.replace(/[^a-zA-Z0-9-]/g, "_");
|
|
62
|
+
const filename = `${annNum}-${slug}-${action ?? "comment"}.md`;
|
|
63
|
+
const filePath = path.join(config.annotationsDir, filename);
|
|
64
|
+
|
|
65
|
+
const content = `---
|
|
66
|
+
target: ${target}
|
|
67
|
+
action: ${action ?? "comment"}
|
|
68
|
+
date: ${new Date().toISOString().split("T")[0]}
|
|
69
|
+
author: ${author ?? ""}
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
${text}
|
|
73
|
+
`;
|
|
74
|
+
writeFileSync(filePath, content);
|
|
75
|
+
return json(res, { ok: true, file: filename });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// GET /api/drift — detect decisions that may have drifted
|
|
79
|
+
if (req.method === "GET" && url.pathname === "/api/drift") {
|
|
80
|
+
const drifted: { dd: string; file: string; lastModified: string; ddDate: string }[] = [];
|
|
81
|
+
|
|
82
|
+
for (const proj of config.projectDirs) {
|
|
83
|
+
const graphPath = path.join(config.distDir, `graph-${proj.name}.json`);
|
|
84
|
+
const fallback = path.join(config.distDir, "graph.json");
|
|
85
|
+
const gp = existsSync(graphPath) ? graphPath : fallback;
|
|
86
|
+
if (!existsSync(gp)) continue;
|
|
87
|
+
|
|
88
|
+
const graph: DVEGraph = JSON.parse(readFileSync(gp, "utf-8"));
|
|
89
|
+
const ddNodes = graph.nodes.filter((n) => n.type === "decision");
|
|
90
|
+
|
|
91
|
+
for (const dd of ddNodes) {
|
|
92
|
+
const data = dd.data as any;
|
|
93
|
+
if (!data.file_path || data.status === "overturned") continue;
|
|
94
|
+
|
|
95
|
+
// Check git: files changed since DD date
|
|
96
|
+
try {
|
|
97
|
+
const since = data.date || "2020-01-01";
|
|
98
|
+
const log = execSync(
|
|
99
|
+
`git log --oneline --since="${since}" -- .`,
|
|
100
|
+
{ cwd: proj.path, encoding: "utf-8", timeout: 5000 }
|
|
101
|
+
).trim();
|
|
102
|
+
if (log.split("\n").length > 5) {
|
|
103
|
+
// Many commits since DD — potential drift
|
|
104
|
+
const ddFile = path.join(proj.path, data.file_path);
|
|
105
|
+
if (existsSync(ddFile)) {
|
|
106
|
+
const stat = statSync(ddFile);
|
|
107
|
+
drifted.push({
|
|
108
|
+
dd: dd.id,
|
|
109
|
+
file: data.file_path,
|
|
110
|
+
lastModified: stat.mtime.toISOString(),
|
|
111
|
+
ddDate: since,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch { /* git not available */ }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return json(res, { drifted });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// GET /api/coverage — character coverage analysis
|
|
123
|
+
if (req.method === "GET" && url.pathname === "/api/coverage") {
|
|
124
|
+
const graphPath = path.join(config.distDir, "graph.json");
|
|
125
|
+
if (!existsSync(graphPath)) return json(res, { error: "graph.json not found" }, 404);
|
|
126
|
+
|
|
127
|
+
const graph: DVEGraph = JSON.parse(readFileSync(graphPath, "utf-8"));
|
|
128
|
+
const charGaps: Record<string, { total: number; critical: number; high: number }> = {};
|
|
129
|
+
|
|
130
|
+
// Count gaps per session's characters
|
|
131
|
+
const sessions = graph.nodes.filter((n) => n.type === "session");
|
|
132
|
+
for (const session of sessions) {
|
|
133
|
+
const chars = (session.data as any).characters ?? [];
|
|
134
|
+
const sessionGaps = graph.edges
|
|
135
|
+
.filter((e) => e.source === session.id && e.type === "discovers")
|
|
136
|
+
.map((e) => graph.nodes.find((n) => n.id === e.target))
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
|
|
139
|
+
for (const char of chars) {
|
|
140
|
+
if (!charGaps[char]) charGaps[char] = { total: 0, critical: 0, high: 0 };
|
|
141
|
+
charGaps[char].total += sessionGaps.length;
|
|
142
|
+
charGaps[char].critical += sessionGaps.filter((g) => (g!.data as any).severity === "Critical").length;
|
|
143
|
+
charGaps[char].high += sessionGaps.filter((g) => (g!.data as any).severity === "High").length;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return json(res, { coverage: charGaps });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// POST /api/scan — scan directory for DxE projects
|
|
151
|
+
if (req.method === "POST" && url.pathname === "/api/scan") {
|
|
152
|
+
const body = await parseBody(req);
|
|
153
|
+
const scanDir = body.dir ?? path.resolve(config.distDir, "..", "..", "..");
|
|
154
|
+
const maxDepth = body.depth ?? 3;
|
|
155
|
+
const results: any[] = [];
|
|
156
|
+
|
|
157
|
+
function scanRec(dir: string, depth: number) {
|
|
158
|
+
if (depth > maxDepth) return;
|
|
159
|
+
let entries: string[];
|
|
160
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
161
|
+
|
|
162
|
+
const isProject = entries.includes(".git") || entries.includes("package.json");
|
|
163
|
+
if (isProject) {
|
|
164
|
+
const hasDGE = existsSync(path.join(dir, "dge")) || existsSync(path.join(dir, ".claude", "skills", "dge-session.md"));
|
|
165
|
+
const hasDRE = existsSync(path.join(dir, ".claude", ".dre-version")) || existsSync(path.join(dir, "dre"));
|
|
166
|
+
const hasDVE = existsSync(path.join(dir, "dve")) || existsSync(path.join(dir, ".claude", "skills", "dve-build.md"));
|
|
167
|
+
const hasDDE = existsSync(path.join(dir, "dde"));
|
|
168
|
+
|
|
169
|
+
let sessions = 0, decisions = 0;
|
|
170
|
+
const sd = path.join(dir, "dge", "sessions");
|
|
171
|
+
const dd = path.join(dir, "dge", "decisions");
|
|
172
|
+
if (existsSync(sd)) try { sessions = readdirSync(sd).filter((f: string) => f.endsWith(".md") && f !== "index.md").length; } catch {}
|
|
173
|
+
if (existsSync(dd)) try { decisions = readdirSync(dd).filter((f: string) => f.endsWith(".md") && f !== "index.md").length; } catch {}
|
|
174
|
+
|
|
175
|
+
if (hasDGE || hasDRE || hasDVE || hasDDE || sessions > 0) {
|
|
176
|
+
results.push({ name: path.basename(dir), path: dir, hasDGE, hasDRE, hasDVE, hasDDE, sessions, decisions });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const skip = new Set(["node_modules", ".git", "dist", "build", ".dre", ".claude", "dve", "dge", "dre", "dde"]);
|
|
181
|
+
for (const e of entries) {
|
|
182
|
+
if (skip.has(e) || e.startsWith(".")) continue;
|
|
183
|
+
const full = path.join(dir, e);
|
|
184
|
+
try { if (statSync(full).isDirectory()) scanRec(full, depth + 1); } catch {}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
scanRec(scanDir, 0);
|
|
189
|
+
return json(res, { dir: scanDir, projects: results });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// POST /api/register — save scanned projects to dve.config.json
|
|
193
|
+
if (req.method === "POST" && url.pathname === "/api/register") {
|
|
194
|
+
const body = await parseBody(req);
|
|
195
|
+
const projects = body.projects ?? [];
|
|
196
|
+
const configPath = path.join(config.distDir, "..", "..", "dve.config.json");
|
|
197
|
+
const newConfig = {
|
|
198
|
+
outputDir: config.distDir,
|
|
199
|
+
projects: projects.map((p: any) => ({
|
|
200
|
+
name: p.name,
|
|
201
|
+
path: p.path,
|
|
202
|
+
})),
|
|
203
|
+
};
|
|
204
|
+
writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n");
|
|
205
|
+
return json(res, { ok: true, registered: projects.length, configPath });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// GET /api/status — project states (DRE + phase)
|
|
209
|
+
if (req.method === "GET" && url.pathname === "/api/status") {
|
|
210
|
+
const states = config.projectDirs.map((p) =>
|
|
211
|
+
detectProjectState(p.name, p.path)
|
|
212
|
+
);
|
|
213
|
+
return json(res, {
|
|
214
|
+
projects: states,
|
|
215
|
+
stateChart: {
|
|
216
|
+
phases: ["spec", "implementation", "stabilization", "maintenance"],
|
|
217
|
+
dreStates: ["FRESH", "INSTALLED", "CUSTOMIZED", "OUTDATED"],
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Slack Integration ───
|
|
223
|
+
|
|
224
|
+
// POST /api/slack/command — slash command handler
|
|
225
|
+
if (req.method === "POST" && url.pathname === "/api/slack/command") {
|
|
226
|
+
// Slack sends x-www-form-urlencoded, not JSON
|
|
227
|
+
const rawBody = await new Promise<string>((resolve) => {
|
|
228
|
+
let b = ""; req.on("data", (c: string) => b += c); req.on("end", () => resolve(b));
|
|
229
|
+
});
|
|
230
|
+
const params = new URLSearchParams(rawBody);
|
|
231
|
+
const text = params.get("text") ?? "";
|
|
232
|
+
const response = handleSlashCommand(text, config.distDir, config.projectDirs);
|
|
233
|
+
return json(res, response);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// POST /api/slack/events — Events API handler
|
|
237
|
+
if (req.method === "POST" && url.pathname === "/api/slack/events") {
|
|
238
|
+
const body = await parseBody(req);
|
|
239
|
+
|
|
240
|
+
// URL verification challenge
|
|
241
|
+
if (body.type === "url_verification") {
|
|
242
|
+
return json(res, { challenge: body.challenge });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Event callback
|
|
246
|
+
if (body.type === "event_callback" && body.event) {
|
|
247
|
+
const reply = handleEvent(body.event, config.distDir);
|
|
248
|
+
if (reply && body.event.channel) {
|
|
249
|
+
// Post reply via Slack API
|
|
250
|
+
const token = process.env.SLACK_BOT_TOKEN;
|
|
251
|
+
if (token) {
|
|
252
|
+
const fetch = globalThis.fetch ?? (await import("node:https")).request;
|
|
253
|
+
try {
|
|
254
|
+
await globalThis.fetch("https://slack.com/api/chat.postMessage", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
257
|
+
body: JSON.stringify({ channel: body.event.channel, text: reply }),
|
|
258
|
+
});
|
|
259
|
+
} catch { /* best effort */ }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return json(res, { ok: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return json(res, { ok: true });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// POST /api/slack/interactive — button clicks from Block Kit
|
|
269
|
+
if (req.method === "POST" && url.pathname === "/api/slack/interactive") {
|
|
270
|
+
const rawBody = await new Promise<string>((resolve) => {
|
|
271
|
+
let b = ""; req.on("data", (c: string) => b += c); req.on("end", () => resolve(b));
|
|
272
|
+
});
|
|
273
|
+
const params = new URLSearchParams(rawBody);
|
|
274
|
+
const payloadStr = params.get("payload") ?? "{}";
|
|
275
|
+
const payload = JSON.parse(payloadStr);
|
|
276
|
+
|
|
277
|
+
if (payload.type === "block_actions" && payload.actions?.[0]) {
|
|
278
|
+
const action = payload.actions[0];
|
|
279
|
+
const nodeId = action.value;
|
|
280
|
+
// Respond with trace/detail for the clicked node
|
|
281
|
+
const response = handleSlashCommand(`trace ${nodeId}`, config.distDir, config.projectDirs);
|
|
282
|
+
return json(res, response);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return json(res, { ok: true });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 404
|
|
289
|
+
json(res, { error: "Not found" }, 404);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
server.listen(port, "0.0.0.0", () => {
|
|
293
|
+
console.log(` DVE API: http://0.0.0.0:${port}`);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return server;
|
|
297
|
+
}
|
package/server/slack.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// DVE Slack Bot — slash commands + event handling
|
|
2
|
+
// Endpoints: POST /api/slack/command, POST /api/slack/events
|
|
3
|
+
|
|
4
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { traceDecision, impactOf, orphanGaps, search, overturned } from "../graph/query.js";
|
|
7
|
+
import { detectProjectState } from "../parser/state-detector.js";
|
|
8
|
+
import type { DVEGraph, Gap, Decision } from "../graph/schema.js";
|
|
9
|
+
|
|
10
|
+
function loadGraph(distDir: string, project?: string): DVEGraph | null {
|
|
11
|
+
const file = project
|
|
12
|
+
? path.join(distDir, `graph-${project}.json`)
|
|
13
|
+
: path.join(distDir, "graph.json");
|
|
14
|
+
if (!existsSync(file)) return null;
|
|
15
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Slash Command Handler ───
|
|
19
|
+
// /dve trace DD-003
|
|
20
|
+
// /dve orphans
|
|
21
|
+
// /dve status
|
|
22
|
+
// /dve search JWT
|
|
23
|
+
// /dve help
|
|
24
|
+
|
|
25
|
+
export function handleSlashCommand(
|
|
26
|
+
text: string,
|
|
27
|
+
distDir: string,
|
|
28
|
+
projectDirs: { name: string; path: string }[]
|
|
29
|
+
): { response_type: string; text: string; blocks?: any[] } {
|
|
30
|
+
const parts = text.trim().split(/\s+/);
|
|
31
|
+
const cmd = parts[0]?.toLowerCase() ?? "help";
|
|
32
|
+
const args = parts.slice(1);
|
|
33
|
+
|
|
34
|
+
const graph = loadGraph(distDir);
|
|
35
|
+
if (!graph && cmd !== "help" && cmd !== "status") {
|
|
36
|
+
return { response_type: "ephemeral", text: "❌ graph.json not found. Run `dve build` first." };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
switch (cmd) {
|
|
40
|
+
case "trace": {
|
|
41
|
+
if (!args[0]) return { response_type: "ephemeral", text: "Usage: /dve trace DD-001" };
|
|
42
|
+
const chain = traceDecision(graph!, args[0]);
|
|
43
|
+
if (chain.length === 0) return { response_type: "ephemeral", text: `❌ ${args[0]} not found.` };
|
|
44
|
+
|
|
45
|
+
const lines = chain.map((n) => {
|
|
46
|
+
const d = n.data as any;
|
|
47
|
+
switch (n.type) {
|
|
48
|
+
case "decision": return `📋 *${n.id}*: ${d.title} (${d.date}) [${d.status}]`;
|
|
49
|
+
case "gap": return ` ← 🔴 ${n.id}: ${(d.summary ?? "").slice(0, 80)} (${d.severity})`;
|
|
50
|
+
case "session": return ` ← 📁 ${n.id} (${(d.characters ?? []).join(", ")})`;
|
|
51
|
+
default: return ` ← ${n.type}: ${n.id}`;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return { response_type: "in_channel", text: `*Trace: ${args[0]}*\n${lines.join("\n")}` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case "orphans": {
|
|
58
|
+
const orphans = orphanGaps(graph!);
|
|
59
|
+
if (orphans.length === 0) return { response_type: "in_channel", text: "✅ No orphan gaps. All gaps linked to decisions." };
|
|
60
|
+
|
|
61
|
+
const lines = orphans.slice(0, 10).map((g) => {
|
|
62
|
+
const d = g.data as Gap;
|
|
63
|
+
return `• ${g.id}: ${(d.summary ?? "").slice(0, 60)} (${d.severity})`;
|
|
64
|
+
});
|
|
65
|
+
const more = orphans.length > 10 ? `\n_... and ${orphans.length - 10} more_` : "";
|
|
66
|
+
return { response_type: "in_channel", text: `*Orphan Gaps (${orphans.length})*\n${lines.join("\n")}${more}` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case "search": {
|
|
70
|
+
if (!args[0]) return { response_type: "ephemeral", text: "Usage: /dve search <keyword>" };
|
|
71
|
+
const results = search(graph!, args.join(" "));
|
|
72
|
+
if (results.length === 0) return { response_type: "ephemeral", text: `No results for "${args.join(" ")}".` };
|
|
73
|
+
|
|
74
|
+
const lines = results.slice(0, 8).map((n) => {
|
|
75
|
+
const d = n.data as any;
|
|
76
|
+
const label = d.title ?? d.summary ?? d.theme ?? "";
|
|
77
|
+
return `• \`${n.type}\` ${n.id}: ${label.slice(0, 60)}`;
|
|
78
|
+
});
|
|
79
|
+
return { response_type: "in_channel", text: `*Search: "${args.join(" ")}"* (${results.length} results)\n${lines.join("\n")}` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case "status": {
|
|
83
|
+
const statusLines = projectDirs.map((p) => {
|
|
84
|
+
const state = detectProjectState(p.name, p.path);
|
|
85
|
+
const wf = state.workflow;
|
|
86
|
+
const current = wf.currentPhase;
|
|
87
|
+
const sub = wf.subState ? ` > ${wf.subState}` : "";
|
|
88
|
+
return `• *${p.name}*: \`${current}${sub}\` | S:${state.dgeSessionCount} DD:${state.ddCount} | DRE: ${state.dre.installState}`;
|
|
89
|
+
});
|
|
90
|
+
return { response_type: "in_channel", text: `*DVE Status*\n${statusLines.join("\n")}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "overturned": {
|
|
94
|
+
const ot = overturned(graph!);
|
|
95
|
+
if (ot.length === 0) return { response_type: "in_channel", text: "✅ No overturned decisions." };
|
|
96
|
+
const lines = ot.map(({ decision, impact }) => {
|
|
97
|
+
const d = decision.data as any;
|
|
98
|
+
return `• ❌ *${decision.id}*: ${d.title} → ${impact.length} affected nodes`;
|
|
99
|
+
});
|
|
100
|
+
return { response_type: "in_channel", text: `*Overturned Decisions*\n${lines.join("\n")}` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case "list": {
|
|
104
|
+
const filter = args[0]?.toLowerCase() ?? "all"; // dd / gap / session / spec / all
|
|
105
|
+
const nodes = graph!.nodes.filter((n) => {
|
|
106
|
+
if (filter === "dd" || filter === "decisions") return n.type === "decision";
|
|
107
|
+
if (filter === "gap" || filter === "gaps") return n.type === "gap";
|
|
108
|
+
if (filter === "session" || filter === "sessions") return n.type === "session";
|
|
109
|
+
if (filter === "spec" || filter === "specs") return n.type === "spec";
|
|
110
|
+
return n.type === "decision" || n.type === "session" || n.type === "spec";
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (nodes.length === 0) return { response_type: "ephemeral", text: `No ${filter} found.` };
|
|
114
|
+
|
|
115
|
+
const blocks: any[] = [
|
|
116
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${filter === "all" ? "All Items" : filter.toUpperCase()}* (${nodes.length})` } },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
// Group by type
|
|
120
|
+
const grouped: Record<string, typeof nodes> = {};
|
|
121
|
+
for (const n of nodes) {
|
|
122
|
+
if (!grouped[n.type]) grouped[n.type] = [];
|
|
123
|
+
grouped[n.type].push(n);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
127
|
+
const icon = type === "decision" ? "📋" : type === "session" ? "📁" : type === "spec" ? "📄" : type === "gap" ? "🔴" : "📎";
|
|
128
|
+
blocks.push({ type: "divider" });
|
|
129
|
+
blocks.push({ type: "section", text: { type: "mrkdwn", text: `*${icon} ${type}* (${items.length})` } });
|
|
130
|
+
|
|
131
|
+
for (const item of items.slice(0, 10)) {
|
|
132
|
+
const d = item.data as any;
|
|
133
|
+
const label = d.title ?? d.theme ?? d.summary?.slice(0, 50) ?? item.id;
|
|
134
|
+
const meta = type === "decision" ? `${d.date ?? ""} | ${d.status ?? ""}` :
|
|
135
|
+
type === "session" ? `${d.date ?? ""} | ${(d.characters ?? []).slice(0, 3).join(", ")}` :
|
|
136
|
+
type === "spec" ? `${d.type ?? ""} | ${d.status ?? ""}` :
|
|
137
|
+
`${(d as any).severity ?? ""}`;
|
|
138
|
+
|
|
139
|
+
blocks.push({
|
|
140
|
+
type: "section",
|
|
141
|
+
text: { type: "mrkdwn", text: `*${item.id}*\n${label}\n_${meta}_` },
|
|
142
|
+
accessory: {
|
|
143
|
+
type: "button",
|
|
144
|
+
text: { type: "plain_text", text: type === "decision" ? "Trace" : "Detail" },
|
|
145
|
+
action_id: `dve_detail_${item.id}`,
|
|
146
|
+
value: item.id,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (items.length > 10) {
|
|
152
|
+
blocks.push({ type: "section", text: { type: "mrkdwn", text: `_... and ${items.length - 10} more_` } });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { response_type: "in_channel", text: `List: ${filter}`, blocks };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case "summary": {
|
|
160
|
+
const s = graph!.stats;
|
|
161
|
+
const orphanCount = orphanGaps(graph!).length;
|
|
162
|
+
const otCount = overturned(graph!).length;
|
|
163
|
+
return {
|
|
164
|
+
response_type: "in_channel",
|
|
165
|
+
text: `*DVE Summary*\nSessions: ${s.sessions} | Gaps: ${s.gaps} | Decisions: ${s.decisions} | Specs: ${s.specs ?? 0} | Annotations: ${s.annotations}\nOrphan Gaps: ${orphanCount} | Overturned: ${otCount}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
default:
|
|
170
|
+
return {
|
|
171
|
+
response_type: "ephemeral",
|
|
172
|
+
text: `*DVE Slack Commands*
|
|
173
|
+
\`/dve list [dd|gap|session|spec]\` — 一覧(ボタン付き)
|
|
174
|
+
\`/dve trace DD-001\` — 因果チェーン
|
|
175
|
+
\`/dve orphans\` — 未解決 Gap
|
|
176
|
+
\`/dve search <keyword>\` — 検索
|
|
177
|
+
\`/dve status\` — 全プロジェクト状態
|
|
178
|
+
\`/dve summary\` — 統計サマリー
|
|
179
|
+
\`/dve overturned\` — 撤回された DD
|
|
180
|
+
\`/dve help\` — このヘルプ`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Events API Handler ───
|
|
186
|
+
// @DVE メンション → 自然言語でクエリ
|
|
187
|
+
|
|
188
|
+
export function handleEvent(event: any, distDir: string): string | null {
|
|
189
|
+
if (event.type !== "app_mention" && event.type !== "message") return null;
|
|
190
|
+
|
|
191
|
+
const text = (event.text ?? "").replace(/<@[^>]+>/g, "").trim().toLowerCase();
|
|
192
|
+
|
|
193
|
+
// Simple keyword routing
|
|
194
|
+
if (/trace|経緯|なぜ/.test(text)) {
|
|
195
|
+
const ddMatch = text.match(/DD-\d+/i);
|
|
196
|
+
if (ddMatch) {
|
|
197
|
+
const graph = loadGraph(distDir);
|
|
198
|
+
if (!graph) return "graph.json not found.";
|
|
199
|
+
const chain = traceDecision(graph, ddMatch[0]);
|
|
200
|
+
return chain.map((n) => `${n.type}: ${n.id}`).join(" ← ");
|
|
201
|
+
}
|
|
202
|
+
return "DD 番号を指定してください(例: DD-003 の経緯)";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (/orphan|未解決|未決定/.test(text)) {
|
|
206
|
+
const graph = loadGraph(distDir);
|
|
207
|
+
if (!graph) return "graph.json not found.";
|
|
208
|
+
const orphans = orphanGaps(graph);
|
|
209
|
+
return `未解決 Gap: ${orphans.length} 件`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (/status|状態|フェーズ/.test(text)) {
|
|
213
|
+
return "Use `/dve status` for project status.";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return "🤖 `/dve help` でコマンド一覧を見れます。";
|
|
217
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Skill: DVE Annotate
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
「DD-005 にコメント」「この決定に異議」「drift を記録」「overturn」
|
|
5
|
+
|
|
6
|
+
## Procedure
|
|
7
|
+
|
|
8
|
+
1. 対象ノード (DD, Gap, or Session) を特定
|
|
9
|
+
2. action を決定:
|
|
10
|
+
- `comment` — 単なるコメント
|
|
11
|
+
- `fork` — ここから DGE 分岐
|
|
12
|
+
- `overturn` — この決定を撤回
|
|
13
|
+
- `constrain` — 制約を追加
|
|
14
|
+
- `drift` — 現実と乖離している
|
|
15
|
+
3. `node dve/kit/dist/cli/dve-tool.js annotate <id> --action <type> --body "text"` を実行
|
|
16
|
+
4. `dve/annotations/` にファイルが生成される
|
|
17
|
+
5. `dve build` でグラフに反映
|
|
18
|
+
|
|
19
|
+
## Annotation の影響
|
|
20
|
+
- `overturn` → DD の枠が赤に、取り消し線
|
|
21
|
+
- `drift` → DD の枠が黄・点線
|
|
22
|
+
- `constrain` → バッジ付き
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
- Session は immutable。Annotation は別レイヤーで保存
|
|
26
|
+
- Web UI からも作成可能(API server 経由)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Skill: DVE Build
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
「DVE ビルド」「決定マップを更新」「graph を更新」
|
|
5
|
+
|
|
6
|
+
## Procedure
|
|
7
|
+
|
|
8
|
+
1. kit をコンパイル: `npx tsc -p dve/kit/tsconfig.json`
|
|
9
|
+
2. graph.json を生成: `node dve/kit/dist/cli/dve-tool.js build`
|
|
10
|
+
3. app をビルド: `cd dve/app && npx vite build`
|
|
11
|
+
4. 結果を報告(sessions / gaps / decisions / specs / annotations の件数)
|
|
12
|
+
|
|
13
|
+
## Notes
|
|
14
|
+
- graph.json に session/DD の全文 (content) が含まれる
|
|
15
|
+
- changelog.json で前回ビルドとの差分を表示
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Skill: DVE Context (DGE 再起動)
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
「DD-003 の経緯で DGE」「この決定を再検討」「制約を追加して DGE」
|
|
5
|
+
|
|
6
|
+
## Procedure
|
|
7
|
+
|
|
8
|
+
1. 対象ノード (DD or Gap) を特定
|
|
9
|
+
2. `node dve/kit/dist/cli/dve-tool.js context <id> [--constraint="..."]` を実行
|
|
10
|
+
3. ContextBundle (JSON) が `dve/contexts/` に保存される
|
|
11
|
+
4. prompt_template が表示される — これを DGE に渡す
|
|
12
|
+
5. そのまま DGE セッションを開始する(prompt_template をコンテキストとして使用)
|
|
13
|
+
|
|
14
|
+
## ContextBundle の中身
|
|
15
|
+
- origin: 起点ノード
|
|
16
|
+
- summary: テーマ、前回の DD 一覧、Gap 一覧、キャラ
|
|
17
|
+
- new_constraints: ユーザーが追加した制約
|
|
18
|
+
- prompt_template: DGE に渡すテキスト(自動生成)
|
|
19
|
+
|
|
20
|
+
## Notes
|
|
21
|
+
- DVE → DGE の橋渡し。疎結合(プロンプトテキストだけが接点)
|
|
22
|
+
- DGE の Phase 0 が context: パスを検出して自動読み込み
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Skill: DVE Serve
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
「DVE を開いて」「決定マップを見せて」「DVE serve」
|
|
5
|
+
|
|
6
|
+
## Procedure
|
|
7
|
+
|
|
8
|
+
1. graph.json が古ければ先にビルド: `node dve/kit/dist/cli/dve-tool.js build`
|
|
9
|
+
2. サーバー起動: `node dve/kit/dist/cli/dve-tool.js serve`
|
|
10
|
+
- Web UI: http://localhost:4173
|
|
11
|
+
- API: http://localhost:4174
|
|
12
|
+
3. `--watch` オプション付きならファイル監視も起��
|
|
13
|
+
4. ユーザーに URL を案内
|
|
14
|
+
|
|
15
|
+
## Notes
|
|
16
|
+
- `serve --watch` で DGE session 保存時に自動リビルド
|
|
17
|
+
- API は annotation 作成、ドリフト検出、キャラカバレッジを提供
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Skill: DVE Status
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
「DVE ステータス」「プロジェクトの状態は」「今どのフェーズ?」
|
|
5
|
+
|
|
6
|
+
## Procedure
|
|
7
|
+
|
|
8
|
+
1. `node dve/kit/dist/cli/dve-tool.js status` を実行
|
|
9
|
+
2. 表示内容:
|
|
10
|
+
- DRE install 状態 (FRESH / INSTALLED / CUSTOMIZED / OUTDATED)
|
|
11
|
+
- Workflow state machine (backlog → spec → {gap_extraction} → impl → review → release)
|
|
12
|
+
- 現在フェーズ + サブステート
|
|
13
|
+
- インストール済み plugin 一覧
|
|
14
|
+
3. 複数プ���ジェクト対応: `dve.config.json` があれば全プロジェクトを表示
|
|
15
|
+
|
|
16
|
+
## Notes
|
|
17
|
+
- 現在フェーズの検出優先度: .dre/context.json > CLAUDE.md active_phase > git log 推定
|
|
18
|
+
- サブステートは plugin の内部 SM (e.g. DGE: flow_detection → dialogue_generation → ...)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Skill: DVE Trace
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
「DD-005 の経緯」「この決定はなぜ?」「因果チェーンを見せて」「trace DD-003」
|
|
5
|
+
|
|
6
|
+
## Procedure
|
|
7
|
+
|
|
8
|
+
1. DD 番号を特定(ユーザー入力 or 文脈から推定)
|
|
9
|
+
2. `node dve/kit/dist/cli/dve-tool.js trace <DD-id>` を実行
|
|
10
|
+
3. 因果チェーンを表示:
|
|
11
|
+
- DD → Gap (severity) → Session (characters, date)
|
|
12
|
+
4. 「この文脈で DGE しますか?」と提案
|
|
13
|
+
|
|
14
|
+
## Notes
|
|
15
|
+
- graph.json が必要。なければ先に `dve build` を実行
|
|
16
|
+
- DD が見つからない場合は `dve search` で検索を提案
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["parser/**/*.ts", "graph/**/*.ts", "context/**/*.ts", "cli/**/*.ts", "server/**/*.ts", "config.ts"],
|
|
14
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
15
|
+
}
|