@wooojin/forgen 0.4.1 → 0.4.3
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/.claude-plugin/plugin.json +5 -5
- package/CHANGELOG.md +164 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +17 -9
- package/README.ko.md +20 -12
- package/README.md +46 -12
- package/README.zh.md +17 -9
- package/assets/README.md +86 -0
- package/assets/architecture.svg +100 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +53 -0
- package/assets/demo/01-install.gif +0 -0
- package/assets/demo/01-install.tape +54 -0
- package/assets/demo/02-compound-learning.gif +0 -0
- package/assets/demo/02-compound-learning.tape +50 -0
- package/assets/demo/03-forge-personalization.gif +0 -0
- package/assets/demo/03-forge-personalization.tape +64 -0
- package/assets/demo/before-after.gif +0 -0
- package/assets/demo/before-after.tape +98 -0
- package/assets/demo-preview.svg +96 -0
- package/assets/icon.png +0 -0
- package/{hooks → assets/shared}/hook-registry.json +2 -1
- package/dist/cli.js +78 -6
- package/dist/core/auto-compound-runner.js +62 -38
- package/dist/core/behavior-classifier.d.ts +28 -0
- package/dist/core/behavior-classifier.js +46 -0
- package/dist/core/dashboard.d.ts +7 -0
- package/dist/core/dashboard.js +32 -0
- package/dist/core/doctor.js +92 -0
- package/dist/core/git-stats.d.ts +36 -0
- package/dist/core/git-stats.js +79 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.js +27 -20
- package/dist/core/host-detect.d.ts +42 -0
- package/dist/core/host-detect.js +68 -0
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +1 -0
- package/dist/core/migrate-cli.js +19 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/settings-injector.js +4 -2
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +4 -11
- package/dist/core/stats-cli.js +12 -0
- package/dist/core/trust-layer-intent.d.ts +35 -0
- package/dist/core/trust-layer-intent.js +30 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +4 -2
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/fgx.js +2 -1
- package/dist/forge/evidence-processor.js +12 -0
- package/dist/forge/onboarding.d.ts +3 -2
- package/dist/forge/onboarding.js +3 -2
- package/dist/hooks/db-guard.js +3 -3
- package/dist/hooks/forge-loop-progress.d.ts +9 -0
- package/dist/hooks/forge-loop-progress.js +38 -0
- package/dist/hooks/hook-registry.js +1 -1
- package/dist/hooks/hooks-generator.d.ts +15 -1
- package/dist/hooks/hooks-generator.js +18 -16
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +1 -1
- package/dist/hooks/post-tool-use.js +13 -4
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.js +4 -4
- package/dist/hooks/rate-limiter.js +2 -2
- package/dist/hooks/session-recovery.js +11 -0
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
- package/dist/hooks/shared/forge-loop-state.js +116 -0
- package/dist/hooks/shared/hook-response.d.ts +18 -0
- package/dist/hooks/shared/hook-response.js +31 -0
- package/dist/hooks/skill-injector.js +1 -1
- package/dist/hooks/stop-guard.js +15 -0
- package/dist/host/capabilities-claude.d.ts +8 -0
- package/dist/host/capabilities-claude.js +46 -0
- package/dist/host/capabilities-codex.d.ts +11 -0
- package/dist/host/capabilities-codex.js +50 -0
- package/dist/host/capabilities-registry.d.ts +11 -0
- package/dist/host/capabilities-registry.js +30 -0
- package/dist/host/codex-adapter.d.ts +8 -5
- package/dist/host/codex-adapter.js +10 -82
- package/dist/host/codex-output-parser.d.ts +39 -0
- package/dist/host/codex-output-parser.js +75 -0
- package/dist/host/exec-host.d.ts +54 -0
- package/dist/host/exec-host.js +92 -0
- package/dist/host/host-runtime.d.ts +37 -0
- package/dist/host/host-runtime.js +51 -0
- package/dist/host/install-claude.d.ts +35 -0
- package/dist/host/install-claude.js +238 -0
- package/dist/host/install-codex.d.ts +44 -0
- package/dist/host/install-codex.js +276 -0
- package/dist/host/install-orchestrator.d.ts +34 -0
- package/dist/host/install-orchestrator.js +126 -0
- package/dist/host/invoke-agent.d.ts +27 -0
- package/dist/host/invoke-agent.js +115 -0
- package/dist/host/parity-harness.d.ts +62 -0
- package/dist/host/parity-harness.js +283 -0
- package/dist/host/projection.d.ts +35 -0
- package/dist/host/projection.js +126 -0
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +47 -0
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +34 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +6 -4
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- /package/{agents → assets/claude/agents}/analyst.md +0 -0
- /package/{agents → assets/claude/agents}/architect.md +0 -0
- /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
- /package/{agents → assets/claude/agents}/critic.md +0 -0
- /package/{agents → assets/claude/agents}/debugger.md +0 -0
- /package/{agents → assets/claude/agents}/designer.md +0 -0
- /package/{agents → assets/claude/agents}/executor.md +0 -0
- /package/{agents → assets/claude/agents}/explore.md +0 -0
- /package/{agents → assets/claude/agents}/git-master.md +0 -0
- /package/{agents → assets/claude/agents}/planner.md +0 -0
- /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
- /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
- /package/{agents → assets/claude/agents}/verifier.md +0 -0
- /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
- /package/{commands → assets/claude/commands}/calibrate.md +0 -0
- /package/{commands → assets/claude/commands}/code-review.md +0 -0
- /package/{commands → assets/claude/commands}/compound.md +0 -0
- /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
- /package/{commands → assets/claude/commands}/docker.md +0 -0
- /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
- /package/{commands → assets/claude/commands}/learn.md +0 -0
- /package/{commands → assets/claude/commands}/retro.md +0 -0
- /package/{commands → assets/claude/commands}/ship.md +0 -0
package/dist/cli.js
CHANGED
|
@@ -112,8 +112,31 @@ const commands = [
|
|
|
112
112
|
await displayHookStatus(process.cwd());
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
else if (sub === 'default-host') {
|
|
116
|
+
const value = args[1];
|
|
117
|
+
const valid = new Set(['claude', 'codex', 'ask']);
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
const { getDefaultHost } = await import('./store/profile-store.js');
|
|
120
|
+
const current = getDefaultHost();
|
|
121
|
+
console.log(` current default_host: ${current ?? '(unset → claude fallback)'}`);
|
|
122
|
+
console.log(' Usage: forgen config default-host {claude|codex|ask}');
|
|
123
|
+
}
|
|
124
|
+
else if (!valid.has(value)) {
|
|
125
|
+
console.log(` Invalid value: ${value}. Use one of: claude, codex, ask`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const { setDefaultHost } = await import('./store/profile-store.js');
|
|
130
|
+
const ok = setDefaultHost(value);
|
|
131
|
+
if (!ok) {
|
|
132
|
+
console.log(' ✗ Profile not found. Run `forgen onboarding` first.');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
console.log(` ✓ default_host set to: ${value}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
115
138
|
else {
|
|
116
|
-
console.log('Usage
|
|
139
|
+
console.log('Usage:\n forgen config hooks [--regenerate]\n forgen config default-host [claude|codex|ask]');
|
|
117
140
|
}
|
|
118
141
|
},
|
|
119
142
|
},
|
|
@@ -133,6 +156,53 @@ const commands = [
|
|
|
133
156
|
await handleInit(args);
|
|
134
157
|
},
|
|
135
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: 'install',
|
|
161
|
+
description: 'Install forgen into a host. Usage: forgen install [claude|codex|both] [--dry-run] [--no-mcp]',
|
|
162
|
+
handler: async (args) => {
|
|
163
|
+
const knownSubs = new Set(['claude', 'codex', 'both']);
|
|
164
|
+
const target = args[0] && knownSubs.has(args[0]) ? args[0] : args[0]?.startsWith('--') ? undefined : args[0];
|
|
165
|
+
if (target !== undefined && !knownSubs.has(target)) {
|
|
166
|
+
console.log('Usage:\n forgen install [claude|codex|both] [--dry-run] [--no-mcp]\n\n No arg → interactive 3-choice (Claude/Codex/Both).');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const dryRun = args.includes('--dry-run');
|
|
170
|
+
const registerMcp = !args.includes('--no-mcp');
|
|
171
|
+
const { runInstall, renderResult, resolvePkgRootFromBinary } = await import('./host/install-orchestrator.js');
|
|
172
|
+
const pkgRoot = resolvePkgRootFromBinary(import.meta.url);
|
|
173
|
+
const result = await runInstall({ target, pkgRoot, dryRun, registerMcp });
|
|
174
|
+
if (result === null) {
|
|
175
|
+
console.log('\n [forgen] Install skipped.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log(renderResult(result, dryRun));
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'parity',
|
|
183
|
+
description: 'Run host parity checks. Usage: forgen parity codex [--dry-run]',
|
|
184
|
+
handler: async (args) => {
|
|
185
|
+
const sub = args[0];
|
|
186
|
+
if (sub !== 'codex') {
|
|
187
|
+
console.log('Usage:\n forgen parity codex [--dry-run]\n\nNotes:\n - source 체크아웃에서만 작동합니다 (tests/ 디렉토리 필요).\n - npm install 로 설치된 패키지에서는 run-parity.sh 가 없습니다.');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
191
|
+
const scriptPath = path.resolve(here, '..', 'tests', 'e2e', 'codex', 'run-parity.sh');
|
|
192
|
+
if (!fs.existsSync(scriptPath)) {
|
|
193
|
+
console.error('[forgen] run-parity.sh 는 source 체크아웃에서만 작동. 직접 git clone 후 실행하세요.');
|
|
194
|
+
console.error(` expected: ${scriptPath}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const { spawnSync } = await import('node:child_process');
|
|
198
|
+
const dryRun = args.includes('--dry-run');
|
|
199
|
+
const spawnArgs = dryRun ? ['--dry-run'] : [];
|
|
200
|
+
const result = spawnSync('bash', [scriptPath, ...spawnArgs], { stdio: 'inherit' });
|
|
201
|
+
if (result.status !== 0) {
|
|
202
|
+
process.exit(result.status ?? 1);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
},
|
|
136
206
|
{
|
|
137
207
|
name: 'notepad',
|
|
138
208
|
description: 'Notepad (show|add|clear)',
|
|
@@ -151,7 +221,7 @@ const commands = [
|
|
|
151
221
|
},
|
|
152
222
|
{
|
|
153
223
|
name: 'onboarding',
|
|
154
|
-
description: 'v1
|
|
224
|
+
description: 'v1 4-question onboarding flow',
|
|
155
225
|
handler: async (_args) => {
|
|
156
226
|
const { runOnboarding } = await import('./forge/onboarding-cli.js');
|
|
157
227
|
await runOnboarding();
|
|
@@ -407,7 +477,8 @@ async function main() {
|
|
|
407
477
|
${dim}Code, forged for you.${reset}
|
|
408
478
|
${dim}Scope: v1(${context.v1.session?.quality_pack ?? 'onboarding needed'})${reset}
|
|
409
479
|
`);
|
|
410
|
-
const
|
|
480
|
+
const { getHostRuntime } = await import('./host/host-runtime.js');
|
|
481
|
+
const runtimeLabel = getHostRuntime(runtime).displayName;
|
|
411
482
|
console.log(`[forgen] Starting ${runtimeLabel}...\n`);
|
|
412
483
|
await spawnClaudeWithResume(args, context, () => prepareHarness(process.cwd(), { runtime }), runtime);
|
|
413
484
|
}
|
|
@@ -441,7 +512,7 @@ function printHelp() {
|
|
|
441
512
|
|
|
442
513
|
Commands:
|
|
443
514
|
forgen forge Personalize your coding profile
|
|
444
|
-
forgen onboarding Run
|
|
515
|
+
forgen onboarding Run 4-question onboarding
|
|
445
516
|
forgen inspect [profile|rules|corrections|session]
|
|
446
517
|
Inspect v1 state (alias: evidence → corrections)
|
|
447
518
|
forgen rule <list|suppress|activate|scan|health-scan|classify>
|
|
@@ -450,8 +521,9 @@ function printHelp() {
|
|
|
450
521
|
forgen last-block Show the most recent block event
|
|
451
522
|
forgen recall [--limit N] [--show]
|
|
452
523
|
최근 compound 주입 이력 (solution body preview)
|
|
453
|
-
forgen migrate [implicit-feedback|all]
|
|
454
|
-
One-shot schema migration (category backfill)
|
|
524
|
+
forgen migrate [implicit-feedback|evidence-host|all]
|
|
525
|
+
One-shot schema migration (category backfill / host backfill)
|
|
526
|
+
forgen parity codex [--dry-run] Run codex parity checks (source checkout only)
|
|
455
527
|
forgen compound Manage accumulated knowledge
|
|
456
528
|
forgen dashboard Compound system dashboard
|
|
457
529
|
forgen me Personal dashboard
|
|
@@ -12,38 +12,66 @@
|
|
|
12
12
|
import * as fs from 'node:fs';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
15
16
|
import { containsPromptInjection, filterSolutionContent } from '../hooks/prompt-injection-filter.js';
|
|
16
17
|
import { redactSecrets } from '../hooks/secret-filter.js';
|
|
17
18
|
import { createEvidence, saveEvidence, promoteSessionCandidates } from '../store/evidence-store.js';
|
|
18
19
|
import { loadProfile } from '../store/profile-store.js';
|
|
19
20
|
import { FORGEN_HOME, ME_DIR } from './paths.js';
|
|
21
|
+
import { classifyBehaviorKind, mapKindToAxisRefs } from './behavior-classifier.js';
|
|
20
22
|
/** Auto-compound에 사용할 모델 — background 추출이므로 haiku로 충분 */
|
|
21
23
|
const COMPOUND_MODEL = 'haiku';
|
|
22
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Host-aware exec retry — feat/codex-support P2-3 (Phase 2 critic fix).
|
|
26
|
+
*
|
|
27
|
+
* 보안 회귀 방지: Claude 분기는 *args 그대로* execFileSync 호출 → P1-S1 의
|
|
28
|
+
* `--allowedTools Bash(forgen compound:*)` sandbox hardening 보존.
|
|
29
|
+
* Codex 분기에서만 -p prompt 추출 → execHost (codex 는 --allowedTools 모름).
|
|
30
|
+
*
|
|
31
|
+
* Codex retry 정책 fix: ETIMEDOUT 시 sleep 후 retry 는 *Claude only*. Codex 는
|
|
32
|
+
* 60-90s response 가 정상이라 timeout 누적 retry 가 무의미 (즉시 fail).
|
|
33
|
+
*/
|
|
23
34
|
function execClaudeRetry(args, opts) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const mod = createRequire(import.meta.url)('../host/exec-host.js');
|
|
36
|
+
// profile.default_host 로 host 결정 (lazy load)
|
|
37
|
+
const profileMod = createRequire(import.meta.url)('../store/profile-store.js');
|
|
38
|
+
const resolved = profileMod.resolveDefaultHost();
|
|
39
|
+
const host = resolved === 'codex' ? 'codex' : 'claude';
|
|
40
|
+
if (host === 'claude') {
|
|
41
|
+
// Claude 측은 기존 보안 hardening 보존: --allowedTools 등 args 그대로 전달.
|
|
42
|
+
const TRANSIENT = /ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE/;
|
|
43
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
44
|
+
try {
|
|
45
|
+
return execFileSync('claude', args, opts);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
49
|
+
if (attempt === 0 && TRANSIENT.test(msg)) {
|
|
50
|
+
process.stderr.write(`[forgen-auto-compound] transient error, retrying in 3s...\n`);
|
|
51
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 3000);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
throw e;
|
|
42
55
|
}
|
|
43
|
-
throw e;
|
|
44
56
|
}
|
|
57
|
+
throw new Error('unreachable');
|
|
45
58
|
}
|
|
46
|
-
|
|
59
|
+
// host === 'codex' — prompt 만 추출 (codex 는 --allowedTools 등 미인식).
|
|
60
|
+
const pIdx = args.indexOf('-p');
|
|
61
|
+
if (pIdx === -1 || !args[pIdx + 1]) {
|
|
62
|
+
throw new Error('execClaudeRetry: codex host requires -p prompt argument');
|
|
63
|
+
}
|
|
64
|
+
const prompt = args[pIdx + 1];
|
|
65
|
+
const modelIdx = args.indexOf('--model');
|
|
66
|
+
const model = modelIdx !== -1 ? args[modelIdx + 1] : undefined;
|
|
67
|
+
const r = mod.execHost({
|
|
68
|
+
prompt,
|
|
69
|
+
model,
|
|
70
|
+
host: 'codex',
|
|
71
|
+
timeout: typeof opts.timeout === 'number' ? opts.timeout : 60000,
|
|
72
|
+
cwd: typeof opts.cwd === 'string' ? opts.cwd : undefined,
|
|
73
|
+
});
|
|
74
|
+
return r.message;
|
|
47
75
|
}
|
|
48
76
|
const [, , cwd, transcriptPath, sessionId] = process.argv;
|
|
49
77
|
if (!cwd || !transcriptPath || !sessionId) {
|
|
@@ -205,9 +233,7 @@ function mergeOrCreateBehavior(dir, newContent, kind, today) {
|
|
|
205
233
|
fs.writeFileSync(filePath, updated);
|
|
206
234
|
return true;
|
|
207
235
|
}
|
|
208
|
-
catch {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
236
|
+
catch { }
|
|
211
237
|
}
|
|
212
238
|
return false;
|
|
213
239
|
}
|
|
@@ -308,14 +334,15 @@ ${sanitizedSummary.slice(0, 6000)}
|
|
|
308
334
|
관찰된 패턴을 다음 형식으로 1~3개만 출력해주세요 (없으면 "관찰된 패턴 없음"):
|
|
309
335
|
- [카테고리] 패턴 설명 (관찰 근거)
|
|
310
336
|
|
|
311
|
-
카테고리:
|
|
337
|
+
카테고리: 커뮤니케이션/작업습관/기술선호/의사결정/워크플로우/품질안전/자율성
|
|
312
338
|
|
|
313
|
-
|
|
314
|
-
-
|
|
315
|
-
-
|
|
316
|
-
-
|
|
339
|
+
각 카테고리 가이드:
|
|
340
|
+
- "워크플로우": 반복하는 작업 순서, 판단 규칙, 조건부 접근법 (예: "테스트 먼저 → 구현 → 리팩토링 순서")
|
|
341
|
+
- "품질안전": 검증/테스트/안전성 관련 강한 선호 (예: "프로덕션 배포 전 Docker e2e 의무", "mock-only 검증 거부")
|
|
342
|
+
- "자율성": 확인/독립 결정 관련 선호 (예: "사소한 변경은 묻지 않고 진행", "큰 결정은 반드시 확인")
|
|
317
343
|
|
|
318
344
|
워크플로우 패턴이 감지되면 반드시 구체적인 순서를 포함하세요.
|
|
345
|
+
품질안전/자율성 패턴은 4축 개인화의 입력이므로 quality/autonomy 신호가 명확하면 반드시 해당 라벨을 사용하세요 (커뮤니케이션/작업습관 으로 흡수 금지).
|
|
319
346
|
|
|
320
347
|
기존 패턴과 중복이면 건너뛰세요.${existingBehaviorPatterns}
|
|
321
348
|
|
|
@@ -346,11 +373,11 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
346
373
|
fs.mkdirSync(BEHAVIOR_DIR, { recursive: true });
|
|
347
374
|
const today = new Date().toISOString().split('T')[0];
|
|
348
375
|
const trimmed = userResult.trim();
|
|
349
|
-
// 카테고리에 따라 kind 분류
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
376
|
+
// 카테고리에 따라 kind 분류 — D1'' (2026-04-27): quality/autonomy 라벨 추가.
|
|
377
|
+
// 이전 3분기(workflow/thinking/preference)는 quality_safety/autonomy 축으로
|
|
378
|
+
// 가는 자동 신호를 communication_style 로 흡수해 626건 중 자동 추출 0건이
|
|
379
|
+
// 이 두 축에 닿지 못했음. 5분기로 확장. (분류 로직은 behavior-classifier.ts)
|
|
380
|
+
const kind = classifyBehaviorKind(trimmed);
|
|
354
381
|
// 기존 유사 패턴이 있으면 observedCount 누적
|
|
355
382
|
const merged = mergeOrCreateBehavior(BEHAVIOR_DIR, trimmed, kind, today);
|
|
356
383
|
if (!merged) {
|
|
@@ -367,10 +394,7 @@ ${sanitizedSummary.slice(0, 4000)}
|
|
|
367
394
|
session_id: sessionId,
|
|
368
395
|
source_component: 'auto-compound-runner',
|
|
369
396
|
summary: trimmed.slice(0, 200),
|
|
370
|
-
axis_refs: kind
|
|
371
|
-
: kind === 'preference' ? ['communication_style']
|
|
372
|
-
: kind === 'thinking' ? ['judgment_philosophy']
|
|
373
|
-
: [],
|
|
397
|
+
axis_refs: mapKindToAxisRefs(kind),
|
|
374
398
|
confidence: 0.6,
|
|
375
399
|
raw_payload: { kind, observedCount: 1 },
|
|
376
400
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior Classifier — D1'' (2026-04-27)
|
|
3
|
+
*
|
|
4
|
+
* LLM 이 추출한 사용자 패턴을 5개 kind 로 분류하고 4축 axis_refs 로 매핑한다.
|
|
5
|
+
*
|
|
6
|
+
* 결함 history:
|
|
7
|
+
* v0.4.1 까지: kind 3분기(workflow/thinking/preference) → axis 2축
|
|
8
|
+
* (judgment_philosophy / communication_style) 만 자동 추출 가능.
|
|
9
|
+
* quality_safety / autonomy 축은 explicit_correction 16건 (Hooks 경로) 으로만
|
|
10
|
+
* 자라고, 자동 학습 600+ 건은 이 두 축에 0% 기여 — 측정 자기증거.
|
|
11
|
+
*
|
|
12
|
+
* v0.4.2: 5분기 [품질안전] / [자율성] 추가 → 4축 모두 cover.
|
|
13
|
+
* LLM prompt (auto-compound-runner) 에도 같은 라벨 가이드를 명시하여
|
|
14
|
+
* 형식 강제. 새 라벨이 안 나오면 기존 5분기로 fallback (호환).
|
|
15
|
+
*/
|
|
16
|
+
export type BehaviorKind = 'safety' | 'autonomy' | 'workflow' | 'thinking' | 'preference';
|
|
17
|
+
/**
|
|
18
|
+
* LLM 출력 텍스트(`[카테고리] 설명` 형식)를 5개 kind 로 분류.
|
|
19
|
+
*
|
|
20
|
+
* 라벨 우선순위 (위에서 아래):
|
|
21
|
+
* 1. [품질안전] → safety
|
|
22
|
+
* 2. [자율성] → autonomy
|
|
23
|
+
* 3. [워크플로우] OR "순서"/"→" 토큰 → workflow
|
|
24
|
+
* 4. [의사결정] → thinking
|
|
25
|
+
* 5. 그 외 → preference (default)
|
|
26
|
+
*/
|
|
27
|
+
export declare function classifyBehaviorKind(text: string): BehaviorKind;
|
|
28
|
+
export declare function mapKindToAxisRefs(kind: BehaviorKind): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior Classifier — D1'' (2026-04-27)
|
|
3
|
+
*
|
|
4
|
+
* LLM 이 추출한 사용자 패턴을 5개 kind 로 분류하고 4축 axis_refs 로 매핑한다.
|
|
5
|
+
*
|
|
6
|
+
* 결함 history:
|
|
7
|
+
* v0.4.1 까지: kind 3분기(workflow/thinking/preference) → axis 2축
|
|
8
|
+
* (judgment_philosophy / communication_style) 만 자동 추출 가능.
|
|
9
|
+
* quality_safety / autonomy 축은 explicit_correction 16건 (Hooks 경로) 으로만
|
|
10
|
+
* 자라고, 자동 학습 600+ 건은 이 두 축에 0% 기여 — 측정 자기증거.
|
|
11
|
+
*
|
|
12
|
+
* v0.4.2: 5분기 [품질안전] / [자율성] 추가 → 4축 모두 cover.
|
|
13
|
+
* LLM prompt (auto-compound-runner) 에도 같은 라벨 가이드를 명시하여
|
|
14
|
+
* 형식 강제. 새 라벨이 안 나오면 기존 5분기로 fallback (호환).
|
|
15
|
+
*/
|
|
16
|
+
const AXIS_REFS_BY_KIND = {
|
|
17
|
+
safety: ['quality_safety'],
|
|
18
|
+
autonomy: ['autonomy'],
|
|
19
|
+
workflow: ['judgment_philosophy'],
|
|
20
|
+
thinking: ['judgment_philosophy'],
|
|
21
|
+
preference: ['communication_style'],
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* LLM 출력 텍스트(`[카테고리] 설명` 형식)를 5개 kind 로 분류.
|
|
25
|
+
*
|
|
26
|
+
* 라벨 우선순위 (위에서 아래):
|
|
27
|
+
* 1. [품질안전] → safety
|
|
28
|
+
* 2. [자율성] → autonomy
|
|
29
|
+
* 3. [워크플로우] OR "순서"/"→" 토큰 → workflow
|
|
30
|
+
* 4. [의사결정] → thinking
|
|
31
|
+
* 5. 그 외 → preference (default)
|
|
32
|
+
*/
|
|
33
|
+
export function classifyBehaviorKind(text) {
|
|
34
|
+
if (text.includes('[품질안전]'))
|
|
35
|
+
return 'safety';
|
|
36
|
+
if (text.includes('[자율성]'))
|
|
37
|
+
return 'autonomy';
|
|
38
|
+
if (text.includes('[워크플로우]') || text.includes('순서') || text.includes('→'))
|
|
39
|
+
return 'workflow';
|
|
40
|
+
if (text.includes('[의사결정]'))
|
|
41
|
+
return 'thinking';
|
|
42
|
+
return 'preference';
|
|
43
|
+
}
|
|
44
|
+
export function mapKindToAxisRefs(kind) {
|
|
45
|
+
return [...AXIS_REFS_BY_KIND[kind]];
|
|
46
|
+
}
|
package/dist/core/dashboard.d.ts
CHANGED
|
@@ -86,6 +86,13 @@ export declare function collectLifecycleActivity(): LifecycleActivity;
|
|
|
86
86
|
export declare function collectSessionHistory(): SessionHistory;
|
|
87
87
|
/** Collect hook error data. */
|
|
88
88
|
export declare function collectHookHealth(): HookHealth;
|
|
89
|
+
export interface MultiHostData {
|
|
90
|
+
claude: number;
|
|
91
|
+
codex: number;
|
|
92
|
+
total: number;
|
|
93
|
+
}
|
|
94
|
+
/** Collect multi-host evidence distribution from host-mismatch store. */
|
|
95
|
+
export declare function collectMultiHostData(): MultiHostData;
|
|
89
96
|
export interface LearningCurve {
|
|
90
97
|
correctionsLast7d: number;
|
|
91
98
|
correctionsPrev7d: number;
|
package/dist/core/dashboard.js
CHANGED
|
@@ -23,6 +23,7 @@ const require = createRequire(import.meta.url);
|
|
|
23
23
|
import { ME_SOLUTIONS, ME_RULES, ME_BEHAVIOR, STATE_DIR, } from './paths.js';
|
|
24
24
|
import { parseFrontmatterOnly } from '../engine/solution-format.js';
|
|
25
25
|
import { readMatchEvalLog } from '../engine/match-eval-log.js';
|
|
26
|
+
import { summarizeAllByHost } from '../store/host-mismatch.js';
|
|
26
27
|
// ── ANSI color helpers ──
|
|
27
28
|
const BOLD = '\x1b[1m';
|
|
28
29
|
const DIM = '\x1b[2m';
|
|
@@ -356,6 +357,34 @@ function renderHookHealth(data) {
|
|
|
356
357
|
lines.push(tableSep(widths, false, true));
|
|
357
358
|
return lines.join('\n');
|
|
358
359
|
}
|
|
360
|
+
/** Collect multi-host evidence distribution from host-mismatch store. */
|
|
361
|
+
export function collectMultiHostData() {
|
|
362
|
+
try {
|
|
363
|
+
return summarizeAllByHost();
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return { claude: 0, codex: 0, total: 0 };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function renderMultiHost(data) {
|
|
370
|
+
const lines = [];
|
|
371
|
+
lines.push(` ${bold(cyan('Multi-Host Evidence'))}`);
|
|
372
|
+
lines.push('');
|
|
373
|
+
if (data.total === 0) {
|
|
374
|
+
lines.push(` ${dim('No evidence recorded yet.')}`);
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
const claudePct = Math.round((data.claude / data.total) * 100);
|
|
378
|
+
const codexPct = Math.round((data.codex / data.total) * 100);
|
|
379
|
+
lines.push(` Hosts claude:${data.claude} (${claudePct}%) codex:${data.codex} (${codexPct}%) total:${data.total}`);
|
|
380
|
+
// skew 경고: 80%+ 집중
|
|
381
|
+
const maxShare = Math.max(claudePct, codexPct);
|
|
382
|
+
if (data.total >= 5 && maxShare >= 80) {
|
|
383
|
+
const dominant = data.claude >= data.codex ? 'claude' : 'codex';
|
|
384
|
+
lines.push(` ${yellow(`⚠ ${dominant} 에 ${maxShare}% 집중 — 다른 host 데이터 부족`)}`);
|
|
385
|
+
}
|
|
386
|
+
return lines.join('\n');
|
|
387
|
+
}
|
|
359
388
|
/**
|
|
360
389
|
* Learning Curve 수집.
|
|
361
390
|
* evidence 파일(교정 기록)과 compound 활용률을 교차 분석하여 "쓸수록 나아진다"를 정량화.
|
|
@@ -523,6 +552,7 @@ export function renderDashboard() {
|
|
|
523
552
|
const session = collectSessionHistory();
|
|
524
553
|
const hookHealth = collectHookHealth();
|
|
525
554
|
const learning = collectLearningCurve();
|
|
555
|
+
const multiHost = collectMultiHostData();
|
|
526
556
|
const divider = ` ${dim('─'.repeat(50))}`;
|
|
527
557
|
const sections = [
|
|
528
558
|
'',
|
|
@@ -538,6 +568,8 @@ export function renderDashboard() {
|
|
|
538
568
|
divider,
|
|
539
569
|
renderInjectionActivity(injection),
|
|
540
570
|
divider,
|
|
571
|
+
renderMultiHost(multiHost),
|
|
572
|
+
divider,
|
|
541
573
|
renderReflectionData(reflection),
|
|
542
574
|
divider,
|
|
543
575
|
renderLifecycleActivity(lifecycle),
|
package/dist/core/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
5
5
|
import { FORGEN_HOME, LAB_DIR, ME_BEHAVIOR, ME_DIR, ME_SOLUTIONS, ME_RULES, ME_SKILLS, PACKS_DIR, SESSIONS_DIR, STATE_DIR } from './paths.js';
|
|
6
6
|
import { getTimingStats } from '../hooks/shared/hook-timing.js';
|
|
7
7
|
import { countSessionScopedFiles, pruneState } from './state-gc.js';
|
|
8
|
+
import { summarizeAllByHost } from '../store/host-mismatch.js';
|
|
8
9
|
/** ~/.claude/projects/ — Claude Code 세션 저장 경로 */
|
|
9
10
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
10
11
|
let currentSection = '';
|
|
@@ -34,6 +35,58 @@ function commandExists(cmd) {
|
|
|
34
35
|
return false;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
/** parity-result.json 내용에서 경과 시간을 사람이 읽기 좋은 문자열로 변환 */
|
|
39
|
+
function relativeTime(isoString) {
|
|
40
|
+
const diffMs = Date.now() - new Date(isoString).getTime();
|
|
41
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
42
|
+
if (diffDays === 0) {
|
|
43
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
44
|
+
if (diffHours === 0) {
|
|
45
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
46
|
+
return `${diffMins}m ago`;
|
|
47
|
+
}
|
|
48
|
+
return `${diffHours}h ago`;
|
|
49
|
+
}
|
|
50
|
+
return `${diffDays}d ago`;
|
|
51
|
+
}
|
|
52
|
+
/** [Codex Parity] 섹션 렌더링 — ~/.forgen/state/parity-result.json 신선도 검사 */
|
|
53
|
+
function renderCodexParity() {
|
|
54
|
+
console.log(' [Codex Parity]');
|
|
55
|
+
const parityPath = path.join(STATE_DIR, 'parity-result.json');
|
|
56
|
+
if (!fs.existsSync(parityPath)) {
|
|
57
|
+
console.log(' △ Codex parity 미실행 — tests/e2e/codex/run-parity.sh 또는 forgen parity codex');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let data;
|
|
61
|
+
try {
|
|
62
|
+
data = JSON.parse(fs.readFileSync(parityPath, 'utf-8'));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
console.log(' ✗ Codex parity — parity-result.json 파싱 실패');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (data.passed === null || data.passed === undefined) {
|
|
69
|
+
console.log(' △ Codex parity dry-run only — 실 실행 필요');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!data.passed) {
|
|
73
|
+
const timeStr = data.at ? relativeTime(data.at) : 'unknown';
|
|
74
|
+
const detail = data.result ?? data.note ?? 'no detail';
|
|
75
|
+
console.log(` ✗ Codex parity FAILED (at: ${timeStr}, detail: ${detail})`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// passed === true
|
|
79
|
+
const timeStr = data.at ? relativeTime(data.at) : 'unknown';
|
|
80
|
+
const version = data.version ? ` version ${data.version}` : '';
|
|
81
|
+
const diffMs = data.at ? Date.now() - new Date(data.at).getTime() : Infinity;
|
|
82
|
+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
83
|
+
if (diffMs > sevenDaysMs) {
|
|
84
|
+
console.log(` △ Codex parity green but stale (last run: ${timeStr}) — 재실행 권장`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log(` ✓ Codex parity green (last run: ${timeStr},${version})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
37
90
|
export async function runDoctor(opts = {}) {
|
|
38
91
|
failedChecks = [];
|
|
39
92
|
console.log('\n Forgen — Diagnostics\n');
|
|
@@ -386,6 +439,45 @@ export async function runDoctor(opts = {}) {
|
|
|
386
439
|
// git 저장소가 아니거나 origin이 없으면 표시하지 않음
|
|
387
440
|
console.log(' git remote: (none)');
|
|
388
441
|
}
|
|
442
|
+
// P4 셀프 가드: fix:feat 비율 30% 초과 시 회귀 패턴 의심 경고.
|
|
443
|
+
try {
|
|
444
|
+
const { computeFixFeatRatio, formatFixRatio } = await import('./git-stats.js');
|
|
445
|
+
const ratio = computeFixFeatRatio();
|
|
446
|
+
if (ratio.available) {
|
|
447
|
+
console.log(` ${formatFixRatio(ratio)}`);
|
|
448
|
+
if (ratio.exceedsThreshold) {
|
|
449
|
+
console.log(' ⚠ fix:feat 비율이 임계값을 초과했습니다. "이거 고치면 저거 버그난다" 패턴 의심 — 검증 레이어 invariant 점검 권장.');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch { /* fail-open */ }
|
|
454
|
+
console.log();
|
|
455
|
+
// [Multi-Host] — host 별 evidence 분포
|
|
456
|
+
console.log(' [Multi-Host]');
|
|
457
|
+
try {
|
|
458
|
+
const hostStats = summarizeAllByHost();
|
|
459
|
+
if (hostStats.total === 0) {
|
|
460
|
+
console.log(' No evidence recorded yet.');
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
const claudePct = hostStats.total > 0 ? Math.round((hostStats.claude / hostStats.total) * 100) : 0;
|
|
464
|
+
const codexPct = hostStats.total > 0 ? Math.round((hostStats.codex / hostStats.total) * 100) : 0;
|
|
465
|
+
console.log(` Registered hosts: claude, codex`);
|
|
466
|
+
console.log(` Evidence by host: claude:${hostStats.claude} (${claudePct}%) codex:${hostStats.codex} (${codexPct}%) total:${hostStats.total}`);
|
|
467
|
+
// 한 host 가 80% 이상이면 skew 경고
|
|
468
|
+
const maxShare = Math.max(claudePct, codexPct);
|
|
469
|
+
if (hostStats.total >= 5 && maxShare >= 80) {
|
|
470
|
+
const dominant = claudePct >= codexPct ? 'claude' : 'codex';
|
|
471
|
+
console.log(` ⚠ evidence 가 ${dominant} 에 ${maxShare}% 집중됨 — 다른 host 에서 학습 데이터 부족 가능`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
console.log(' Unable to read host evidence data.');
|
|
477
|
+
}
|
|
478
|
+
console.log();
|
|
479
|
+
// [Codex Parity] — parity-result.json 신선도 검사 (v0.4.2 패턴 확장)
|
|
480
|
+
renderCodexParity();
|
|
389
481
|
console.log();
|
|
390
482
|
// [Summary] — 최종 상태 요약과 복구 액션을 한눈에 보이게
|
|
391
483
|
console.log(' [Summary]');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Stats — P4 셀프 가드 (2026-04-27)
|
|
3
|
+
*
|
|
4
|
+
* 최근 N커밋의 conventional commit 분포를 측정해 fix:feat 비율을 계산.
|
|
5
|
+
* 정상 OSS 권장은 fix < 20%. 36% 초과 시 회귀 패턴 의심 — forgen 의 자기 메타 가드.
|
|
6
|
+
*
|
|
7
|
+
* 이번 세션 측정값: v0.4.1 시점 fix 비율 36% (정상의 약 2배). 이 코드가 다음 릴리즈
|
|
8
|
+
* 시 같은 비율을 자동 노출하여 사용자가 회귀 패턴을 빠르게 인지하게 한다.
|
|
9
|
+
*/
|
|
10
|
+
export interface FixRatioStats {
|
|
11
|
+
windowSize: number;
|
|
12
|
+
fixCount: number;
|
|
13
|
+
featCount: number;
|
|
14
|
+
/** fix / (fix + feat), 0~1. fix+feat=0 이면 0. */
|
|
15
|
+
ratio: number;
|
|
16
|
+
threshold: number;
|
|
17
|
+
exceedsThreshold: boolean;
|
|
18
|
+
/** git 명령이 성공했는지 (저장소 외부 또는 git 미설치 시 false). */
|
|
19
|
+
available: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* git log --no-merges -N 결과에서 conventional commit 형식의 fix/feat 만 카운트.
|
|
23
|
+
*
|
|
24
|
+
* 분류:
|
|
25
|
+
* - `feat: ...` / `feat(scope): ...` → feat
|
|
26
|
+
* - `fix: ...` / `fix(scope): ...` → fix (단, scope ∈ {test, tests, docs, doc} 제외)
|
|
27
|
+
* - 그 외 (chore, refactor, docs, style, test, hash 없는 라인) → 무시
|
|
28
|
+
*
|
|
29
|
+
* fix(test):, fix(docs): 가 제외되는 이유: 사소한 노이즈 fix 가 회귀 신호를
|
|
30
|
+
* 흐리지 않도록. 진짜 위험은 fix(core), fix(hook), fix(api) 같은 logic fix.
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeFixFeatRatio(cwd?: string, windowSize?: number, threshold?: number): FixRatioStats;
|
|
33
|
+
/** 테스트용 — git log 출력 텍스트를 직접 파싱. */
|
|
34
|
+
export declare function parseGitLog(rawLog: string, windowSize?: number, threshold?: number): FixRatioStats;
|
|
35
|
+
/** 사람용 한 줄 라벨. */
|
|
36
|
+
export declare function formatFixRatio(s: FixRatioStats): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Stats — P4 셀프 가드 (2026-04-27)
|
|
3
|
+
*
|
|
4
|
+
* 최근 N커밋의 conventional commit 분포를 측정해 fix:feat 비율을 계산.
|
|
5
|
+
* 정상 OSS 권장은 fix < 20%. 36% 초과 시 회귀 패턴 의심 — forgen 의 자기 메타 가드.
|
|
6
|
+
*
|
|
7
|
+
* 이번 세션 측정값: v0.4.1 시점 fix 비율 36% (정상의 약 2배). 이 코드가 다음 릴리즈
|
|
8
|
+
* 시 같은 비율을 자동 노출하여 사용자가 회귀 패턴을 빠르게 인지하게 한다.
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
const DEFAULT_THRESHOLD = 0.30;
|
|
12
|
+
const DEFAULT_WINDOW = 30;
|
|
13
|
+
const SCOPE_EXCLUSIONS = new Set(['test', 'tests', 'docs', 'doc']);
|
|
14
|
+
/**
|
|
15
|
+
* git log --no-merges -N 결과에서 conventional commit 형식의 fix/feat 만 카운트.
|
|
16
|
+
*
|
|
17
|
+
* 분류:
|
|
18
|
+
* - `feat: ...` / `feat(scope): ...` → feat
|
|
19
|
+
* - `fix: ...` / `fix(scope): ...` → fix (단, scope ∈ {test, tests, docs, doc} 제외)
|
|
20
|
+
* - 그 외 (chore, refactor, docs, style, test, hash 없는 라인) → 무시
|
|
21
|
+
*
|
|
22
|
+
* fix(test):, fix(docs): 가 제외되는 이유: 사소한 노이즈 fix 가 회귀 신호를
|
|
23
|
+
* 흐리지 않도록. 진짜 위험은 fix(core), fix(hook), fix(api) 같은 logic fix.
|
|
24
|
+
*/
|
|
25
|
+
export function computeFixFeatRatio(cwd = process.cwd(), windowSize = DEFAULT_WINDOW, threshold = DEFAULT_THRESHOLD) {
|
|
26
|
+
try {
|
|
27
|
+
const out = execFileSync('git', ['log', '--no-merges', '--oneline', `-${windowSize}`], { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
28
|
+
return parseGitLog(out, windowSize, threshold);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return makeUnavailable(windowSize, threshold);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** 테스트용 — git log 출력 텍스트를 직접 파싱. */
|
|
35
|
+
export function parseGitLog(rawLog, windowSize = DEFAULT_WINDOW, threshold = DEFAULT_THRESHOLD) {
|
|
36
|
+
const lines = rawLog.trim().split('\n').filter(Boolean);
|
|
37
|
+
let fix = 0;
|
|
38
|
+
let feat = 0;
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const msg = line.replace(/^[a-f0-9]{4,40}\s+/, '');
|
|
41
|
+
const m = msg.match(/^(fix|feat)(?:\(([^)]+)\))?:/);
|
|
42
|
+
if (!m)
|
|
43
|
+
continue;
|
|
44
|
+
const type = m[1];
|
|
45
|
+
const scope = (m[2] ?? '').toLowerCase().trim();
|
|
46
|
+
if (type === 'fix' && SCOPE_EXCLUSIONS.has(scope))
|
|
47
|
+
continue;
|
|
48
|
+
if (type === 'fix')
|
|
49
|
+
fix++;
|
|
50
|
+
else
|
|
51
|
+
feat++;
|
|
52
|
+
}
|
|
53
|
+
const total = fix + feat;
|
|
54
|
+
const ratio = total === 0 ? 0 : fix / total;
|
|
55
|
+
return {
|
|
56
|
+
windowSize,
|
|
57
|
+
fixCount: fix,
|
|
58
|
+
featCount: feat,
|
|
59
|
+
ratio,
|
|
60
|
+
threshold,
|
|
61
|
+
exceedsThreshold: ratio > threshold,
|
|
62
|
+
available: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function makeUnavailable(windowSize, threshold) {
|
|
66
|
+
return {
|
|
67
|
+
windowSize, fixCount: 0, featCount: 0, ratio: 0,
|
|
68
|
+
threshold, exceedsThreshold: false, available: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** 사람용 한 줄 라벨. */
|
|
72
|
+
export function formatFixRatio(s) {
|
|
73
|
+
if (!s.available)
|
|
74
|
+
return 'fix:feat ratio n/a (git unavailable)';
|
|
75
|
+
const pct = (s.ratio * 100).toFixed(0);
|
|
76
|
+
const thresholdPct = (s.threshold * 100).toFixed(0);
|
|
77
|
+
const flag = s.exceedsThreshold ? ` ⚠ over ${thresholdPct}%` : '';
|
|
78
|
+
return `fix:feat ratio ${pct}% (${s.fixCount}/${s.fixCount + s.featCount} in last ${s.windowSize})${flag}`;
|
|
79
|
+
}
|