@wooojin/forgen 0.2.0 → 0.3.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 (158) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.ja.md +79 -14
  3. package/README.ko.md +100 -14
  4. package/README.md +124 -17
  5. package/README.zh.md +79 -14
  6. package/agents/analyst.md +48 -4
  7. package/agents/architect.md +39 -4
  8. package/agents/code-reviewer.md +107 -77
  9. package/agents/critic.md +47 -4
  10. package/agents/debugger.md +46 -4
  11. package/agents/designer.md +40 -4
  12. package/agents/executor.md +112 -30
  13. package/agents/explore.md +45 -5
  14. package/agents/git-master.md +48 -4
  15. package/agents/planner.md +121 -18
  16. package/agents/test-engineer.md +58 -4
  17. package/agents/verifier.md +92 -77
  18. package/commands/architecture-decision.md +127 -258
  19. package/commands/calibrate.md +225 -0
  20. package/commands/code-review.md +163 -178
  21. package/commands/compound.md +127 -68
  22. package/commands/deep-interview.md +273 -0
  23. package/commands/docker.md +68 -178
  24. package/commands/forge-loop.md +215 -0
  25. package/commands/learn.md +231 -0
  26. package/commands/retro.md +215 -0
  27. package/commands/ship.md +277 -0
  28. package/dist/cli.js +26 -9
  29. package/dist/core/auto-compound-runner.js +14 -0
  30. package/dist/core/config-injector.d.ts +2 -1
  31. package/dist/core/config-injector.js +2 -1
  32. package/dist/core/dashboard.d.ts +108 -0
  33. package/dist/core/dashboard.js +495 -0
  34. package/dist/core/doctor.js +151 -21
  35. package/dist/core/drift-score.d.ts +49 -0
  36. package/dist/core/drift-score.js +87 -0
  37. package/dist/core/harness.d.ts +6 -1
  38. package/dist/core/harness.js +75 -19
  39. package/dist/core/mcp-config.d.ts +2 -0
  40. package/dist/core/mcp-config.js +6 -1
  41. package/dist/core/paths.d.ts +6 -1
  42. package/dist/core/paths.js +18 -2
  43. package/dist/core/spawn.d.ts +3 -2
  44. package/dist/core/spawn.js +27 -8
  45. package/dist/core/types.d.ts +34 -0
  46. package/dist/engine/compound-export.d.ts +41 -0
  47. package/dist/engine/compound-export.js +169 -0
  48. package/dist/engine/compound-lifecycle.d.ts +4 -3
  49. package/dist/engine/compound-lifecycle.js +91 -46
  50. package/dist/engine/compound-loop.js +18 -0
  51. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  52. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  53. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  54. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  55. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  56. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  57. package/dist/engine/meta-learning/runner.d.ts +14 -0
  58. package/dist/engine/meta-learning/runner.js +90 -0
  59. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  60. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  61. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  62. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  63. package/dist/engine/meta-learning/types.d.ts +114 -0
  64. package/dist/engine/meta-learning/types.js +43 -0
  65. package/dist/engine/solution-format.d.ts +2 -2
  66. package/dist/engine/solution-format.js +249 -34
  67. package/dist/engine/solution-index.d.ts +1 -1
  68. package/dist/engine/solution-matcher.d.ts +30 -1
  69. package/dist/engine/solution-matcher.js +235 -45
  70. package/dist/fgx.js +12 -8
  71. package/dist/hooks/context-guard.d.ts +15 -0
  72. package/dist/hooks/context-guard.js +218 -56
  73. package/dist/hooks/db-guard.js +2 -2
  74. package/dist/hooks/hook-config.d.ts +27 -1
  75. package/dist/hooks/hook-config.js +72 -12
  76. package/dist/hooks/hooks-generator.d.ts +3 -0
  77. package/dist/hooks/hooks-generator.js +23 -6
  78. package/dist/hooks/intent-classifier.d.ts +0 -2
  79. package/dist/hooks/intent-classifier.js +32 -18
  80. package/dist/hooks/keyword-detector.js +126 -204
  81. package/dist/hooks/notepad-injector.js +2 -2
  82. package/dist/hooks/permission-handler.js +2 -2
  83. package/dist/hooks/post-tool-failure.js +12 -6
  84. package/dist/hooks/post-tool-handlers.d.ts +1 -1
  85. package/dist/hooks/post-tool-handlers.js +14 -11
  86. package/dist/hooks/post-tool-use.d.ts +11 -0
  87. package/dist/hooks/post-tool-use.js +184 -71
  88. package/dist/hooks/pre-compact.d.ts +11 -1
  89. package/dist/hooks/pre-compact.js +112 -37
  90. package/dist/hooks/pre-tool-use.js +86 -56
  91. package/dist/hooks/rate-limiter.js +3 -3
  92. package/dist/hooks/secret-filter.js +2 -2
  93. package/dist/hooks/session-recovery.js +256 -236
  94. package/dist/hooks/shared/hook-response.d.ts +4 -4
  95. package/dist/hooks/shared/hook-response.js +13 -24
  96. package/dist/hooks/shared/hook-timing.d.ts +15 -0
  97. package/dist/hooks/shared/hook-timing.js +64 -0
  98. package/dist/hooks/skill-injector.d.ts +4 -3
  99. package/dist/hooks/skill-injector.js +47 -16
  100. package/dist/hooks/slop-detector.js +3 -3
  101. package/dist/hooks/solution-injector.js +224 -197
  102. package/dist/hooks/subagent-tracker.js +2 -2
  103. package/dist/host/codex-adapter.d.ts +10 -0
  104. package/dist/host/codex-adapter.js +154 -0
  105. package/dist/mcp/solution-reader.d.ts +5 -5
  106. package/dist/mcp/solution-reader.js +34 -24
  107. package/dist/renderer/rule-renderer.js +9 -11
  108. package/dist/services/session.d.ts +19 -0
  109. package/dist/services/session.js +62 -0
  110. package/hooks/hooks.json +2 -2
  111. package/package.json +2 -1
  112. package/skills/architecture-decision/SKILL.md +113 -257
  113. package/skills/calibrate/SKILL.md +207 -0
  114. package/skills/code-review/SKILL.md +151 -178
  115. package/skills/compound/SKILL.md +126 -68
  116. package/skills/deep-interview/SKILL.md +266 -0
  117. package/skills/docker/SKILL.md +57 -179
  118. package/skills/forge-loop/SKILL.md +198 -0
  119. package/skills/learn/SKILL.md +216 -0
  120. package/skills/retro/SKILL.md +199 -0
  121. package/skills/ship/SKILL.md +259 -0
  122. package/agents/code-simplifier.md +0 -197
  123. package/agents/performance-reviewer.md +0 -172
  124. package/agents/qa-tester.md +0 -158
  125. package/agents/refactoring-expert.md +0 -168
  126. package/agents/scientist.md +0 -144
  127. package/agents/security-reviewer.md +0 -137
  128. package/agents/writer.md +0 -184
  129. package/commands/api-design.md +0 -268
  130. package/commands/ci-cd.md +0 -270
  131. package/commands/database.md +0 -263
  132. package/commands/debug-detective.md +0 -99
  133. package/commands/documentation.md +0 -276
  134. package/commands/ecomode.md +0 -51
  135. package/commands/frontend.md +0 -271
  136. package/commands/git-master.md +0 -90
  137. package/commands/incident-response.md +0 -292
  138. package/commands/migrate.md +0 -101
  139. package/commands/performance.md +0 -288
  140. package/commands/refactor.md +0 -105
  141. package/commands/security-review.md +0 -288
  142. package/commands/tdd.md +0 -183
  143. package/commands/testing-strategy.md +0 -265
  144. package/skills/api-design/SKILL.md +0 -262
  145. package/skills/ci-cd/SKILL.md +0 -264
  146. package/skills/database/SKILL.md +0 -257
  147. package/skills/debug-detective/SKILL.md +0 -95
  148. package/skills/documentation/SKILL.md +0 -270
  149. package/skills/ecomode/SKILL.md +0 -46
  150. package/skills/frontend/SKILL.md +0 -265
  151. package/skills/git-master/SKILL.md +0 -86
  152. package/skills/incident-response/SKILL.md +0 -286
  153. package/skills/migrate/SKILL.md +0 -96
  154. package/skills/performance/SKILL.md +0 -282
  155. package/skills/refactor/SKILL.md +0 -100
  156. package/skills/security-review/SKILL.md +0 -282
  157. package/skills/tdd/SKILL.md +0 -178
  158. package/skills/testing-strategy/SKILL.md +0 -260
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Forgen — Compound Dashboard
3
+ *
4
+ * Provides a rich terminal overview of the compound knowledge system:
5
+ * knowledge inventory, injection activity, lifecycle transitions,
6
+ * session history, and hook health.
7
+ *
8
+ * Data is collected from:
9
+ * - ME_SOLUTIONS, ME_RULES, ME_BEHAVIOR (knowledge files)
10
+ * - MATCH_EVAL_LOG_PATH (injection/matching decisions)
11
+ * - STATE_DIR (hook-errors.json, last-extraction.json)
12
+ * - Solution frontmatter (lifecycle evidence fields)
13
+ */
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { ME_SOLUTIONS, ME_RULES, ME_BEHAVIOR, STATE_DIR, V1_EVIDENCE_DIR, } from './paths.js';
17
+ import { parseFrontmatterOnly } from '../engine/solution-format.js';
18
+ import { readMatchEvalLog } from '../engine/match-eval-log.js';
19
+ // ── ANSI color helpers ──
20
+ const BOLD = '\x1b[1m';
21
+ const DIM = '\x1b[2m';
22
+ const RED = '\x1b[31m';
23
+ const GREEN = '\x1b[32m';
24
+ const YELLOW = '\x1b[33m';
25
+ const CYAN = '\x1b[36m';
26
+ const RESET = '\x1b[0m';
27
+ function bold(s) { return `${BOLD}${s}${RESET}`; }
28
+ function green(s) { return `${GREEN}${s}${RESET}`; }
29
+ function yellow(s) { return `${YELLOW}${s}${RESET}`; }
30
+ function red(s) { return `${RED}${s}${RESET}`; }
31
+ function dim(s) { return `${DIM}${s}${RESET}`; }
32
+ function cyan(s) { return `${CYAN}${s}${RESET}`; }
33
+ // ── Box-drawing table helpers ──
34
+ function tableRow(cols, widths) {
35
+ return ' │ ' + cols.map((c, i) => c.padEnd(widths[i])).join(' │ ') + ' │';
36
+ }
37
+ function tableSep(widths, top = false, bottom = false) {
38
+ const left = top ? '┌' : bottom ? '└' : '├';
39
+ const mid = top ? '┬' : bottom ? '┴' : '┼';
40
+ const right = top ? '┐' : bottom ? '┘' : '┤';
41
+ return ' ' + left + widths.map(w => '─'.repeat(w + 2)).join(mid) + right;
42
+ }
43
+ // ── Data Collection Functions ──
44
+ /** Read all .md files in a directory and return their frontmatter. */
45
+ function readFrontmatters(dir) {
46
+ const results = [];
47
+ try {
48
+ if (!fs.existsSync(dir))
49
+ return results;
50
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
51
+ for (const file of files) {
52
+ try {
53
+ const content = fs.readFileSync(path.join(dir, file), 'utf-8');
54
+ const fm = parseFrontmatterOnly(content);
55
+ if (fm)
56
+ results.push(fm);
57
+ }
58
+ catch { /* skip unreadable files */ }
59
+ }
60
+ }
61
+ catch { /* skip unreadable directories */ }
62
+ return results;
63
+ }
64
+ /** Count files in a directory (non-recursive). */
65
+ function countDirFiles(dir, ext) {
66
+ try {
67
+ if (!fs.existsSync(dir))
68
+ return 0;
69
+ const files = fs.readdirSync(dir);
70
+ return ext ? files.filter(f => f.endsWith(ext)).length : files.length;
71
+ }
72
+ catch {
73
+ return 0;
74
+ }
75
+ }
76
+ /** Collect knowledge overview data. */
77
+ export function collectKnowledgeOverview() {
78
+ const solutionFms = readFrontmatters(ME_SOLUTIONS);
79
+ const ruleFms = readFrontmatters(ME_RULES);
80
+ const byStatus = {
81
+ experiment: 0, candidate: 0, verified: 0, mature: 0, retired: 0,
82
+ };
83
+ for (const fm of solutionFms) {
84
+ if (fm.status in byStatus)
85
+ byStatus[fm.status]++;
86
+ }
87
+ // Rules categorized by type
88
+ const ruleCategories = {};
89
+ for (const fm of ruleFms) {
90
+ const key = fm.type ?? 'unknown';
91
+ ruleCategories[key] = (ruleCategories[key] ?? 0) + 1;
92
+ }
93
+ // Behavior file count
94
+ const behaviorCount = countDirFiles(ME_BEHAVIOR, '.md') + countDirFiles(ME_BEHAVIOR, '.json');
95
+ // Date range across all frontmatters
96
+ const allFms = [...solutionFms, ...ruleFms];
97
+ let oldest = null;
98
+ let newest = null;
99
+ for (const fm of allFms) {
100
+ if (!oldest || fm.created < oldest)
101
+ oldest = fm.created;
102
+ if (!newest || fm.updated > newest)
103
+ newest = fm.updated;
104
+ }
105
+ return {
106
+ solutions: { total: solutionFms.length, byStatus },
107
+ rules: { total: ruleFms.length, categories: ruleCategories },
108
+ behavior: { total: behaviorCount },
109
+ dateRange: { oldest, newest },
110
+ };
111
+ }
112
+ /** Collect injection activity from match-eval-log. */
113
+ export function collectInjectionActivity() {
114
+ const records = readMatchEvalLog();
115
+ // Recent injections (last 10)
116
+ const sorted = [...records].sort((a, b) => b.ts.localeCompare(a.ts));
117
+ const recentInjections = sorted.slice(0, 10).flatMap(r => r.rankedTopN.map(name => ({ name, ts: r.ts, source: r.source }))).slice(0, 10);
118
+ // Top 5 most frequently injected solutions
119
+ const freq = new Map();
120
+ for (const r of records) {
121
+ for (const name of r.rankedTopN) {
122
+ freq.set(name, (freq.get(name) ?? 0) + 1);
123
+ }
124
+ }
125
+ const topSolutions = [...freq.entries()]
126
+ .sort((a, b) => b[1] - a[1])
127
+ .slice(0, 5)
128
+ .map(([name, count]) => ({ name, count }));
129
+ const hookCount = records.filter(r => r.source === 'hook').length;
130
+ const mcpCount = records.filter(r => r.source === 'mcp').length;
131
+ return {
132
+ totalRecords: records.length,
133
+ recentInjections,
134
+ topSolutions,
135
+ hookCount,
136
+ mcpCount,
137
+ };
138
+ }
139
+ /** Collect code reflection data from solution evidence. */
140
+ export function collectReflectionData() {
141
+ const fms = readFrontmatters(ME_SOLUTIONS);
142
+ const reflected = fms.filter(fm => fm.evidence.reflected > 0).length;
143
+ const unreflected = fms.filter(fm => fm.evidence.reflected === 0 && fm.status !== 'retired').length;
144
+ const activeFms = fms.filter(fm => fm.status !== 'retired');
145
+ const rate = activeFms.length > 0 ? (reflected / activeFms.length) * 100 : 0;
146
+ return {
147
+ totalSolutions: fms.length,
148
+ reflectedCount: reflected,
149
+ unreflectedCount: unreflected,
150
+ reflectionRate: rate,
151
+ };
152
+ }
153
+ /** Collect lifecycle activity data. */
154
+ export function collectLifecycleActivity() {
155
+ const fms = readFrontmatters(ME_SOLUTIONS);
156
+ const statusDistribution = {
157
+ experiment: 0, candidate: 0, verified: 0, mature: 0, retired: 0,
158
+ };
159
+ for (const fm of fms) {
160
+ if (fm.status in statusDistribution)
161
+ statusDistribution[fm.status]++;
162
+ }
163
+ // Solutions approaching promotion (high evidence, not yet promoted)
164
+ const candidates = fms
165
+ .filter(fm => fm.status !== 'retired' && fm.status !== 'mature')
166
+ .map(fm => ({
167
+ name: fm.name,
168
+ status: fm.status,
169
+ evidence: {
170
+ reflected: fm.evidence.reflected,
171
+ sessions: fm.evidence.sessions,
172
+ negative: fm.evidence.negative,
173
+ },
174
+ }))
175
+ .sort((a, b) => b.evidence.reflected - a.evidence.reflected)
176
+ .slice(0, 5);
177
+ return { recentPromotionCandidates: candidates, statusDistribution };
178
+ }
179
+ /** Collect session extraction history. */
180
+ export function collectSessionHistory() {
181
+ const lastExtractionPath = path.join(STATE_DIR, 'last-extraction.json');
182
+ try {
183
+ if (fs.existsSync(lastExtractionPath)) {
184
+ const data = JSON.parse(fs.readFileSync(lastExtractionPath, 'utf-8'));
185
+ return {
186
+ lastExtraction: {
187
+ date: data.lastExtractedAt ?? 'unknown',
188
+ extractionsToday: data.extractionsToday ?? 0,
189
+ },
190
+ };
191
+ }
192
+ }
193
+ catch { /* skip */ }
194
+ return { lastExtraction: null };
195
+ }
196
+ /** Collect hook error data. */
197
+ export function collectHookHealth() {
198
+ const errorPath = path.join(STATE_DIR, 'hook-errors.json');
199
+ try {
200
+ if (fs.existsSync(errorPath)) {
201
+ const data = JSON.parse(fs.readFileSync(errorPath, 'utf-8'));
202
+ const errors = Object.entries(data).map(([hookName, info]) => ({
203
+ hookName,
204
+ count: info.count,
205
+ lastAt: info.lastAt,
206
+ }));
207
+ return { errors };
208
+ }
209
+ }
210
+ catch { /* skip */ }
211
+ return { errors: [] };
212
+ }
213
+ // ── Rendering ──
214
+ function renderKnowledgeOverview(data) {
215
+ const lines = [];
216
+ lines.push(` ${bold(cyan('Knowledge Overview'))}`);
217
+ lines.push('');
218
+ // Solutions table
219
+ const statusWidths = [14, 6];
220
+ lines.push(tableSep(statusWidths, true));
221
+ lines.push(tableRow(['Status', 'Count'], statusWidths));
222
+ lines.push(tableSep(statusWidths));
223
+ for (const [status, count] of Object.entries(data.solutions.byStatus)) {
224
+ if (count === 0 && status === 'retired')
225
+ continue;
226
+ const colorFn = status === 'mature' ? green
227
+ : status === 'verified' ? green
228
+ : status === 'experiment' ? yellow
229
+ : status === 'retired' ? red
230
+ : (s) => s;
231
+ lines.push(tableRow([colorFn(status), String(count)], statusWidths));
232
+ }
233
+ lines.push(tableSep(statusWidths, false, true));
234
+ lines.push(` Solutions: ${bold(String(data.solutions.total))} Rules: ${bold(String(data.rules.total))} Behavior: ${bold(String(data.behavior.total))}`);
235
+ if (data.dateRange.oldest && data.dateRange.newest) {
236
+ lines.push(` Date range: ${dim(data.dateRange.oldest)} → ${dim(data.dateRange.newest)}`);
237
+ }
238
+ return lines.join('\n');
239
+ }
240
+ function renderInjectionActivity(data) {
241
+ const lines = [];
242
+ lines.push(` ${bold(cyan('Injection Activity'))}`);
243
+ lines.push('');
244
+ if (data.totalRecords === 0) {
245
+ lines.push(` ${dim('No injection records found.')}`);
246
+ return lines.join('\n');
247
+ }
248
+ lines.push(` Total decisions: ${bold(String(data.totalRecords))} (hook: ${data.hookCount}, mcp: ${data.mcpCount})`);
249
+ lines.push('');
250
+ if (data.topSolutions.length > 0) {
251
+ lines.push(` ${bold('Top injected solutions:')}`);
252
+ for (const s of data.topSolutions) {
253
+ const bar = '█'.repeat(Math.min(s.count, 30));
254
+ lines.push(` ${s.name.padEnd(35)} ${green(bar)} ${s.count}`);
255
+ }
256
+ }
257
+ if (data.recentInjections.length > 0) {
258
+ lines.push('');
259
+ lines.push(` ${bold('Recent injections:')}`);
260
+ for (const inj of data.recentInjections.slice(0, 5)) {
261
+ const date = inj.ts.slice(0, 16).replace('T', ' ');
262
+ lines.push(` ${dim(date)} [${inj.source}] ${inj.name}`);
263
+ }
264
+ }
265
+ return lines.join('\n');
266
+ }
267
+ function renderReflectionData(data) {
268
+ const lines = [];
269
+ lines.push(` ${bold(cyan('Code Reflection'))}`);
270
+ lines.push('');
271
+ if (data.totalSolutions === 0) {
272
+ lines.push(` ${dim('No solutions to analyze.')}`);
273
+ return lines.join('\n');
274
+ }
275
+ const rateColor = data.reflectionRate >= 50 ? green
276
+ : data.reflectionRate >= 20 ? yellow
277
+ : red;
278
+ lines.push(` Reflection rate: ${rateColor(`${data.reflectionRate.toFixed(1)}%`)}`);
279
+ lines.push(` Reflected in code: ${green(String(data.reflectedCount))} Not reflected: ${data.unreflectedCount > 0 ? yellow(String(data.unreflectedCount)) : String(data.unreflectedCount)}`);
280
+ return lines.join('\n');
281
+ }
282
+ function renderLifecycleActivity(data) {
283
+ const lines = [];
284
+ lines.push(` ${bold(cyan('Lifecycle Activity'))}`);
285
+ lines.push('');
286
+ // Status distribution bar
287
+ const total = Object.values(data.statusDistribution).reduce((a, b) => a + b, 0);
288
+ if (total > 0) {
289
+ const barWidth = 30;
290
+ const segments = [];
291
+ for (const [status, count] of Object.entries(data.statusDistribution)) {
292
+ if (count === 0)
293
+ continue;
294
+ const width = Math.max(1, Math.round((count / total) * barWidth));
295
+ const char = status === 'mature' ? `${GREEN}${'█'.repeat(width)}${RESET}`
296
+ : status === 'verified' ? `${GREEN}${'▓'.repeat(width)}${RESET}`
297
+ : status === 'candidate' ? `${CYAN}${'▒'.repeat(width)}${RESET}`
298
+ : status === 'experiment' ? `${YELLOW}${'░'.repeat(width)}${RESET}`
299
+ : `${RED}${'·'.repeat(width)}${RESET}`;
300
+ segments.push(char);
301
+ }
302
+ lines.push(` ${segments.join('')}`);
303
+ lines.push(` ${dim('█ mature ▓ verified ▒ candidate ░ experiment · retired')}`);
304
+ }
305
+ if (data.recentPromotionCandidates.length > 0) {
306
+ lines.push('');
307
+ lines.push(` ${bold('Approaching promotion:')}`);
308
+ for (const c of data.recentPromotionCandidates) {
309
+ const ev = c.evidence;
310
+ const negStr = ev.negative > 0 ? red(` neg:${ev.negative}`) : '';
311
+ lines.push(` ${c.name.padEnd(35)} [${c.status}] ref:${ev.reflected} sess:${ev.sessions}${negStr}`);
312
+ }
313
+ }
314
+ return lines.join('\n');
315
+ }
316
+ function renderSessionHistory(data) {
317
+ const lines = [];
318
+ lines.push(` ${bold(cyan('Session History'))}`);
319
+ lines.push('');
320
+ if (!data.lastExtraction) {
321
+ lines.push(` ${dim('No extraction history found.')}`);
322
+ return lines.join('\n');
323
+ }
324
+ const ext = data.lastExtraction;
325
+ lines.push(` Last extraction: ${dim(ext.date)}`);
326
+ lines.push(` Extractions today: ${bold(String(ext.extractionsToday))}`);
327
+ return lines.join('\n');
328
+ }
329
+ function renderHookHealth(data) {
330
+ const lines = [];
331
+ lines.push(` ${bold(cyan('Hook Health'))}`);
332
+ lines.push('');
333
+ if (data.errors.length === 0) {
334
+ lines.push(` ${green('All hooks healthy — no errors recorded.')}`);
335
+ return lines.join('\n');
336
+ }
337
+ const widths = [25, 6, 20];
338
+ lines.push(tableSep(widths, true));
339
+ lines.push(tableRow(['Hook', 'Errors', 'Last Error'], widths));
340
+ lines.push(tableSep(widths));
341
+ for (const err of data.errors.sort((a, b) => b.count - a.count)) {
342
+ const lastDate = err.lastAt.slice(0, 16).replace('T', ' ');
343
+ lines.push(tableRow([
344
+ err.hookName,
345
+ red(String(err.count)),
346
+ dim(lastDate),
347
+ ], widths));
348
+ }
349
+ lines.push(tableSep(widths, false, true));
350
+ return lines.join('\n');
351
+ }
352
+ /**
353
+ * Learning Curve 수집.
354
+ * evidence 파일(교정 기록)과 compound 활용률을 교차 분석하여 "쓸수록 나아진다"를 정량화.
355
+ */
356
+ export function collectLearningCurve() {
357
+ const now = Date.now();
358
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
359
+ let correctionsLast7d = 0;
360
+ let correctionsPrev7d = 0;
361
+ const axisCounts = new Map();
362
+ const uniqueDays = new Set();
363
+ try {
364
+ if (fs.existsSync(V1_EVIDENCE_DIR)) {
365
+ const files = fs.readdirSync(V1_EVIDENCE_DIR).filter(f => f.endsWith('.json'));
366
+ for (const f of files) {
367
+ try {
368
+ const data = JSON.parse(fs.readFileSync(path.join(V1_EVIDENCE_DIR, f), 'utf-8'));
369
+ if (!data.timestamp)
370
+ continue;
371
+ const ts = new Date(data.timestamp).getTime();
372
+ if (!Number.isFinite(ts))
373
+ continue;
374
+ const age = now - ts;
375
+ if (age < SEVEN_DAYS_MS)
376
+ correctionsLast7d++;
377
+ else if (age < 2 * SEVEN_DAYS_MS)
378
+ correctionsPrev7d++;
379
+ if (data.axis_hint) {
380
+ axisCounts.set(data.axis_hint, (axisCounts.get(data.axis_hint) ?? 0) + 1);
381
+ }
382
+ uniqueDays.add(new Date(ts).toISOString().slice(0, 10));
383
+ }
384
+ catch { /* 개별 파일 파싱 실패 무시 */ }
385
+ }
386
+ }
387
+ }
388
+ catch { /* fail-open */ }
389
+ // 추세 판정: 전주 대비 30% 이상 감소 = improving, 30% 이상 증가 = worsening
390
+ let correctionTrend = 'stable';
391
+ if (correctionsPrev7d > 0) {
392
+ const delta = (correctionsLast7d - correctionsPrev7d) / correctionsPrev7d;
393
+ if (delta < -0.3)
394
+ correctionTrend = 'improving';
395
+ else if (delta > 0.3)
396
+ correctionTrend = 'worsening';
397
+ }
398
+ // 상위 교정 축
399
+ const topCorrectionAxes = Array.from(axisCounts.entries())
400
+ .sort((a, b) => b[1] - a[1])
401
+ .slice(0, 3)
402
+ .map(([axis, count]) => ({ axis, count }));
403
+ // 세션 수: evidence 날짜 기준 (고유 날짜 × 평균 2세션/일 가정)
404
+ const sessionsAnalyzed = uniqueDays.size * 2;
405
+ // 추정 절약 시간: 지난 7일 compound 주입 이벤트당 평균 8분 절약 가정
406
+ // (경쟁자 분석에서 도출한 경험적 수치 — 카운터팩추얼의 하한 추정)
407
+ const injection = collectInjectionActivity();
408
+ let successfulInjections = 0;
409
+ try {
410
+ for (const rec of injection.recentInjections ?? []) {
411
+ const ts = new Date(rec.ts).getTime();
412
+ if (Number.isFinite(ts) && now - ts < SEVEN_DAYS_MS)
413
+ successfulInjections++;
414
+ }
415
+ }
416
+ catch { /* fail-open */ }
417
+ const estimatedMinutesSaved = Math.round(successfulInjections * 8);
418
+ return {
419
+ correctionsLast7d,
420
+ correctionsPrev7d,
421
+ correctionTrend,
422
+ evidenceTotalDays: uniqueDays.size,
423
+ sessionsAnalyzed,
424
+ estimatedMinutesSaved,
425
+ topCorrectionAxes,
426
+ };
427
+ }
428
+ function renderLearningCurve(data) {
429
+ const trendIcon = data.correctionTrend === 'improving'
430
+ ? green('↓ 감소')
431
+ : data.correctionTrend === 'worsening'
432
+ ? red('↑ 증가')
433
+ : dim('→ 유지');
434
+ const axisLines = data.topCorrectionAxes.length > 0
435
+ ? data.topCorrectionAxes.map(a => ` ${a.axis}: ${a.count}회`).join('\n')
436
+ : ` ${dim('(아직 교정 데이터 없음)')}`;
437
+ const savedHours = Math.floor(data.estimatedMinutesSaved / 60);
438
+ const savedMins = data.estimatedMinutesSaved % 60;
439
+ const savedStr = savedHours > 0 ? `${savedHours}시간 ${savedMins}분` : `${savedMins}분`;
440
+ return [
441
+ ` ${bold('📈 Learning Curve / 학습 곡선')}`,
442
+ ``,
443
+ ` 교정 추이 (지난 7일):`,
444
+ ` 이번 주: ${data.correctionsLast7d}건`,
445
+ ` 지난 주: ${data.correctionsPrev7d}건`,
446
+ ` 추세: ${trendIcon}`,
447
+ ``,
448
+ ` 주요 교정 축 (누적):`,
449
+ axisLines,
450
+ ``,
451
+ ` 누적 사용:`,
452
+ ` 활동한 일수: ${data.evidenceTotalDays}일`,
453
+ ` 분석된 세션: 약 ${data.sessionsAnalyzed}회`,
454
+ ``,
455
+ ` ${cyan('추정 절약 시간')} (compound 주입 성공 기반):`,
456
+ ` ${bold(savedStr)} ${dim('(지난 7일)')}`,
457
+ ` ${dim('※ compound가 힌트를 제공한 매 1회당 평균 8분 절약 가정')}`,
458
+ ].join('\n');
459
+ }
460
+ export function renderDashboard() {
461
+ const knowledge = collectKnowledgeOverview();
462
+ const injection = collectInjectionActivity();
463
+ const reflection = collectReflectionData();
464
+ const lifecycle = collectLifecycleActivity();
465
+ const session = collectSessionHistory();
466
+ const hookHealth = collectHookHealth();
467
+ const learning = collectLearningCurve();
468
+ const divider = ` ${dim('─'.repeat(50))}`;
469
+ const sections = [
470
+ '',
471
+ ` ${BOLD}${CYAN}╔══════════════════════════════════════════════╗${RESET}`,
472
+ ` ${BOLD}${CYAN}║ Forgen Compound Dashboard ║${RESET}`,
473
+ ` ${BOLD}${CYAN}╚══════════════════════════════════════════════╝${RESET}`,
474
+ '',
475
+ renderLearningCurve(learning),
476
+ divider,
477
+ renderKnowledgeOverview(knowledge),
478
+ divider,
479
+ renderInjectionActivity(injection),
480
+ divider,
481
+ renderReflectionData(reflection),
482
+ divider,
483
+ renderLifecycleActivity(lifecycle),
484
+ divider,
485
+ renderSessionHistory(session),
486
+ divider,
487
+ renderHookHealth(hookHealth),
488
+ '',
489
+ ];
490
+ return sections.join('\n');
491
+ }
492
+ /** CLI handler: forgen dashboard */
493
+ export async function handleDashboard() {
494
+ console.log(renderDashboard());
495
+ }