@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.
- package/CHANGELOG.md +72 -0
- package/README.ja.md +79 -14
- package/README.ko.md +100 -14
- package/README.md +124 -17
- package/README.zh.md +79 -14
- package/agents/analyst.md +48 -4
- package/agents/architect.md +39 -4
- package/agents/code-reviewer.md +107 -77
- package/agents/critic.md +47 -4
- package/agents/debugger.md +46 -4
- package/agents/designer.md +40 -4
- package/agents/executor.md +112 -30
- package/agents/explore.md +45 -5
- package/agents/git-master.md +48 -4
- package/agents/planner.md +121 -18
- package/agents/test-engineer.md +58 -4
- package/agents/verifier.md +92 -77
- package/commands/architecture-decision.md +127 -258
- package/commands/calibrate.md +225 -0
- package/commands/code-review.md +163 -178
- package/commands/compound.md +127 -68
- package/commands/deep-interview.md +273 -0
- package/commands/docker.md +68 -178
- package/commands/forge-loop.md +215 -0
- package/commands/learn.md +231 -0
- package/commands/retro.md +215 -0
- package/commands/ship.md +277 -0
- package/dist/cli.js +26 -9
- package/dist/core/auto-compound-runner.js +14 -0
- package/dist/core/config-injector.d.ts +2 -1
- package/dist/core/config-injector.js +2 -1
- package/dist/core/dashboard.d.ts +108 -0
- package/dist/core/dashboard.js +495 -0
- package/dist/core/doctor.js +151 -21
- package/dist/core/drift-score.d.ts +49 -0
- package/dist/core/drift-score.js +87 -0
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/mcp-config.d.ts +2 -0
- package/dist/core/mcp-config.js +6 -1
- package/dist/core/paths.d.ts +6 -1
- package/dist/core/paths.js +18 -2
- package/dist/core/spawn.d.ts +3 -2
- package/dist/core/spawn.js +27 -8
- package/dist/core/types.d.ts +34 -0
- package/dist/engine/compound-export.d.ts +41 -0
- package/dist/engine/compound-export.js +169 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/compound-loop.js +18 -0
- package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
- package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
- package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
- package/dist/engine/meta-learning/extraction-tuner.js +99 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
- package/dist/engine/meta-learning/runner.d.ts +14 -0
- package/dist/engine/meta-learning/runner.js +90 -0
- package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
- package/dist/engine/meta-learning/scope-promoter.js +84 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
- package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
- package/dist/engine/meta-learning/types.d.ts +114 -0
- package/dist/engine/meta-learning/types.js +43 -0
- package/dist/engine/solution-format.d.ts +2 -2
- package/dist/engine/solution-format.js +249 -34
- package/dist/engine/solution-index.d.ts +1 -1
- package/dist/engine/solution-matcher.d.ts +30 -1
- package/dist/engine/solution-matcher.js +235 -45
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +15 -0
- package/dist/hooks/context-guard.js +218 -56
- package/dist/hooks/db-guard.js +2 -2
- package/dist/hooks/hook-config.d.ts +27 -1
- package/dist/hooks/hook-config.js +72 -12
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/intent-classifier.d.ts +0 -2
- package/dist/hooks/intent-classifier.js +32 -18
- package/dist/hooks/keyword-detector.js +126 -204
- package/dist/hooks/notepad-injector.js +2 -2
- package/dist/hooks/permission-handler.js +2 -2
- package/dist/hooks/post-tool-failure.js +12 -6
- package/dist/hooks/post-tool-handlers.d.ts +1 -1
- package/dist/hooks/post-tool-handlers.js +14 -11
- package/dist/hooks/post-tool-use.d.ts +11 -0
- package/dist/hooks/post-tool-use.js +184 -71
- package/dist/hooks/pre-compact.d.ts +11 -1
- package/dist/hooks/pre-compact.js +112 -37
- package/dist/hooks/pre-tool-use.js +86 -56
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/session-recovery.js +256 -236
- package/dist/hooks/shared/hook-response.d.ts +4 -4
- package/dist/hooks/shared/hook-response.js +13 -24
- package/dist/hooks/shared/hook-timing.d.ts +15 -0
- package/dist/hooks/shared/hook-timing.js +64 -0
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +47 -16
- package/dist/hooks/slop-detector.js +3 -3
- package/dist/hooks/solution-injector.js +224 -197
- package/dist/hooks/subagent-tracker.js +2 -2
- package/dist/host/codex-adapter.d.ts +10 -0
- package/dist/host/codex-adapter.js +154 -0
- package/dist/mcp/solution-reader.d.ts +5 -5
- package/dist/mcp/solution-reader.js +34 -24
- package/dist/renderer/rule-renderer.js +9 -11
- package/dist/services/session.d.ts +19 -0
- package/dist/services/session.js +62 -0
- package/hooks/hooks.json +2 -2
- package/package.json +2 -1
- package/skills/architecture-decision/SKILL.md +113 -257
- package/skills/calibrate/SKILL.md +207 -0
- package/skills/code-review/SKILL.md +151 -178
- package/skills/compound/SKILL.md +126 -68
- package/skills/deep-interview/SKILL.md +266 -0
- package/skills/docker/SKILL.md +57 -179
- package/skills/forge-loop/SKILL.md +198 -0
- package/skills/learn/SKILL.md +216 -0
- package/skills/retro/SKILL.md +199 -0
- package/skills/ship/SKILL.md +259 -0
- package/agents/code-simplifier.md +0 -197
- package/agents/performance-reviewer.md +0 -172
- package/agents/qa-tester.md +0 -158
- package/agents/refactoring-expert.md +0 -168
- package/agents/scientist.md +0 -144
- package/agents/security-reviewer.md +0 -137
- package/agents/writer.md +0 -184
- package/commands/api-design.md +0 -268
- package/commands/ci-cd.md +0 -270
- package/commands/database.md +0 -263
- package/commands/debug-detective.md +0 -99
- package/commands/documentation.md +0 -276
- package/commands/ecomode.md +0 -51
- package/commands/frontend.md +0 -271
- package/commands/git-master.md +0 -90
- package/commands/incident-response.md +0 -292
- package/commands/migrate.md +0 -101
- package/commands/performance.md +0 -288
- package/commands/refactor.md +0 -105
- package/commands/security-review.md +0 -288
- package/commands/tdd.md +0 -183
- package/commands/testing-strategy.md +0 -265
- package/skills/api-design/SKILL.md +0 -262
- package/skills/ci-cd/SKILL.md +0 -264
- package/skills/database/SKILL.md +0 -257
- package/skills/debug-detective/SKILL.md +0 -95
- package/skills/documentation/SKILL.md +0 -270
- package/skills/ecomode/SKILL.md +0 -46
- package/skills/frontend/SKILL.md +0 -265
- package/skills/git-master/SKILL.md +0 -86
- package/skills/incident-response/SKILL.md +0 -286
- package/skills/migrate/SKILL.md +0 -96
- package/skills/performance/SKILL.md +0 -282
- package/skills/refactor/SKILL.md +0 -100
- package/skills/security-review/SKILL.md +0 -282
- package/skills/tdd/SKILL.md +0 -178
- 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
|
+
}
|