autonomous-flow-daemon 1.6.0 → 1.9.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +85 -85
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -266
  5. package/mcp-config.json +10 -10
  6. package/package.json +4 -2
  7. package/src/adapters/index.ts +370 -370
  8. package/src/cli.ts +162 -127
  9. package/src/commands/benchmark.ts +187 -187
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/evolution.ts +84 -1
  13. package/src/commands/fix.ts +158 -158
  14. package/src/commands/lang.ts +41 -41
  15. package/src/commands/plugin.ts +110 -0
  16. package/src/commands/restart.ts +14 -14
  17. package/src/commands/score.ts +276 -276
  18. package/src/commands/start.ts +155 -155
  19. package/src/commands/status.ts +157 -157
  20. package/src/commands/stop.ts +68 -68
  21. package/src/commands/suggest.ts +211 -0
  22. package/src/commands/sync.ts +329 -16
  23. package/src/constants.ts +32 -32
  24. package/src/core/boast.ts +280 -280
  25. package/src/core/config.ts +49 -49
  26. package/src/core/correlation-engine.ts +265 -0
  27. package/src/core/db.ts +145 -117
  28. package/src/core/discovery.ts +65 -65
  29. package/src/core/federation.ts +129 -0
  30. package/src/core/hologram/engine.ts +71 -71
  31. package/src/core/hologram/fallback.ts +11 -11
  32. package/src/core/hologram/go-extractor.ts +203 -0
  33. package/src/core/hologram/incremental.ts +227 -227
  34. package/src/core/hologram/py-extractor.ts +132 -132
  35. package/src/core/hologram/rust-extractor.ts +244 -0
  36. package/src/core/hologram/ts-extractor.ts +406 -320
  37. package/src/core/hologram/types.ts +27 -25
  38. package/src/core/hologram.ts +73 -71
  39. package/src/core/i18n/messages.ts +309 -309
  40. package/src/core/locale.ts +88 -88
  41. package/src/core/log-rotate.ts +33 -33
  42. package/src/core/log-utils.ts +38 -38
  43. package/src/core/lru-map.ts +61 -61
  44. package/src/core/notify.ts +74 -74
  45. package/src/core/plugin-manager.ts +225 -0
  46. package/src/core/rule-suggestion.ts +127 -0
  47. package/src/core/validator-generator.ts +224 -0
  48. package/src/core/workspace.ts +28 -28
  49. package/src/daemon/client.ts +78 -65
  50. package/src/daemon/event-batcher.ts +108 -108
  51. package/src/daemon/guards.ts +13 -13
  52. package/src/daemon/http-routes.ts +376 -293
  53. package/src/daemon/mcp-handler.ts +575 -270
  54. package/src/daemon/mcp-subscriptions.ts +81 -0
  55. package/src/daemon/mesh.ts +51 -0
  56. package/src/daemon/server.ts +655 -590
  57. package/src/daemon/types.ts +121 -100
  58. package/src/daemon/workspace-map.ts +104 -92
  59. package/src/platform.ts +60 -60
  60. package/src/version.ts +15 -15
  61. package/README.ko.md +0 -266
