@doquflow/cli 1.2.1 → 1.3.1

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,331 @@
1
+ "use strict";
2
+ /**
3
+ * docuflow review
4
+ *
5
+ * Review git changes and surface deterministic findings plus actionable
6
+ * improvements. Optional --ai mode appends Copilot analysis.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getChangedFiles = getChangedFiles;
13
+ exports.getDiffText = getDiffText;
14
+ exports.buildDeterministicReview = buildDeterministicReview;
15
+ exports.runCopilotReview = runCopilotReview;
16
+ exports.run = run;
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const node_child_process_1 = require("node:child_process");
19
+ const DIFF_CAP_BYTES = 200 * 1024;
20
+ const AI_DIFF_CAP_CHARS = 40_000;
21
+ const c = {
22
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
23
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
24
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
25
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
26
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
27
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
28
+ };
29
+ function runGit(projectPath, args) {
30
+ const result = (0, node_child_process_1.spawnSync)("git", args, { cwd: projectPath, encoding: "utf8" });
31
+ if (result.status !== 0) {
32
+ const stderr = (result.stderr ?? "").trim();
33
+ const badRef = /bad revision|unknown revision|ambiguous argument/i.test(stderr);
34
+ if (badRef) {
35
+ throw new Error(`Invalid git ref: ${args.join(" ")}`);
36
+ }
37
+ throw new Error(stderr || `git ${args.join(" ")} failed`);
38
+ }
39
+ return (result.stdout ?? "").trim();
40
+ }
41
+ function runGitAllowStatuses(projectPath, args, allowedStatuses) {
42
+ const result = (0, node_child_process_1.spawnSync)("git", args, { cwd: projectPath, encoding: "utf8" });
43
+ if (result.status === null || !allowedStatuses.includes(result.status)) {
44
+ const stderr = (result.stderr ?? "").trim();
45
+ const badRef = /bad revision|unknown revision|ambiguous argument/i.test(stderr);
46
+ if (badRef) {
47
+ throw new Error(`Invalid git ref: ${args.join(" ")}`);
48
+ }
49
+ throw new Error(stderr || `git ${args.join(" ")} failed`);
50
+ }
51
+ return (result.stdout ?? "").trim();
52
+ }
53
+ function ensureGitRepo(projectPath) {
54
+ try {
55
+ const out = runGit(projectPath, ["rev-parse", "--is-inside-work-tree"]);
56
+ if (out !== "true") {
57
+ throw new Error("not a git repository");
58
+ }
59
+ }
60
+ catch {
61
+ throw new Error(`No git repository detected at ${projectPath}`);
62
+ }
63
+ }
64
+ function parsePorcelainPaths(porcelain) {
65
+ const out = new Set();
66
+ for (const line of porcelain.split("\n")) {
67
+ const trimmed = line.trim();
68
+ if (!trimmed)
69
+ continue;
70
+ const payload = line.replace(/^[ MARCUD?!]{1,2}\s+/, "").trim();
71
+ if (!payload)
72
+ continue;
73
+ if (payload.includes(" -> ")) {
74
+ const parts = payload.split(" -> ");
75
+ out.add(parts[parts.length - 1].trim());
76
+ }
77
+ else {
78
+ out.add(payload);
79
+ }
80
+ }
81
+ return Array.from(out).sort((a, b) => a.localeCompare(b));
82
+ }
83
+ function capDiff(diffText) {
84
+ const bytes = Buffer.byteLength(diffText, "utf8");
85
+ if (bytes <= DIFF_CAP_BYTES)
86
+ return diffText;
87
+ const truncated = Buffer.from(diffText, "utf8").subarray(0, DIFF_CAP_BYTES).toString("utf8");
88
+ return `${truncated}\n\n[DOCUFLOW_DIFF_TRUNCATED: analyzed first ${DIFF_CAP_BYTES} bytes of ${bytes} bytes]\n`;
89
+ }
90
+ function pushUnique(arr, item) {
91
+ if (!arr.includes(item))
92
+ arr.push(item);
93
+ }
94
+ function getChangedFiles(projectPath, staged, sinceCommit) {
95
+ if (sinceCommit) {
96
+ const output = runGit(projectPath, ["diff", "--name-only", sinceCommit, "HEAD"]);
97
+ return output ? output.split("\n").filter(Boolean) : [];
98
+ }
99
+ if (staged) {
100
+ const output = runGit(projectPath, ["diff", "--name-only", "--cached"]);
101
+ return output ? output.split("\n").filter(Boolean) : [];
102
+ }
103
+ return parsePorcelainPaths(runGit(projectPath, ["status", "--porcelain"]));
104
+ }
105
+ function getDiffText(projectPath, staged, sinceCommit) {
106
+ if (sinceCommit) {
107
+ return capDiff(runGit(projectPath, ["diff", sinceCommit, "HEAD"]));
108
+ }
109
+ if (staged) {
110
+ return capDiff(runGit(projectPath, ["diff", "--cached"]));
111
+ }
112
+ const untracked = runGit(projectPath, ["ls-files", "--others", "--exclude-standard"])
113
+ .split("\n")
114
+ .map(line => line.trim())
115
+ .filter(Boolean);
116
+ const untrackedPatches = [];
117
+ for (const file of untracked) {
118
+ const patch = runGitAllowStatuses(projectPath, ["diff", "--no-index", "--", "/dev/null", file], [0, 1]);
119
+ if (patch) {
120
+ untrackedPatches.push(patch);
121
+ }
122
+ }
123
+ const stagedDiff = runGit(projectPath, ["diff", "--cached"]);
124
+ const workingDiff = runGit(projectPath, ["diff"]);
125
+ const joined = [
126
+ "=== STAGED DIFF ===",
127
+ stagedDiff,
128
+ "",
129
+ "=== WORKING TREE DIFF ===",
130
+ workingDiff,
131
+ "",
132
+ "=== UNTRACKED FILE DIFF ===",
133
+ untrackedPatches.join("\n"),
134
+ ].join("\n");
135
+ return capDiff(joined);
136
+ }
137
+ function buildDeterministicReview(changedFiles, diffText) {
138
+ const summary = [];
139
+ const critical = [];
140
+ const warnings = [];
141
+ const improvements = [];
142
+ summary.push(`Changed files: ${changedFiles.length}`);
143
+ for (const f of changedFiles.slice(0, 10)) {
144
+ summary.push(f);
145
+ }
146
+ if (changedFiles.length > 10) {
147
+ summary.push(`...and ${changedFiles.length - 10} more`);
148
+ }
149
+ const truncated = diffText.includes("[DOCUFLOW_DIFF_TRUNCATED:");
150
+ if (truncated) {
151
+ warnings.push("Large diff detected; review is partial due to diff size cap.");
152
+ improvements.push("Run focused reviews per file or per commit range for full coverage.");
153
+ }
154
+ const addedLines = diffText
155
+ .split("\n")
156
+ .filter(line => line.startsWith("+") && !line.startsWith("+++"))
157
+ .map(line => line.slice(1));
158
+ if (changedFiles.length > 0 && addedLines.length === 0) {
159
+ summary.push("Textual diff is empty (likely binary/rename-only changes).");
160
+ improvements.push("Manually review binary assets and metadata changes before commit.");
161
+ return { summary, critical, warnings, improvements };
162
+ }
163
+ const addedText = addedLines.join("\n");
164
+ if (/(AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|sk-[A-Za-z0-9]{20,}|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----)/.test(addedText)) {
165
+ critical.push("Potential secret/key material detected in added lines.");
166
+ improvements.push("Remove credentials from code and load secrets via environment variables.");
167
+ }
168
+ if (/(password|token|secret)\s*[:=]\s*["'][^"']{4,}["']/i.test(addedText)) {
169
+ critical.push("Hardcoded credential-like value detected.");
170
+ }
171
+ if (/\b(eval|new Function)\s*\(/.test(addedText)) {
172
+ critical.push("Dynamic code execution (`eval`/`new Function`) detected.");
173
+ improvements.push("Replace dynamic execution with explicit parsing or whitelisted dispatch.");
174
+ }
175
+ if (/\bchild_process\.(exec|execSync)\s*\(/.test(addedText)) {
176
+ warnings.push("Shell execution added via child_process.exec/execSync.");
177
+ improvements.push("Prefer spawn/spawnSync with argument arrays and explicit input validation.");
178
+ }
179
+ if (/\bTODO\b|\bFIXME\b|\bHACK\b/.test(addedText)) {
180
+ warnings.push("TODO/FIXME/HACK markers found in added code.");
181
+ improvements.push("Resolve or ticket these markers before merge to avoid hidden follow-up work.");
182
+ }
183
+ if (/console\.log\s*\(/.test(addedText)) {
184
+ warnings.push("console.log statements found in added lines.");
185
+ improvements.push("Use structured logger patterns or remove debug logging before merge.");
186
+ }
187
+ if (/\bas any\b|:\s*any\b/.test(addedText)) {
188
+ warnings.push("Type safety weakened with `any` usage.");
189
+ improvements.push("Tighten types with explicit interfaces or narrow unions.");
190
+ }
191
+ if (/@ts-ignore/.test(addedText)) {
192
+ warnings.push("@ts-ignore found in added lines.");
193
+ improvements.push("Replace @ts-ignore with proper typing or guarded runtime checks.");
194
+ }
195
+ if (/catch\s*(\([^)]*\))?\s*\{\s*\}/.test(addedText)) {
196
+ warnings.push("Empty catch block detected.");
197
+ improvements.push("Handle caught errors explicitly and emit actionable context.");
198
+ }
199
+ const longLines = addedLines.filter(line => line.length > 160).length;
200
+ if (longLines > 0) {
201
+ warnings.push(`${longLines} long added line(s) (>160 chars) may hurt readability.`);
202
+ }
203
+ const hasTestFile = changedFiles.some(f => /(^|\/)(test|tests|__tests__)\/|\.test\.[a-z]+$|\.spec\.[a-z]+$/i.test(f));
204
+ if (!hasTestFile) {
205
+ improvements.push("Consider adding or updating tests for changed behavior.");
206
+ }
207
+ if (critical.length === 0 && warnings.length === 0) {
208
+ summary.push("No deterministic critical/warning findings detected.");
209
+ improvements.push("Quick manual pass for architecture consistency and naming is still recommended.");
210
+ }
211
+ return { summary, critical, warnings, improvements };
212
+ }
213
+ function buildCopilotPrompt(projectPath, changedFiles, diffText) {
214
+ const diffForPrompt = diffText.slice(0, AI_DIFF_CAP_CHARS);
215
+ return [
216
+ `Review these git changes for project: ${projectPath}`,
217
+ ``,
218
+ `Changed files:`,
219
+ changedFiles.slice(0, 40).map(f => `- ${f}`).join("\n") || "(none)",
220
+ ``,
221
+ `Diff:`,
222
+ diffForPrompt || "(no textual diff)",
223
+ ``,
224
+ `Return concise markdown with sections:`,
225
+ `1) Critical issues`,
226
+ `2) Warnings`,
227
+ `3) Concrete improvements`,
228
+ `Focus on correctness, security, and maintainability.`,
229
+ ].join("\n");
230
+ }
231
+ function runCopilotReview(prompt) {
232
+ const result = (0, node_child_process_1.spawnSync)("copilot", [
233
+ "--prompt", prompt,
234
+ "--allow-all-tools",
235
+ "--allow-all-paths",
236
+ "--no-ask-user",
237
+ "--output-format", "json"
238
+ ], { encoding: "utf8", timeout: 180_000 });
239
+ if (result.error || result.status !== 0)
240
+ return null;
241
+ let lastMessage = null;
242
+ for (const line of (result.stdout ?? "").split("\n")) {
243
+ const trimmed = line.trim();
244
+ if (!trimmed)
245
+ continue;
246
+ try {
247
+ const obj = JSON.parse(trimmed);
248
+ if (obj.type === "assistant.message" && obj.data?.content) {
249
+ lastMessage = obj.data.content;
250
+ }
251
+ }
252
+ catch {
253
+ // ignore malformed json lines
254
+ }
255
+ }
256
+ return lastMessage;
257
+ }
258
+ function printSection(title, lines, quiet) {
259
+ const label = quiet ? title : c.bold(title);
260
+ console.log(`\n${label}`);
261
+ if (lines.length === 0) {
262
+ console.log(" - none");
263
+ return;
264
+ }
265
+ for (const line of lines) {
266
+ console.log(` - ${line}`);
267
+ }
268
+ }
269
+ async function run(options = {}) {
270
+ const projectPath = node_path_1.default.resolve(options.projectPath ?? process.cwd());
271
+ const staged = !!options.staged;
272
+ const sinceCommit = options.sinceCommit;
273
+ const quiet = !!options.quiet;
274
+ const failOnCritical = !!options.failOnCritical;
275
+ const ai = !!options.ai;
276
+ try {
277
+ ensureGitRepo(projectPath);
278
+ }
279
+ catch (error) {
280
+ console.error(c.red(`✗ ${error.message}`));
281
+ process.exit(2);
282
+ }
283
+ let changedFiles = [];
284
+ let diffText = "";
285
+ try {
286
+ changedFiles = getChangedFiles(projectPath, staged, sinceCommit);
287
+ diffText = getDiffText(projectPath, staged, sinceCommit);
288
+ }
289
+ catch (error) {
290
+ const message = error.message;
291
+ if (message.startsWith("Invalid git ref:")) {
292
+ console.error(c.red(`✗ Invalid --since-commit ref: ${sinceCommit}`));
293
+ process.exit(2);
294
+ }
295
+ console.error(c.red(`✗ ${message}`));
296
+ process.exit(2);
297
+ }
298
+ if (changedFiles.length === 0) {
299
+ console.log(quiet ? "nothing to review" : c.cyan("Nothing to review: no changed files in selected scope."));
300
+ return;
301
+ }
302
+ const modeLabel = sinceCommit
303
+ ? `since ${sinceCommit}`
304
+ : staged
305
+ ? "staged changes"
306
+ : "working tree (staged + unstaged)";
307
+ if (!quiet) {
308
+ console.log(c.bold("\n🔍 DocuFlow Review\n"));
309
+ console.log(`Scope: ${c.cyan(modeLabel)}`);
310
+ }
311
+ const deterministic = buildDeterministicReview(changedFiles, diffText);
312
+ printSection("Summary", deterministic.summary, quiet);
313
+ printSection("Critical", deterministic.critical, quiet);
314
+ printSection("Warnings", deterministic.warnings, quiet);
315
+ printSection("Improvements", deterministic.improvements, quiet);
316
+ if (ai) {
317
+ const prompt = buildCopilotPrompt(projectPath, changedFiles, diffText);
318
+ const aiResult = runCopilotReview(prompt);
319
+ if (aiResult) {
320
+ const aiLabel = quiet ? "AI Review (Copilot)" : c.bold("\nAI Review (Copilot)");
321
+ console.log(`\n${aiLabel}`);
322
+ console.log(aiResult);
323
+ }
324
+ else {
325
+ deterministic.improvements.push("Copilot AI review unavailable; using deterministic review only");
326
+ }
327
+ }
328
+ if (failOnCritical && deterministic.critical.length > 0) {
329
+ process.exit(1);
330
+ }
331
+ }
@@ -137,6 +137,31 @@ async function runStatus(projectPath) {
137
137
  console.log(` Uptime: ${formatUptime(data.started_at)}`);
138
138
  console.log(` Started: ${new Date(data.started_at).toLocaleString()}`);
139
139
  console.log(` Bridge: ${bridgeLabel}`);
140
+ // Active bridge — only shown when it differs from startup bridge (failover occurred)
141
+ const activeBridge = data.active_bridge ?? data.bridge;
142
+ if (activeBridge !== data.bridge) {
143
+ const activeBridgeLabel = activeBridge === "none" ? c.dim("sources-only (no AI)") :
144
+ activeBridge === "copilot" ? c.green("copilot — direct MCP ⚡") :
145
+ activeBridge === "claude" ? c.green("claude — direct MCP ⚡") :
146
+ activeBridge === "codex" ? c.yellow("codex — doc-gen mode") :
147
+ activeBridge === "api" ? c.yellow("api — doc-gen mode") : activeBridge;
148
+ console.log(` Active bridge: ${activeBridgeLabel} ${c.yellow("(failed over)")}`);
149
+ }
150
+ // Failover stats
151
+ const failover = data.failover ?? { count: 0, last_at: null, from: null, to: null, reason: null };
152
+ if (failover.count > 0) {
153
+ const lastAt = failover.last_at
154
+ ? new Date(failover.last_at).toLocaleTimeString()
155
+ : "unknown";
156
+ console.log(` Failovers: ${c.yellow(String(failover.count))} total`);
157
+ console.log(` Last failover: ${lastAt} — ${failover.from} → ${failover.to}`);
158
+ if (failover.reason) {
159
+ console.log(` Reason: ${c.dim(failover.reason)}`);
160
+ }
161
+ }
162
+ else {
163
+ console.log(` Failovers: ${c.dim("none")}`);
164
+ }
140
165
  console.log(` Project: ${data.project_path}`);
141
166
  if (data.options.lintIntervalHours) {
142
167
  console.log(` Lint: every ${data.options.lintIntervalHours}h`);
@@ -39,6 +39,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  };
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.detectBridge = detectBridge;
42
+ exports.getNextBridge = getNextBridge;
43
+ exports.recordFailover = recordFailover;
42
44
  exports.getPidFilePath = getPidFilePath;
43
45
  exports.writePidFile = writePidFile;
44
46
  exports.removePidFile = removePidFile;
@@ -282,7 +284,11 @@ function buildDocGenPrompt(projectPath, changedFiles) {
282
284
  ].join("\n");
283
285
  }
284
286
  // ─── Core sync dispatcher ─────────────────────────────────────────────────────
285
- async function syncWithAI(projectPath, changedFiles, bridge) {
287
+ async function syncWithAI(projectPath, changedFiles, bridge, depth = 0) {
288
+ if (bridge === "none" || depth >= 4) {
289
+ await directIngestAll(projectPath);
290
+ return;
291
+ }
286
292
  const bridgeLabel = bridge === "copilot" ? "Copilot" : bridge === "claude" ? "Claude" : bridge === "codex" ? "Codex" : "API";
287
293
  log("🤖", `${changedFiles.length} file(s) changed — asking ${c.cyan(bridgeLabel)} to update wiki...`);
288
294
  // Copilot and Claude: DIRECT MCP tool calling
@@ -294,8 +300,10 @@ async function syncWithAI(projectPath, changedFiles, bridge) {
294
300
  console.log(c.dim(` ${result.replace(/\n/g, "\n ")}`));
295
301
  }
296
302
  else {
297
- log("⚠️ ", c.yellow("Copilot returned no result — falling back to direct ingest"));
298
- await directIngestAll(projectPath);
303
+ const next = getNextBridge(bridge);
304
+ await recordFailover(projectPath, bridge, next, "no output");
305
+ log("⚠️ ", c.yellow(`Copilot failed — falling over to ${next}`));
306
+ await syncWithAI(projectPath, changedFiles, next, depth + 1);
299
307
  }
300
308
  return;
301
309
  }
@@ -307,8 +315,10 @@ async function syncWithAI(projectPath, changedFiles, bridge) {
307
315
  console.log(c.dim(` ${result.replace(/\n/g, "\n ")}`));
308
316
  }
309
317
  else {
310
- log("⚠️ ", c.yellow("Claude returned no result — falling back to direct ingest"));
311
- await directIngestAll(projectPath);
318
+ const next = getNextBridge(bridge);
319
+ await recordFailover(projectPath, bridge, next, "no output");
320
+ log("⚠️ ", c.yellow(`Claude failed — falling over to ${next}`));
321
+ await syncWithAI(projectPath, changedFiles, next, depth + 1);
312
322
  }
313
323
  return;
314
324
  }
@@ -322,8 +332,10 @@ async function syncWithAI(projectPath, changedFiles, bridge) {
322
332
  docContent = await callAnthropicAPI(docPrompt);
323
333
  }
324
334
  if (!docContent) {
325
- log("⚠️ ", c.yellow("AI returned no content — falling back to direct ingest"));
326
- await directIngestAll(projectPath);
335
+ const next = getNextBridge(bridge);
336
+ await recordFailover(projectPath, bridge, next, "no output");
337
+ log("⚠️ ", c.yellow(`${bridgeLabel} failed — falling over to ${next}`));
338
+ await syncWithAI(projectPath, changedFiles, next, depth + 1);
327
339
  return;
328
340
  }
329
341
  // Save AI-generated doc to sources/
@@ -335,13 +347,23 @@ async function syncWithAI(projectPath, changedFiles, bridge) {
335
347
  log("💾", `AI doc saved → ${c.cyan(autoFilename)}`);
336
348
  await directIngest(projectPath, autoFilename);
337
349
  }
338
- async function scheduledLintWithAI(projectPath, bridge) {
350
+ async function scheduledLintWithAI(projectPath, bridge, depth = 0) {
351
+ if (bridge === "none" || depth >= 4) {
352
+ // Lint is best-effort — silent return at terminal level (no directIngestAll)
353
+ return;
354
+ }
339
355
  if (bridge === "copilot") {
340
356
  log("🔍", `Running scheduled lint via ${c.cyan("Copilot")} (direct MCP call)...`);
341
357
  const result = runCopilotCLI(buildCopilotLintPrompt(projectPath));
342
358
  if (result) {
343
359
  console.log(c.dim(` ${result.replace(/\n/g, "\n ")}`));
344
360
  }
361
+ else {
362
+ const next = getNextBridge(bridge);
363
+ await recordFailover(projectPath, bridge, next, "no output during lint");
364
+ log("⚠️ ", c.yellow(`Copilot lint failed — falling over to ${next}`));
365
+ await scheduledLintWithAI(projectPath, next, depth + 1);
366
+ }
345
367
  return;
346
368
  }
347
369
  if (bridge === "claude") {
@@ -350,10 +372,17 @@ async function scheduledLintWithAI(projectPath, bridge) {
350
372
  if (result) {
351
373
  console.log(c.dim(` ${result.replace(/\n/g, "\n ")}`));
352
374
  }
375
+ else {
376
+ const next = getNextBridge(bridge);
377
+ await recordFailover(projectPath, bridge, next, "no output during lint");
378
+ log("⚠️ ", c.yellow(`Claude lint failed — falling over to ${next}`));
379
+ await scheduledLintWithAI(projectPath, next, depth + 1);
380
+ }
353
381
  return;
354
382
  }
355
- // Fallback: call lint tool directly
356
- await directLint(projectPath);
383
+ const next = getNextBridge(bridge);
384
+ log("ℹ️ ", c.dim(`${bridge} cannot perform MCP lint — escalating to ${next}`));
385
+ await scheduledLintWithAI(projectPath, next, depth + 1);
357
386
  }
358
387
  // ─── Direct tool calls (no AI) ───────────────────────────────────────────────
359
388
  async function directIngest(projectPath, filename) {
@@ -378,22 +407,6 @@ async function directIngestAll(projectPath) {
378
407
  }
379
408
  catch { }
380
409
  }
381
- async function directLint(projectPath) {
382
- const { lintWiki } = loadServerTool("lint-wiki");
383
- log("🔍", "Running scheduled lint check...");
384
- const result = await lintWiki({ project_path: projectPath, check_type: "all" });
385
- const score = result.health_score ?? 0;
386
- const scoreLabel = score >= 90 ? c.green(`${score}/100`) : score >= 70 ? c.yellow(`${score}/100`) : c.red(`${score}/100`);
387
- log("📊", `Health score: ${scoreLabel}`);
388
- if (result.issues_found?.length > 0) {
389
- const high = result.issues_found.filter((i) => i.severity === "high").length;
390
- const med = result.issues_found.filter((i) => i.severity === "medium").length;
391
- log("⚠️ ", `Issues: 🔴 ${high} high 🟡 ${med} medium`);
392
- for (const rec of result.recommendations?.slice(0, 3) ?? []) {
393
- console.log(c.dim(` → ${rec}`));
394
- }
395
- }
396
- }
397
410
  // ─── Debounce helper ─────────────────────────────────────────────────────────
398
411
  function debounce(fn, ms) {
399
412
  let timer;
@@ -408,6 +421,32 @@ const DEFAULT_CODE_EXTS = new Set([
408
421
  ".py", ".go", ".rb", ".java", ".cs",
409
422
  ".php", ".rs", ".kt", ".swift", ".vue",
410
423
  ]);
424
+ function getNextBridge(current) {
425
+ const chain = ["copilot", "claude", "codex", "api", "none"];
426
+ const idx = chain.indexOf(current);
427
+ return (idx !== -1 && idx + 1 < chain.length) ? chain[idx + 1] : "none";
428
+ }
429
+ async function recordFailover(projectPath, from, to, reason) {
430
+ try {
431
+ const data = await readPidFile(projectPath);
432
+ if (!data)
433
+ return;
434
+ if (!data.failover) {
435
+ data.failover = { count: 0, last_at: null, from: null, to: null, reason: null };
436
+ }
437
+ data.failover.count += 1;
438
+ data.failover.last_at = new Date().toISOString();
439
+ data.failover.from = from;
440
+ data.failover.to = to;
441
+ data.failover.reason = reason.slice(0, 120);
442
+ data.active_bridge = to;
443
+ try {
444
+ await writePidFile(projectPath, data);
445
+ }
446
+ catch { }
447
+ }
448
+ catch { }
449
+ }
411
450
  function getPidFilePath(projectPath) {
412
451
  return node_path_1.default.join(projectPath, ".docuflow", "watch.pid");
413
452
  }
@@ -501,6 +540,7 @@ async function run(options = {}) {
501
540
  pid: process.pid,
502
541
  started_at: new Date().toISOString(),
503
542
  bridge,
543
+ active_bridge: bridge,
504
544
  project_path: projectPath,
505
545
  options: {
506
546
  ai: !!options.ai,
@@ -510,6 +550,13 @@ async function run(options = {}) {
510
550
  lintIntervalHours: options.lintIntervalHours ?? 24,
511
551
  codeExtensions: options.codeExtensions,
512
552
  },
553
+ failover: {
554
+ count: 0,
555
+ last_at: null,
556
+ from: null,
557
+ to: null,
558
+ reason: null,
559
+ },
513
560
  });
514
561
  log("💾", `PID ${process.pid} written to ${c.dim(".docuflow/watch.pid")}`);
515
562
  // ── Watch 1: .docuflow/sources/ ─────────────────────────────────────────
package/dist/index.js CHANGED
@@ -113,6 +113,16 @@ else if (cmd === 'sync') {
113
113
  quiet: hasFlag('--quiet', '-q'),
114
114
  allowDangerousPermissions: hasFlag('--allow-dangerous-permissions'),
115
115
  }));
116
+ // ── review — git change review & improvement suggestions ───────────────────────
117
+ }
118
+ else if (cmd === 'review') {
119
+ Promise.resolve().then(() => __importStar(require('./commands/review'))).then(m => m.run({
120
+ staged: hasFlag('--staged'),
121
+ sinceCommit: getFlagValue('--since-commit'),
122
+ ai: hasFlag('--ai'),
123
+ failOnCritical: hasFlag('--fail-on-critical'),
124
+ quiet: hasFlag('--quiet', '-q'),
125
+ }));
116
126
  }
117
127
  else {
118
128
  console.log(`DocuFlow v${version}`);
@@ -148,6 +158,12 @@ else {
148
158
  console.log(' sync --no-lint Skip health check (faster)');
149
159
  console.log(' sync --fail-on-score N Exit 1 if health score < N (default: 70)');
150
160
  console.log(' sync --quiet Suppress output (CI mode)');
161
+ console.log(' review Review current git changes and suggest improvements');
162
+ console.log(' review --staged Review staged changes only');
163
+ console.log(' review --since-commit REF Review changes since git ref (e.g. HEAD~1)');
164
+ console.log(' review --ai Append Copilot AI review to deterministic findings');
165
+ console.log(' review --fail-on-critical Exit 1 if critical findings are detected');
166
+ console.log(' review --quiet Compact output for CI/scripting');
151
167
  console.log('');
152
168
  console.log('Options:');
153
169
  console.log(' --version, -v Print version number');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doquflow/cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "CLI for setting up Docuflow in your project",
5
5
  "author": "Docuflow <hello@doquflows.dev>",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "build": "tsc && node -e \"const fs=require('fs'),p=require('path'),src=p.join(process.cwd(),'../ui/dist'),dst=p.join(process.cwd(),'ui-dist');if(!fs.existsSync(src)){console.log('Warning: packages/ui/dist not found — run npm run build:ui first');process.exit(0)}fs.mkdirSync(dst,{recursive:true});fs.cpSync(src,dst,{recursive:true,force:true});console.log(' ✓ ui-dist synced from packages/ui/dist ('+(fs.readdirSync(dst).length)+' files at root)')\""
32
32
  },
33
33
  "dependencies": {
34
- "@doquflow/server": "1.2.1",
34
+ "@doquflow/server": "1.3.1",
35
35
  "cors": "^2.8.5",
36
36
  "express": "^4.19.2"
37
37
  },
@@ -0,0 +1 @@
1
+ :root{--df-bg: #0a0a0b;--df-surface: #111113;--df-surface-2: #161619;--df-surface-3: #1d1d22;--df-border: #1a1a1f;--df-border-2: #26262c;--df-border-3: #2c2c33;--df-border-hi: #3a3a44;--df-text: #ededee;--df-text-2: #d4d4d8;--df-text-3: #a1a1aa;--df-text-4: #6b6b74;--df-accent: #6366f1;--df-accent-hover: #5558e3;--df-accent-2: #818cf8;--df-accent-text: #a5b4fc;--df-accent-soft: rgba(99,102,241,.14);--df-accent-glow: rgba(99,102,241,.18);--df-accent-ring: rgba(99,102,241,.4);--df-accent-halo: rgba(99,102,241,.08);--df-green: #10b981;--df-green-text: #34d399;--df-green-soft: rgba(16,185,129,.12);--df-green-ring: rgba(16,185,129,.3);--df-amber: #f59e0b;--df-amber-text: #fbbf24;--df-amber-soft: rgba(245,158,11,.12);--df-amber-ring: rgba(245,158,11,.3);--df-red: #ef4444;--df-red-text: #f87171;--df-red-soft: rgba(239,68,68,.12);--df-red-ring: rgba(239,68,68,.3);--df-pink: #ec4899;--df-pink-text: #f472b6;--df-pink-soft: rgba(236,72,153,.12);--df-pink-ring: rgba(236,72,153,.3);--df-font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--df-font-serif: "Source Serif 4", "Iowan Old Style", Georgia, serif;--df-font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;--df-r-sm: 4px;--df-r-md: 6px;--df-r-lg: 8px;--df-r-xl: 10px;--df-r-pill: 999px;--df-h-btn: 28px;--df-h-input: 30px;--df-h-bar: 44px;--df-w-rail: 60px;--df-w-rail-detail: 280px;--df-w-tree: 260px;--df-ease: cubic-bezier(.2,.7,.3,1);--df-fast: .12s;--df-base: .2s;--df-slow: .35s;--df-content-max: 720px;--df-content-pad: 56px}*,*:before,*:after{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--df-bg);color:var(--df-text);font-family:var(--df-font-sans);font-feature-settings:"cv11","ss01";font-size:13px;line-height:1.5;letter-spacing:-.005em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}button{font-family:inherit}.df-mono{font-family:var(--df-font-mono);font-feature-settings:"cv01","cv02"}.df-serif{font-family:var(--df-font-serif)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:var(--df-border-2);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--df-border-3)}::-webkit-scrollbar-track{background:transparent}::selection{background:var(--df-accent-soft);color:var(--df-text)}.df-app{width:100vw;height:100vh;display:flex;flex-direction:column;overflow:hidden}.df-app__main{flex:1;display:flex;overflow:hidden;min-height:0}.df-view{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;background:var(--df-bg)}@keyframes df-fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes df-spin{to{transform:rotate(360deg)}}@keyframes df-pulse{0%,to{opacity:.4}50%{opacity:1}}@keyframes df-blink{0%,49%{opacity:1}50%,to{opacity:0}}@keyframes df-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}@keyframes df-bar-grow{0%{transform:scaleY(0)}to{transform:scaleY(1)}}@keyframes df-page-enter{0%{opacity:0;transform:translate(8px)}to{opacity:1;transform:translate(0)}}.df-anim-fade{animation:df-fade-in var(--df-base) var(--df-ease) both}.df-anim-page{animation:df-page-enter var(--df-slow) var(--df-ease) both}.df-pill{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;border-radius:var(--df-r-sm);font-size:11px;font-weight:500;font-family:var(--df-font-sans);background:var(--df-surface-3);border:1px solid var(--df-border-3);color:var(--df-text-3);white-space:nowrap}.df-pill--accent{background:var(--df-accent-soft);border-color:var(--df-accent-ring);color:var(--df-accent-text)}.df-pill--green{background:var(--df-green-soft);border-color:var(--df-green-ring);color:var(--df-green-text)}.df-pill--amber{background:var(--df-amber-soft);border-color:var(--df-amber-ring);color:var(--df-amber-text)}.df-pill--red{background:var(--df-red-soft);border-color:var(--df-red-ring);color:var(--df-red-text)}.df-pill--pink{background:var(--df-pink-soft);border-color:var(--df-pink-ring);color:var(--df-pink-text)}.df-pill__dot{width:5px;height:5px;border-radius:50%;background:currentColor}.df-btn{display:inline-flex;align-items:center;gap:6px;height:var(--df-h-btn);padding:0 10px;border-radius:var(--df-r-md);background:var(--df-surface-3);border:1px solid var(--df-border-3);color:var(--df-text);font-size:12px;font-weight:500;letter-spacing:-.005em;cursor:pointer;transition:background var(--df-fast),border-color var(--df-fast),color var(--df-fast);white-space:nowrap}.df-btn:hover{background:var(--df-surface-2)}.df-btn--primary{background:var(--df-accent);border-color:var(--df-accent);color:#fff}.df-btn--primary:hover{background:var(--df-accent-hover);border-color:var(--df-accent-hover)}.df-btn--ghost{background:transparent;border-color:transparent;color:var(--df-text-3)}.df-btn--ghost:hover{background:var(--df-surface-3);color:var(--df-text)}.df-btn--active{background:var(--df-surface-2);border-color:var(--df-border-hi)}.df-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:var(--df-r-sm);background:var(--df-surface-3);border:1px solid var(--df-border-3);color:var(--df-text-3);font:11px/1 var(--df-font-mono)}.df-btn--primary .df-kbd{background:#ffffff2e;border:none;color:#fff}.df-card{background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl)}.df-card--padded{padding:18px}.df-card--rows>*+*{border-top:1px solid var(--df-border)}.df-card__row{display:flex;align-items:center;gap:14px;padding:12px 16px;font-size:12px}.df-eyebrow{font-size:11px;color:var(--df-text-4);text-transform:uppercase;letter-spacing:.08em}.df-h1{margin:0;font-size:24px;font-weight:600;letter-spacing:-.02em;color:var(--df-text)}.df-h1--display{font-size:26px}.df-h1--serif{font-family:var(--df-font-serif);font-weight:400;font-size:32px;letter-spacing:-.02em;line-height:1.15}.df-subtle{color:var(--df-text-4);font-size:13px}.df-topbar{height:var(--df-h-bar);display:flex;align-items:center;gap:12px;padding:0 16px;background:var(--df-bg);border-bottom:1px solid var(--df-border);flex-shrink:0;flex-wrap:nowrap;white-space:nowrap;overflow:hidden;min-width:0}.df-topbar>*{flex-shrink:0}.df-topbar__crumb{display:flex;align-items:center;gap:8px;font-size:13px}.df-topbar__sep{color:var(--df-border-3)}.df-topbar__meta{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--df-text-4);min-width:0;overflow:hidden;text-overflow:ellipsis}.df-topbar__meta>span:last-child{overflow:hidden;text-overflow:ellipsis}.df-topbar__divider{width:1px;height:18px;background:var(--df-border);margin:0 4px}.df-topbar__avatar{width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,var(--df-pink),var(--df-accent))}.df-rail{width:var(--df-w-rail);background:var(--df-bg);border-right:1px solid var(--df-border);display:flex;flex-direction:column;align-items:center;padding:12px 0;gap:2px;flex-shrink:0}.df-rail__logo{width:32px;height:32px;margin-bottom:14px;border-radius:var(--df-r-lg);background:linear-gradient(135deg,var(--df-accent),#a855f7);display:flex;align-items:center;justify-content:center;color:#fff}.df-rail__item{position:relative;width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;border-radius:var(--df-r-lg);color:var(--df-text-4);cursor:pointer;transition:background var(--df-fast),color var(--df-fast)}.df-rail__item:hover{color:var(--df-text-3)}.df-rail__item--active{background:var(--df-surface-3);color:var(--df-text)}.df-rail__item--active:before{content:"";position:absolute;left:-6px;top:10px;bottom:10px;width:2px;border-radius:1px;background:var(--df-accent)}.df-rail__spacer{flex:1}.df-rail__groups{width:100%;padding:0 8px}.df-rail__group{margin-bottom:8px}.df-rail__group-label{font-size:9px;letter-spacing:.12em;color:var(--df-text-5);text-transform:uppercase;text-align:center;margin-bottom:4px}.df-rail__divider{width:20px;height:1px;margin:4px auto;background:var(--df-border-3)}.df-rail__project-picker{width:32px;margin-bottom:4px}.df-rail__project-picker-wrap{position:relative;width:32px;height:32px;border-radius:var(--df-r-lg);background:var(--df-surface-3);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background var(--df-fast)}.df-rail__project-picker-wrap:hover,.df-rail__project-picker-wrap.open{background:var(--df-surface-2)}.df-rail__project-picker-caret{position:absolute;right:2px;top:50%;transform:translateY(-50%);width:0;height:0;border:3.5px solid var(--df-text-4);border-width:3.5px 0 3.5px 4px;transition:transform var(--df-fast)}.df-rail__project-picker-wrap.open .df-rail__project-picker-caret{transform:translateY(-50%) rotate(-90deg)}.df-rail__project-picker-menu{position:absolute;top:38px;left:0;width:220px;padding:4px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-lg);box-shadow:var(--df-shadow);z-index:100;display:none}.df-rail__project-picker-wrap.open+.df-rail__project-picker-menu,.df-rail__project-picker-menu--open{display:block}.df-rail__project-picker-search{width:100%;padding:7px 10px;background:var(--df-bg);border:1px solid var(--df-border-2);border-radius:var(--df-r-sm);font-size:12px;color:var(--df-text);margin-bottom:4px}.df-rail__project-picker-list{max-height:180px;overflow-y:auto}.df-rail__project-picker-item{display:flex;align-items:center;gap:8px;padding:7px 8px;font-size:12px;color:var(--df-text-3);cursor:pointer;border-radius:var(--df-r-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.df-rail__project-picker-item:hover,.df-rail__project-picker-item--selected{background:var(--df-accent-halo);color:var(--df-accent-text)}.df-rail__project-picker-icon{width:16px;height:16px;flex-shrink:0}.df-rail__project-picker-item-badge{font-size:10px;padding:1px 6px;border-radius:var(--df-r-pill);background:var(--df-accent-halo);color:var(--df-accent-text);border:1px solid var(--df-accent-ring);white-space:nowrap;flex-shrink:0}.df-rail__project-picker-item-name{flex:1;overflow:hidden;text-overflow:ellipsis}.df-rail__project-picker-form{padding:4px;border-top:1px solid var(--df-border);margin-top:4px}.df-rail__project-picker-name{width:100%;padding:6px 8px;margin-bottom:4px;background:var(--df-bg);border:1px solid var(--df-border-2);border-radius:var(--df-r-sm);font-size:12px;color:var(--df-text);outline:none}.df-rail__project-picker-name:focus{border-color:var(--df-accent-ring);box-shadow:0 0 0 3px var(--df-accent-halo)}.df-rail__project-picker-select{width:100%;padding:6px 8px;margin-bottom:4px;background:var(--df-bg);border:1px solid var(--df-border-2);border-radius:var(--df-r-sm);font-size:12px;color:var(--df-text)}.df-rail__project-picker-create{width:100%;padding:5px;margin-top:4px;background:var(--df-accent);border-radius:var(--df-r-sm);font-size:11px;font-weight:600;color:#fff;cursor:pointer}.df-rail__project-picker-create:hover{opacity:.9}.df-rail__tip{pointer-events:none;position:absolute;left:46px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-sm);padding:3px 8px;font-size:11px;color:var(--df-text-2);white-space:nowrap;z-index:200;box-shadow:var(--df-shadow);transform:translate(-6px);opacity:0;transition:all var(--df-fast) var(--df-ease)}.df-rail__item:hover .df-rail__tip{transform:translate(0);opacity:1}.df-rail__item:hover .df-rail__tip--lg{left:auto;right:-70px}.df-rail--collapsed{width:64px!important}.df-rail--collapsed .df-rail__groups,.df-rail--collapsed .df-rail__project-picker{display:none}.df-rail--collapsed .df-rail__item:not(.df-rail__project-picker-wrap):hover .df-rail__tip{display:none}.df-field{display:block}.df-field__label{font-size:11px;color:var(--df-text-4);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}.df-field__value{padding:10px 12px;background:var(--df-surface);border:1px solid var(--df-border-2);border-radius:var(--df-r-lg);font-size:13px;color:var(--df-text)}.df-field__value--mono{font-family:var(--df-font-mono)}.df-code{background:var(--df-surface-3);border:1px solid var(--df-border-2);padding:1px 6px;border-radius:var(--df-r-sm);font-family:var(--df-font-mono);font-size:.82em;color:var(--df-accent-text)}.df-status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--df-text-4)}.df-status-dot--live{background:var(--df-green);animation:df-pulse 1.4s infinite}.df-status-dot--paused{background:var(--df-text-4)}.df-ask{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--df-bg)}.df-ask__header{padding:40px var(--df-content-pad) 24px;border-bottom:1px solid var(--df-border)}.df-ask__eyebrow{display:flex;align-items:center;gap:8px;margin-bottom:10px}.df-ask__title-accent{color:var(--df-accent-text)}.df-ask__intro{margin:6px 0 0;color:var(--df-text-4);font-size:13px}.df-ask__box-wrap{margin-top:24px;position:relative}.df-ask__box{display:flex;align-items:center;gap:10px;padding:14px 18px;border-radius:var(--df-r-xl);background:var(--df-surface);border:1px solid var(--df-border-2);transition:all var(--df-base) var(--df-ease)}.df-ask__box--active{border-color:var(--df-accent-ring);box-shadow:0 0 0 4px var(--df-accent-halo)}.df-ask__box-input{flex:1;font-size:15px;color:var(--df-text);min-height:22px}.df-ask__placeholder{color:var(--df-text-4)}.df-ask__caret{display:inline-block;width:2px;height:16px;background:var(--df-accent-text);vertical-align:-2px;margin-left:1px;animation:df-blink .8s infinite}.df-ask__run{height:28px;padding:0 14px;border-radius:var(--df-r-md);background:var(--df-accent);color:#fff;border:none;font-size:12px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:5px}.df-ask__run:hover{background:var(--df-accent-hover)}.df-ask__reset{padding:4px 10px;border-radius:var(--df-r-md);background:transparent;color:var(--df-text-4);border:1px solid var(--df-border-3);font-size:11px;cursor:pointer}.df-ask__chips{margin-top:12px;display:flex;flex-wrap:wrap;gap:6px}.df-chip{padding:6px 11px;border-radius:var(--df-r-pill);background:transparent;border:1px solid var(--df-border-2);color:var(--df-text-3);font-size:12px;cursor:pointer;font-family:inherit;transition:all var(--df-fast)}.df-chip:hover{background:var(--df-surface-2);border-color:var(--df-border-hi);color:var(--df-text)}.df-ask__body{flex:1;display:flex;overflow:hidden;min-height:0}.df-ask__main{flex:1;padding:28px var(--df-content-pad) 56px;min-width:0;overflow:auto}.df-search__head{display:flex;align-items:center;gap:10px;color:var(--df-accent-text);font-size:13px;margin-bottom:18px}.df-search__spinner{width:14px;height:14px;border:2px solid var(--df-border-3);border-top-color:var(--df-accent);border-radius:50%;animation:df-spin .7s linear infinite}.df-search__stage{display:flex;align-items:center;gap:10px;padding:8px 0;font-size:12px;color:var(--df-text-4)}.df-search__stage--done{color:var(--df-green)}.df-search__skel{margin-top:24px;display:flex;flex-direction:column;gap:10px}.df-search__skel-bar{height:12px;border-radius:var(--df-r-sm);background:linear-gradient(90deg,var(--df-surface-2),var(--df-surface-3),var(--df-surface-2));background-size:200% 100%;animation:df-shimmer 1.4s infinite linear}.df-answer{max-width:var(--df-content-max)}.df-answer__meta{display:flex;align-items:center;gap:10px;margin-bottom:20px}.df-answer__doc{font-family:var(--df-font-serif);border-top:1px solid var(--df-border);border-bottom:1px solid var(--df-border);padding:28px 0 32px}.df-answer__h{margin:0 0 22px;font-size:24px;font-weight:400;color:var(--df-text);letter-spacing:-.01em;line-height:1.25}.df-answer__prose{font-size:16px;line-height:1.72;color:var(--df-text-2)}.df-answer__prose p{margin:0 0 16px}.df-answer__prose strong{color:var(--df-text);font-weight:600}.df-answer__sup{color:var(--df-accent-text);font-weight:600;margin-left:2px;font-family:var(--df-font-sans);font-size:.7em}.df-answer__list{margin:0 0 18px;padding-left:0;list-style:none}.df-answer__li{position:relative;padding:8px 0 8px 22px;margin-left:6px;border-left:1px solid var(--df-border-2)}.df-answer__num{position:absolute;left:-10px;top:11px;width:18px;height:18px;border-radius:50%;background:var(--df-bg);border:1px solid var(--df-border-hi);color:var(--df-accent-text);font-size:10.5px;font-weight:600;display:flex;align-items:center;justify-content:center;font-family:var(--df-font-sans)}.df-cites{margin-top:24px;font-family:var(--df-font-serif)}.df-cites__row{display:flex;gap:14px;padding:12px 0;border-top:1px solid var(--df-border)}.df-cites__row:first-child{border-top:none}.df-cites__num{width:24px;color:var(--df-accent-text);font-weight:600;font-family:var(--df-font-sans);font-size:12px;margin-top:2px}.df-cites__title{font-size:15px;color:var(--df-text)}.df-cites__path{font-family:var(--df-font-mono);font-size:11px;color:var(--df-text-4);margin-top:2px}.df-related{margin-top:28px;padding-top:20px;border-top:1px solid var(--df-border)}.df-related__list{display:flex;flex-direction:column;gap:6px}.df-related__item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:transparent;border:1px solid var(--df-border);border-radius:var(--df-r-md);color:var(--df-text-3);font-size:13px;cursor:pointer;font-family:inherit;text-align:left;transition:border-color var(--df-fast),color var(--df-fast)}.df-related__item:hover{border-color:var(--df-border-3);color:var(--df-text)}.df-actions{margin-top:24px;display:flex;gap:8px}.df-cites-rail{width:300px;border-left:1px solid var(--df-border);padding:28px 20px;background:#0c0c0e;flex-shrink:0;overflow:auto}.df-cites-rail__list{display:flex;flex-direction:column;gap:8px}.df-cites-card{padding:12px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-lg);cursor:pointer;transition:border-color var(--df-fast)}.df-cites-card:hover{border-color:var(--df-border-3)}.df-cites-card__head{display:flex;align-items:center;gap:6px;margin-bottom:6px}.df-cites-card__num{width:18px;height:18px;border-radius:5px;background:var(--df-accent-soft);color:var(--df-accent-text);font-size:10px;font-weight:600;display:flex;align-items:center;justify-content:center;font-family:var(--df-font-mono)}.df-cites-card__title{font-size:13px;font-weight:500;color:var(--df-text);flex:1}.df-cites-card__path{font-size:11px;color:var(--df-text-4);font-family:var(--df-font-mono);margin-bottom:8px}.df-cites-card__foot{display:flex;align-items:center;gap:6px;font-size:10px}.df-activity__row{display:flex;align-items:center;gap:14px;padding:10px 14px;font-size:12px;border-top:1px solid var(--df-border)}.df-activity__row:first-child{border-top:none}.df-activity__time{width:48px;color:var(--df-text-4);font-size:11px;font-family:var(--df-font-mono)}.df-activity__target{flex:1;color:var(--df-text-3);font-family:var(--df-font-mono)}.df-activity__delta{color:var(--df-text-4)}.df-wiki{flex:1;display:flex;overflow:hidden}.df-tree{width:var(--df-w-tree);border-right:1px solid var(--df-border);padding:14px 10px;overflow:auto;flex-shrink:0}.df-tree__filter{display:flex;align-items:center;gap:6px;padding:6px 8px;margin-bottom:8px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-md);font-size:12px;color:var(--df-text-4)}.df-tree__heading{font-size:10px;color:var(--df-text-4);text-transform:uppercase;letter-spacing:.08em;padding:8px 8px 4px}.df-tree__row{display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:var(--df-r-sm);font-size:12px;color:var(--df-text-3);cursor:pointer;transition:background var(--df-fast)}.df-tree__row:hover{background:var(--df-surface-2)}.df-tree__row--active{background:var(--df-accent-soft);color:var(--df-accent-text)}.df-tree__row--active:hover{background:var(--df-accent-soft)}.df-tree__row--stale{color:var(--df-amber-text)}.df-tree__caret{width:11px;display:flex;color:var(--df-text-4)}.df-tree__indicator{width:5px;height:5px;border-radius:50%}.df-tree__indicator--stale{background:var(--df-amber)}.df-tree__indicator--highlight{background:var(--df-accent)}.df-page{flex:1;overflow:auto;padding:40px var(--df-content-pad);max-width:780px;font-family:var(--df-font-serif)}.df-page__crumb{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--df-text-4);margin-bottom:10px;font-family:var(--df-font-sans)}.df-page__crumb-active{color:var(--df-text-3)}.df-page__meta{display:flex;align-items:center;gap:8px;margin:14px 0 28px;font-family:var(--df-font-sans)}.df-page__path{font-size:11px;color:var(--df-text-4);font-family:var(--df-font-mono)}.df-page__prose{font-size:16px;line-height:1.72;color:var(--df-text-2)}.df-page__prose p:first-child{margin-top:0}.df-page__prose ul{padding-left:18px;line-height:1.7}.df-page__prose a{color:var(--df-accent-text);text-decoration:none}.df-page__prose a:hover{text-decoration:underline}.df-page__h3{font-family:var(--df-font-sans);font-size:13px;font-weight:600;color:var(--df-text);margin-top:28px;text-transform:uppercase;letter-spacing:.08em}.df-page__kv-row{display:flex;padding:8px 14px;border-top:1px solid var(--df-border);font-size:13px}.df-page__kv-row:first-child{border-top:none}.df-page__kv-key{width:120px;color:var(--df-accent-text);font-family:var(--df-font-mono)}.df-page__kv-val{color:var(--df-text-3)}.df-graph{flex:1;display:flex;overflow:hidden}.df-graph__canvas{flex:1;position:relative;overflow:hidden}.df-graph__grid{position:absolute;top:0;right:0;bottom:0;left:0;background-image:radial-gradient(circle,var(--df-border) 1px,transparent 1px);background-size:24px 24px;opacity:.5}.df-graph__legend{position:absolute;top:16px;left:16px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-lg);padding:10px 12px;display:flex;flex-direction:column;gap:6px;font-size:11px;z-index:2}.df-graph__legend-row{display:flex;align-items:center;gap:7px;color:var(--df-text-3);text-transform:capitalize}.df-graph__legend-swatch{width:8px;height:8px;border-radius:50%}.df-graph__controls{position:absolute;top:16px;right:16px;display:flex;gap:6px;z-index:2}.df-graph__svg{position:absolute;top:0;right:0;bottom:0;left:0}.df-graph__detail{width:var(--df-w-rail-detail);border-left:1px solid var(--df-border);padding:24px 20px;flex-shrink:0;overflow:auto}.df-graph__connection{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:var(--df-r-sm);cursor:pointer;font-size:12px;color:var(--df-text-3)}.df-graph__connection:hover{background:var(--df-surface-2)}.df-graph__connection-dot{width:6px;height:6px;border-radius:50%}.df-health{flex:1;overflow:auto;padding:40px var(--df-content-pad)}.df-health__grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:24px 0 32px}.df-stat{padding:18px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl)}.df-stat__num{font-size:34px;font-weight:600;letter-spacing:-.02em;margin-top:6px}.df-stat__num--green{color:var(--df-green)}.df-stat__num--amber{color:var(--df-amber-text)}.df-stat__num--red{color:var(--df-red)}.df-stat__bar{height:4px;background:var(--df-border);border-radius:2px;margin-top:10px;overflow:hidden}.df-stat__bar-fill{height:100%;background:linear-gradient(90deg,var(--df-green),var(--df-green-text));border-radius:2px}.df-stat__caption{font-size:11px;color:var(--df-text-4);margin-top:10px}.df-trend{background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl);padding:20px;margin-bottom:24px}.df-trend__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.df-trend__title{font-size:13px;font-weight:500}.df-trend__bars{display:flex;align-items:flex-end;gap:6px;height:80px}.df-trend__bar{flex:1;border-radius:3px 3px 0 0;opacity:.85;transform-origin:bottom;animation:df-bar-grow .5s var(--df-ease) both}.df-issues__row{display:flex;align-items:center;gap:14px;padding:14px 16px;border-top:1px solid var(--df-border)}.df-issues__row:first-child{border-top:none}.df-issues__page{font-size:13px;font-weight:500;color:var(--df-text)}.df-issues__msg{flex:1;font-size:12px;color:var(--df-text-3)}.df-issues__age{font-size:11px;color:var(--df-text-4);font-family:var(--df-font-mono)}.df-sync{flex:1;overflow:auto;padding:40px var(--df-content-pad)}.df-sync__head{display:flex;align-items:center;justify-content:space-between}.df-sync__cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:24px 0 28px}.df-sync-card{padding:16px;background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl)}.df-sync-card__body{margin-top:8px;font-weight:500;display:flex;align-items:center;gap:8px}.df-sync-card__sub{font-size:11px;color:var(--df-text-4);margin-top:6px;font-family:var(--df-font-mono)}.df-sync-card__sub--green{color:var(--df-green);font-family:inherit}.df-timeline{background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl);padding:8px 0}.df-timeline__row{display:flex;align-items:center;gap:14px;padding:10px 18px;position:relative}.df-timeline__time{width:48px;font-size:11px;color:var(--df-text-4);font-family:var(--df-font-mono)}.df-timeline__line{position:absolute;left:78px;top:0;bottom:0;width:1px;background:var(--df-border)}.df-timeline__dot{position:relative;width:9px;height:9px;border-radius:50%;background:var(--df-border-3);z-index:1}.df-timeline__dot--commit{background:var(--df-pink)}.df-timeline__dot--sync{background:var(--df-accent)}.df-timeline__dot--done{background:var(--df-green);box-shadow:0 0 0 3px #10b9812e}.df-timeline__msg{flex:1;font-size:13px;color:var(--df-text-2)}.df-timeline__msg--done{color:var(--df-green)}.df-onboard{flex:1;display:flex;flex-direction:column;overflow:auto}.df-onboard__center{flex:1;display:flex;align-items:center;justify-content:center;padding:48px 32px}.df-onboard__inner{width:100%;max-width:640px}.df-onboard__steps{display:flex;align-items:center;gap:8px;margin-bottom:32px;font-size:12px}.df-onboard__step{display:flex;align-items:center;gap:8px;color:var(--df-text-4)}.df-onboard__step--active,.df-onboard__step--done{color:var(--df-text)}.df-onboard__step-num{width:22px;height:22px;border-radius:50%;background:var(--df-surface-3);color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600}.df-onboard__step--active .df-onboard__step-num{background:var(--df-accent)}.df-onboard__step--done .df-onboard__step-num{background:var(--df-green)}.df-onboard__step-line{flex:1;height:1px;background:var(--df-border)}.df-onboard__step-line--done{background:var(--df-green)}.df-onboard__intro{color:var(--df-text-4);margin:6px 0 24px}.df-domains{display:grid;grid-template-columns:1fr 1fr;gap:10px}.df-domain{text-align:left;padding:18px;border-radius:var(--df-r-xl);background:var(--df-surface);border:1px solid var(--df-border);cursor:pointer;font-family:inherit;color:var(--df-text);transition:all var(--df-fast)}.df-domain--selected{background:var(--df-accent-halo);border-color:var(--df-accent)}.df-domain__icon{width:32px;height:32px;border-radius:var(--df-r-lg);background:var(--df-surface-3);display:flex;align-items:center;justify-content:center;margin-bottom:10px;color:var(--df-text-3)}.df-domain--selected .df-domain__icon{background:var(--df-accent-soft);color:var(--df-accent-text)}.df-domain__label{font-weight:500;font-size:14px}.df-domain__desc{font-size:12px;color:var(--df-text-4);margin-top:4px}.df-onboard__fields{display:flex;flex-direction:column;gap:14px}.df-dropzone{padding:32px;border:1.5px dashed var(--df-border-3);border-radius:12px;text-align:center;background:#0c0c0e}.df-dropzone__title{margin-top:12px;font-size:14px;color:var(--df-text)}.df-dropzone__sub{margin-top:4px;font-size:12px;color:var(--df-text-4)}.df-init-log{background:var(--df-surface);border:1px solid var(--df-border);border-radius:var(--df-r-xl);padding:16px;font-family:var(--df-font-mono);font-size:12px}.df-init-log__line{display:flex;gap:10px;padding:4px 0;color:var(--df-text-3)}.df-init-log__mark--ok{color:var(--df-green)}.df-init-log__mark--run{color:var(--df-accent-text)}.df-onboard__nav{display:flex;justify-content:space-between;margin-top:32px}