@@ -0,0 +1,404 @@
1
+ import { getDaemonInfo, daemonRequest } from "../daemon/client";
2
+ import { fmtNum, visualWidth } from "../core/boast";
3
+
4
+ // ── Types ────────────────────────────────────────────────────────────────────
5
+
6
+ interface HologramEntry {
7
+ requests: number;
8
+ originalChars: number;
9
+ hologramChars: number;
10
+ savings: number;
11
+ }
12
+
13
+ interface HologramDailyRow {
14
+ date: string;
15
+ requests: number;
16
+ originalChars: number;
17
+ hologramChars: number;
18
+ }
19
+
20
+ interface HologramScore {
21
+ lifetime: HologramEntry;
22
+ today: HologramEntry | null;
23
+ daily: HologramDailyRow[];
24
+ }
25
+
26
+ interface CtxSavingsRow {
27
+ date: string;
28
+ type: string;
29
+ requests: number;
30
+ original_chars: number;
31
+ saved_chars: number;
32
+ }
33
+
34
+ interface CtxSavingsLifetimeRow {
35
+ type: string;
36
+ total_requests: number;
37
+ total_original_chars: number;
38
+ total_saved_chars: number;
39
+ }
40
+
41
+ interface ScoreData {
42
+ uptime: number;
43
+ totalEvents: number;
44
+ hologram: HologramScore;
45
+ ctxSavings: {
46
+ daily: CtxSavingsRow[];
47
+ lifetime: CtxSavingsLifetimeRow[];
48
+ };
49
+ }
50
+
51
+ // ── Locale ───────────────────────────────────────────────────────────────────
52
+
53
+ function detectKorean(): boolean {
54
+ const lang = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? "";
55
+ if (/ko[_\-]/i.test(lang)) return true;
56
+ try {
57
+ return Intl.DateTimeFormat().resolvedOptions().locale.startsWith("ko");
58
+ } catch { return false; }
59
+ }
60
+
61
+ const isKo = detectKorean();
62
+
63
+ const T = isKo ? {
64
+ title: "afd 토큰 대시보드",
65
+ todaySavings: "오늘의 절약",
66
+ lifetimeRoi: "누적 ROI & 분류",
67
+ weekHistory: "최근 7일 내역",
68
+ systemStatus: "시스템 상태",
69
+ startTracking:"afd_read 또는 afd_hologram 사용 시 추적 시작",
70
+ noHistory: "아직 일별 기록이 없습니다",
71
+ totalSaved: "총 절약량",
72
+ estValue: "추정 가치",
73
+ hologram: "홀로그램",
74
+ wsmap: "워크스페이스 맵",
75
+ pinpoint: "핀포인트",
76
+ requests: "요청",
77
+ uptime: "가동시간",
78
+ events: "이벤트",
79
+ updated: "갱신",
80
+ exitHint: "Ctrl+C로 종료",
81
+ labelOrig: "원본 ",
82
+ labelAct: "실제 ",
83
+ labelSaved: "절약 ",
84
+ savedSuffix: "절약됨",
85
+ todayLabel: "오늘",
86
+ } : {
87
+ title: "afd token dashboard",
88
+ todaySavings: "TODAY'S SAVINGS",
89
+ lifetimeRoi: "LIFETIME ROI & BREAKDOWN",
90
+ weekHistory: "7-DAY HISTORY",
91
+ systemStatus: "SYSTEM STATUS",
92
+ startTracking:"Use afd_read or afd_hologram to start tracking",
93
+ noHistory: "No daily history yet",
94
+ totalSaved: "Total Saved",
95
+ estValue: "Est. Value",
96
+ hologram: "Hologram",
97
+ wsmap: "W/S Map",
98
+ pinpoint: "Pinpoint",
99
+ requests: "Requests",
100
+ uptime: "Uptime",
101
+ events: "Events",
102
+ updated: "Updated",
103
+ exitHint: "Press Ctrl+C to exit",
104
+ labelOrig: "Original ",
105
+ labelAct: "Actual ",
106
+ labelSaved: "Saved ",
107
+ savedSuffix: "saved",
108
+ todayLabel: "Today",
109
+ };
110
+
111
+ // ── ANSI ──────────────────────────────────────────────────────────────────────
112
+
113
+ const C = {
114
+ reset: "\x1b[0m",
115
+ bold: "\x1b[1m",
116
+ dim: "\x1b[2m",
117
+ red: "\x1b[31m",
118
+ green: "\x1b[32m",
119
+ yellow: "\x1b[33m",
120
+ cyan: "\x1b[36m",
121
+ };
122
+
123
+ const CHARS_PER_TOKEN = 3.5;
124
+ const W = 58;
125
+ const INNER = W - 2;
126
+ const HBAR = "─".repeat(W);
127
+
128
+ function vw(s: string): number {
129
+ return visualWidth(s.replace(/\x1b\[[0-9;]*m/g, ""));
130
+ }
131
+
132
+ function row(content: string): string {
133
+ const pad = Math.max(0, INNER - vw(content));
134
+ return `│${content}${" ".repeat(pad)}│`;
135
+ }
136
+
137
+ function divider(): string {
138
+ return `├${HBAR}┤`;
139
+ }
140
+
141
+ function formatK(chars: number): string {
142
+ const tok = chars / CHARS_PER_TOKEN;
143
+ if (tok >= 1_000_000) return `${(tok / 1_000_000).toFixed(1)}M tok`;
144
+ if (tok >= 1_000) return `${(tok / 1_000).toFixed(1)}K tok`;
145
+ return `${Math.round(tok)} tok`;
146
+ }
147
+
148
+ function formatUptime(s: number): string {
149
+ if (s < 60) return `${s}s`;
150
+ if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
151
+ return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
152
+ }
153
+
154
+ function weekday(dateStr: string, todayStr: string): string {
155
+ if (dateStr === todayStr) return T.todayLabel;
156
+ try {
157
+ const locale = isKo ? "ko" : "en";
158
+ return new Date(dateStr + "T12:00:00").toLocaleDateString(locale, { weekday: "short" });
159
+ } catch { return ""; }
160
+ }
161
+
162
+ // ── Bar helpers ───────────────────────────────────────────────────────────────
163
+
164
+ function barSaved(pct: number, width: number): string {
165
+ const filled = Math.min(width, Math.round((pct / 100) * width));
166
+ const empty = width - filled;
167
+ return `${C.green}${"▓".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
168
+ }
169
+
170
+ function barActual(pct: number, width: number): string {
171
+ const filled = Math.min(width, Math.round((pct / 100) * width));
172
+ const empty = width - filled;
173
+ return `${C.yellow}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
174
+ }
175
+
176
+ function barOriginal(width: number): string {
177
+ return `${C.dim}${"█".repeat(width)}${C.reset}`;
178
+ }
179
+
180
+ // ── Render ────────────────────────────────────────────────────────────────────
181
+
182
+ function render(score: ScoreData, lastUpdated: string): void {
183
+ process.stdout.write("\x1b[2J\x1b[H");
184
+
185
+ const out: string[] = [];
186
+ const today = new Date().toISOString().slice(0, 10);
187
+ const h = score.hologram;
188
+ const ctx = score.ctxSavings ?? { daily: [], lifetime: [] };
189
+
190
+ // ── Header ──
191
+ out.push(`┌${HBAR}┐`);
192
+ {
193
+ const liveBadge = `${C.red}[● live]${C.reset}`;
194
+ const dateStr = `${C.dim}${today}${C.reset}`;
195
+ const title = ` ${C.cyan}${C.bold}${T.title}${C.reset} ${liveBadge} ${dateStr}`;
196
+ out.push(row(title));
197
+ }
198
+ out.push(divider());
199
+
200
+ // ── TODAY'S SAVINGS ──
201
+ // Denominator = ALL bytes processed by afd tools (hologram_daily includes small files
202
+ // since afd_read now records even full-content reads).
203
+ {
204
+ out.push(row(` ${C.cyan}${C.bold}${T.todaySavings}${C.reset}`));
205
+
206
+ const hToday = h.today ?? (h.daily[0]?.date === today ? h.daily[0] : null);
207
+ // holoOriginal = total bytes that went through afd_read (small files + large files)
208
+ // holoActual = bytes actually sent (small files at full size + large files at hologram size)
209
+ const holoOriginal = hToday?.originalChars ?? 0;
210
+ const holoActual = hToday?.hologramChars ?? 0;
211
+ const holoRequests = hToday?.requests ?? 0;
212
+
213
+ const wsmapToday = ctx.daily.find(r => r.date === today && r.type === "wsmap");
214
+ const pinpointToday = ctx.daily.find(r => r.date === today && r.type === "pinpoint");
215
+ const wsmapOriginal = wsmapToday?.original_chars ?? 0;
216
+ const wsmapSavedChars = wsmapToday?.saved_chars ?? 0;
217
+ const pinpointOriginal = pinpointToday?.original_chars ?? 0;
218
+ const pinpointSavedChars = pinpointToday?.saved_chars ?? 0;
219
+
220
+ // totalOriginal = what WOULD have been consumed without afd
221
+ // totalActual = what WAS actually consumed through afd
222
+ const totalOriginal = holoOriginal + wsmapOriginal + pinpointOriginal;
223
+ const totalActual = holoActual + (wsmapOriginal - wsmapSavedChars) + (pinpointOriginal - pinpointSavedChars);
224
+ const hasData = holoRequests > 0 || (wsmapToday?.requests ?? 0) > 0 || (pinpointToday?.requests ?? 0) > 0;
225
+
226
+ if (hasData && totalOriginal > 0) {
227
+ const barW = 20;
228
+ const savedTok = Math.max(0, totalOriginal - totalActual);
229
+ const savedPct = Math.round((savedTok / totalOriginal) * 100);
230
+ const actPct = (totalActual / totalOriginal) * 100;
231
+
232
+ out.push(row(` ${T.labelOrig}${barOriginal(barW)} ${C.dim}${formatK(totalOriginal)}${C.reset}`));
233
+ out.push(row(` ${T.labelAct}${barActual(actPct, barW)} ${C.yellow}${formatK(totalActual)}${C.reset}`));
234
+ out.push(row(` ${T.labelSaved}${barSaved(savedPct, barW)} ${C.green}${savedPct}%${C.reset} ${C.dim}(${formatK(savedTok)} ${T.savedSuffix})${C.reset}`));
235
+ } else {
236
+ out.push(row(` ${C.dim}${T.startTracking}${C.reset}`));
237
+ }
238
+ }
239
+
240
+ out.push(divider());
241
+
242
+ // ── LIFETIME ROI & BREAKDOWN ──
243
+ {
244
+ out.push(row(` ${C.cyan}${C.bold}${T.lifetimeRoi}${C.reset}`));
245
+
246
+ const lt = h.lifetime;
247
+ // hologramSavedChars = compression savings only (small files cancel out: orig == actual)
248
+ const hologramSavedChars = Math.max(0, lt.originalChars - lt.hologramChars);
249
+
250
+ const wsmapRow = ctx.lifetime.find(r => r.type === "wsmap");
251
+ const pinpointRow = ctx.lifetime.find(r => r.type === "pinpoint");
252
+ const wsmapSaved = wsmapRow?.total_saved_chars ?? 0;
253
+ const pinpointSaved = pinpointRow?.total_saved_chars ?? 0;
254
+ const totalSavedChars = hologramSavedChars + wsmapSaved + pinpointSaved;
255
+ const totalSavedTok = totalSavedChars / CHARS_PER_TOKEN;
256
+ const estValue = Math.round(totalSavedTok / 1000 * 0.003 * 100) / 100;
257
+
258
+ const totalLine = ` ${C.bold}${C.green}${T.totalSaved} ~${fmtNum(Math.round(totalSavedTok))} tok${C.reset} ${C.dim}│ ${T.estValue} $${estValue.toFixed(2)}${C.reset}`;
259
+ out.push(row(totalLine));
260
+ out.push(row(` ${C.dim}${"─".repeat(INNER - 4)}${C.reset}`));
261
+
262
+ const holoTok = Math.round(hologramSavedChars / CHARS_PER_TOKEN);
263
+ const holoColor = holoTok > 0 ? C.green : C.dim;
264
+ const holoPct = totalSavedChars > 0 ? Math.round((hologramSavedChars / totalSavedChars) * 100) : 0;
265
+ out.push(row(` ${holoColor}[${holoTok > 0 ? "✓" : "·"}] ${T.hologram.padEnd(12)} ~${formatK(hologramSavedChars).padEnd(9)} (${holoPct}%)${C.reset}`));
266
+
267
+ const wsmapTok = Math.round(wsmapSaved / CHARS_PER_TOKEN);
268
+ const wsmapColor = wsmapTok > 0 ? C.green : C.dim;
269
+ const wsmapPct = totalSavedChars > 0 ? Math.round((wsmapSaved / totalSavedChars) * 100) : 0;
270
+ out.push(row(` ${wsmapColor}[${wsmapTok > 0 ? "✓" : "·"}] ${T.wsmap.padEnd(12)} ~${formatK(wsmapSaved).padEnd(9)} (${wsmapPct}%)${C.reset}`));
271
+
272
+ const pinTok = Math.round(pinpointSaved / CHARS_PER_TOKEN);
273
+ const pinColor = pinTok > 0 ? C.green : C.dim;
274
+ const pinPct = totalSavedChars > 0 ? Math.round((pinpointSaved / totalSavedChars) * 100) : 0;
275
+ out.push(row(` ${pinColor}[${pinTok > 0 ? "✓" : "·"}] ${T.pinpoint.padEnd(12)} ~${formatK(pinpointSaved).padEnd(9)} (${pinPct}%)${C.reset}`));
276
+ }
277
+
278
+ out.push(divider());
279
+
280
+ // ── 7-DAY HISTORY ──
281
+ {
282
+ out.push(row(` ${C.cyan}${C.bold}${T.weekHistory}${C.reset}`));
283
+
284
+ // Merge hologram daily (which now includes small file reads) + wsmap/pinpoint daily
285
+ const dailyMap = new Map<string, { original: number; saved: number }>();
286
+ for (const d of h.daily) {
287
+ dailyMap.set(d.date, { original: d.originalChars, saved: d.originalChars - d.hologramChars });
288
+ }
289
+ for (const r of ctx.daily) {
290
+ const entry = dailyMap.get(r.date) ?? { original: 0, saved: 0 };
291
+ dailyMap.set(r.date, { original: entry.original + r.original_chars, saved: entry.saved + r.saved_chars });
292
+ }
293
+
294
+ const sortedDates = [...dailyMap.keys()].sort().reverse().slice(0, 7);
295
+
296
+ if (sortedDates.length > 0) {
297
+ for (const date of sortedDates) {
298
+ const { original, saved } = dailyMap.get(date)!;
299
+ const pct = original > 0 ? Math.round((saved / original) * 100) : 0;
300
+ const barW = 14;
301
+ const filled = Math.min(barW, Math.round((pct / 100) * barW));
302
+ const empty = barW - filled;
303
+ const barColor = pct >= 70 ? C.green : pct >= 40 ? C.yellow : C.dim;
304
+ const bar = `${barColor}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
305
+ const wd = weekday(date, today);
306
+ const dateLabel = `${C.dim}${date.slice(5)}${C.reset} ${C.dim}(${wd})${C.reset}`;
307
+ const pctColor = pct >= 70 ? C.green : pct >= 40 ? C.yellow : C.dim;
308
+ const tokRange = `${C.dim}${formatK(original)} → ${formatK(original - saved)}${C.reset}`;
309
+ out.push(row(` ${dateLabel} ${bar} ${pctColor}${pct}%${C.reset} │ ${tokRange}`));
310
+ }
311
+ } else {
312
+ out.push(row(` ${C.dim}${T.noHistory}${C.reset}`));
313
+ }
314
+ }
315
+
316
+ out.push(divider());
317
+
318
+ // ── SYSTEM STATUS ──
319
+ {
320
+ out.push(row(` ${C.cyan}${C.bold}${T.systemStatus}${C.reset}`));
321
+ const lt = h.lifetime;
322
+ const reqStr = `${T.requests}: ${lt.requests}`;
323
+ const uptStr = `${T.uptime}: ${formatUptime(score.uptime)}`;
324
+ const evtStr = `${T.events}: ${score.totalEvents}`;
325
+ out.push(row(` ${C.dim}${reqStr} │ ${uptStr} │ ${evtStr}${C.reset}`));
326
+ }
327
+
328
+ out.push(`└${HBAR}┘`);
329
+ out.push(` ${C.dim}${T.updated}: ${lastUpdated} | ${T.exitHint}${C.reset}`);
330
+
331
+ process.stdout.write(out.join("\n") + "\n");
332
+ }
333
+
334
+ // ── Live loop ─────────────────────────────────────────────────────────────────
335
+
336
+ export async function dashboardCommand(): Promise<void> {
337
+ const info = getDaemonInfo();
338
+ if (!info) {
339
+ const msg = isKo
340
+ ? `[afd] 데몬이 실행 중이 아닙니다. \`afd start\`를 먼저 실행하세요.`
341
+ : `[afd] Daemon not running. Run \`afd start\` first.`;
342
+ console.error(`${C.red}${msg}${C.reset}`);
343
+ process.exit(1);
344
+ }
345
+
346
+ const ac = new AbortController();
347
+
348
+ process.on("exit", () => process.stdout.write("\x1b[?25h"));
349
+ const cleanup = () => { ac.abort(); process.exit(0); };
350
+ process.on("SIGINT", cleanup);
351
+ process.on("SIGTERM", cleanup);
352
+ process.stdout.write("\x1b[?25l");
353
+
354
+ // Initial render
355
+ try {
356
+ const score = await daemonRequest<ScoreData>("/score");
357
+ render(score, new Date().toLocaleTimeString());
358
+ } catch (err) {
359
+ process.stdout.write("\x1b[?25h");
360
+ const msg = err instanceof Error ? err.message : String(err);
361
+ console.error(`${C.red}[afd] ${msg}${C.reset}`);
362
+ process.exit(1);
363
+ }
364
+
365
+ async function doRefresh() {
366
+ try {
367
+ const score = await daemonRequest<ScoreData>("/score");
368
+ render(score, new Date().toLocaleTimeString());
369
+ } catch { /* non-fatal */ }
370
+ }
371
+
372
+ const pollTimer = setInterval(doRefresh, 3000);
373
+ ac.signal.addEventListener("abort", () => clearInterval(pollTimer));
374
+
375
+ // SSE for instant updates
376
+ (async () => {
377
+ while (!ac.signal.aborted) {
378
+ try {
379
+ const res = await fetch(`http://127.0.0.1:${info.port}/events`, { signal: ac.signal });
380
+ if (!res.body) { await new Promise(r => setTimeout(r, 3000)); continue; }
381
+
382
+ const reader = res.body.getReader();
383
+ const dec = new TextDecoder();
384
+ let buf = "";
385
+
386
+ while (!ac.signal.aborted) {
387
+ const { value, done } = await reader.read();
388
+ if (done) break;
389
+ buf += dec.decode(value, { stream: true });
390
+ const frames = buf.split("\n\n");
391
+ buf = frames.pop() ?? "";
392
+ for (const frame of frames) {
393
+ if (frame.trim() && frame.includes("data:")) await doRefresh();
394
+ }
395
+ }
396
+ } catch {
397
+ if (ac.signal.aborted) break;
398
+ await new Promise(r => setTimeout(r, 3000));
399
+ }
400
+ }
401
+ })();
402
+
403
+ await new Promise<void>(resolve => ac.signal.addEventListener("abort", resolve));
404
+ }
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  import { evolve, analyzeQuarantine, listQuarantine } from "../core/evolution";
9
+ import { generateValidators } from "../core/validator-generator";
10
+ import type { ValidatorGenInput } from "../core/validator-generator";
9
11
  import { getSystemLanguage } from "../core/locale";
10
12
 
11
13
  const msgs = {
@@ -21,6 +23,10 @@ const msgs = {
21
23
  learned: "Learned",
22
24
  pending: "Pending",
23
25
  lessonDetail: "New Lessons",
26
+ genTitle: "Auto-Validator Generation",
27
+ genWritten: (n: number) => `${n} validator(s) generated in .afd/validators/`,
28
+ genSkipped: (n: number, reason: string) => `${n} skipped (${reason})`,
29
+ genNone: "No pending patterns to generate validators from.",
24
30
  },
25
31
  ko: {
26
32
  title: "afd 자가 진화 리포트",
@@ -34,6 +40,10 @@ const msgs = {
34
40
  learned: "학습 완료",
35
41
  pending: "대기 중",
36
42
  lessonDetail: "새로운 교훈",
43
+ genTitle: "자동 검증기 생성",
44
+ genWritten: (n: number) => `${n}개의 검증기가 .afd/validators/에 생성되었습니다`,
45
+ genSkipped: (n: number, reason: string) => `${n}개 건너뜀 (${reason})`,
46
+ genNone: "검증기를 생성할 대기 중인 패턴이 없습니다.",
37
47
  },
38
48
  };
39
49
 
@@ -57,7 +67,7 @@ function visualWidth(s: string): number {
57
67
  return w;
58
68
  }
59
69
 
60
- export async function evolutionCommand() {
70
+ export async function evolutionCommand(opts: { generate?: boolean } = {}) {
61
71
  const lang = getSystemLanguage();
62
72
  const m = msgs[lang];
63
73
 
@@ -68,6 +78,62 @@ export async function evolutionCommand() {
68
78
  }
69
79
 
70
80
  const stats = analyzeQuarantine();
81
+
82
+ // --generate: produce validators from ALL quarantine patterns (not just pending)
83
+ if (opts.generate) {
84
+ const allStats = analyzeQuarantine();
85
+ // Also include already-learned entries for validator generation
86
+ const allEntries = listQuarantine();
87
+ const inputs: ValidatorGenInput[] = allEntries.map(entry => {
88
+ const lesson = allStats.lessons.find(l => l.entry.quarantinePath === entry.quarantinePath);
89
+ if (lesson) {
90
+ return {
91
+ failureType: lesson.failureType,
92
+ originalPath: lesson.entry.originalPath,
93
+ corruptedContent: lesson.corruptedContent,
94
+ restoredContent: lesson.restoredContent,
95
+ };
96
+ }
97
+ // For already-learned entries, read quarantine file directly
98
+ const { readFileSync, existsSync } = require("fs");
99
+ const corruptedContent = readFileSync(entry.quarantinePath, "utf-8") as string;
100
+ const failureType = corruptedContent.trim() === "DELETED" ? "deletion" as const : "corruption" as const;
101
+ const restoredContent = existsSync(entry.originalPath)
102
+ ? readFileSync(entry.originalPath, "utf-8") as string
103
+ : null;
104
+ return { failureType, originalPath: entry.originalPath, corruptedContent, restoredContent };
105
+ });
106
+
107
+ if (inputs.length === 0) {
108
+ console.log(m.genNone);
109
+ return;
110
+ }
111
+
112
+ const results = generateValidators(inputs);
113
+ const written = results.filter(r => r.written);
114
+ const skipped = results.filter(r => !r.written);
115
+
116
+ console.log("");
117
+ console.log(hline(BOX.tl, BOX.tr));
118
+ console.log(row(`🧬 ${m.genTitle}`));
119
+ console.log(hline(BOX.ml, BOX.mr));
120
+
121
+ for (const r of written) {
122
+ console.log(row(` ✅ ${r.filename}`));
123
+ }
124
+ for (const r of skipped) {
125
+ console.log(row(` ⏭️ ${r.filename} (${r.reason})`));
126
+ }
127
+
128
+ console.log(hline(BOX.ml, BOX.mr));
129
+ console.log(row(m.genWritten(written.length)));
130
+ if (skipped.length > 0) {
131
+ console.log(row(m.genSkipped(skipped.length, "user-modified")));
132
+ }
133
+ console.log(hline(BOX.bl, BOX.br));
134
+ return;
135
+ }
136
+
71
137
  if (stats.pending === 0) {
72
138
  console.log(m.noPending);
73
139
  return;
@@ -76,6 +142,16 @@ export async function evolutionCommand() {
76
142
  console.log(m.analyzing);
77
143
  const result = evolve();
78
144
 
145
+ // Auto-generate validators for newly learned patterns
146
+ const genInputs: ValidatorGenInput[] = stats.lessons.map(lesson => ({
147
+ failureType: lesson.failureType,
148
+ originalPath: lesson.entry.originalPath,
149
+ corruptedContent: lesson.corruptedContent,
150
+ restoredContent: lesson.restoredContent,
151
+ }));
152
+ const genResults = generateValidators(genInputs);
153
+ const genWritten = genResults.filter(r => r.written);
154
+
79
155
  console.log("");
80
156
  console.log(hline(BOX.tl, BOX.tr));
81
157
  console.log(row(`🧬 ${m.title}`));
@@ -103,5 +179,12 @@ export async function evolutionCommand() {
103
179
  console.log(hline(BOX.ml, BOX.mr));
104
180
  console.log(row(m.written(result.lessonsWritten)));
105
181
  console.log(row(m.total(result.totalLessons)));
182
+ if (genWritten.length > 0) {
183
+ console.log(hline(BOX.ml, BOX.mr));
184
+ console.log(row(`🧬 ${m.genWritten(genWritten.length)}`));
185
+ for (const r of genWritten) {
186
+ console.log(row(` ✅ ${r.filename}`));
187
+ }
188
+ }
106
189
  console.log(hline(BOX.bl, BOX.br));
107
190
  }