@wooojin/forgen 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +94 -0
- package/README.ja.md +119 -8
- package/README.ko.md +73 -2
- package/README.md +163 -9
- package/README.zh.md +87 -7
- package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
- package/dist/checks/conclusion-verification-ratio.js +86 -0
- package/dist/checks/fact-vs-agreement.d.ts +47 -0
- package/dist/checks/fact-vs-agreement.js +92 -0
- package/dist/checks/self-score-deflation.d.ts +38 -0
- package/dist/checks/self-score-deflation.js +108 -0
- package/dist/cli.js +158 -6
- package/dist/core/auto-compound-runner.js +85 -13
- package/dist/core/dashboard.js +9 -2
- package/dist/core/doctor.js +90 -15
- package/dist/core/extraction-notice.d.ts +18 -0
- package/dist/core/extraction-notice.js +64 -0
- package/dist/core/init-cli.d.ts +26 -0
- package/dist/core/init-cli.js +104 -0
- package/dist/core/init.js +17 -0
- package/dist/core/inspect-cli.js +64 -5
- package/dist/core/migrate-cli.d.ts +10 -0
- package/dist/core/migrate-cli.js +34 -0
- package/dist/core/paths.d.ts +8 -1
- package/dist/core/paths.js +11 -2
- package/dist/core/recall-cli.d.ts +26 -0
- package/dist/core/recall-cli.js +125 -0
- package/dist/core/recall-reference-detector.d.ts +43 -0
- package/dist/core/recall-reference-detector.js +65 -0
- package/dist/core/state-gc.d.ts +19 -0
- package/dist/core/state-gc.js +48 -4
- package/dist/core/stats-cli.d.ts +36 -0
- package/dist/core/stats-cli.js +254 -0
- package/dist/core/uninstall.d.ts +1 -0
- package/dist/core/uninstall.js +25 -1
- package/dist/core/v1-bootstrap.js +9 -1
- package/dist/engine/classify-enforce-cli.d.ts +8 -0
- package/dist/engine/classify-enforce-cli.js +61 -0
- package/dist/engine/compound-cli.js +1 -0
- package/dist/engine/compound-export.js +8 -3
- package/dist/engine/enforce-classifier.d.ts +31 -0
- package/dist/engine/enforce-classifier.js +123 -0
- package/dist/engine/learn-cli.js +1 -4
- package/dist/engine/lifecycle/bypass-detector.d.ts +34 -0
- package/dist/engine/lifecycle/bypass-detector.js +82 -0
- package/dist/engine/lifecycle/lifecycle-cli.d.ts +7 -0
- package/dist/engine/lifecycle/lifecycle-cli.js +102 -0
- package/dist/engine/lifecycle/meta-cli.d.ts +4 -0
- package/dist/engine/lifecycle/meta-cli.js +7 -0
- package/dist/engine/lifecycle/meta-reclassifier.d.ts +78 -0
- package/dist/engine/lifecycle/meta-reclassifier.js +351 -0
- package/dist/engine/lifecycle/orchestrator.d.ts +32 -0
- package/dist/engine/lifecycle/orchestrator.js +131 -0
- package/dist/engine/lifecycle/signals.d.ts +30 -0
- package/dist/engine/lifecycle/signals.js +142 -0
- package/dist/engine/lifecycle/trigger-t1-correction.d.ts +23 -0
- package/dist/engine/lifecycle/trigger-t1-correction.js +78 -0
- package/dist/engine/lifecycle/trigger-t2-violation.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t2-violation.js +42 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.d.ts +17 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.js +39 -0
- package/dist/engine/lifecycle/trigger-t4-decay.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t4-decay.js +40 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.d.ts +16 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.js +78 -0
- package/dist/engine/lifecycle/types.d.ts +52 -0
- package/dist/engine/lifecycle/types.js +7 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
- package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
- package/dist/engine/rule-toggle-cli.d.ts +13 -0
- package/dist/engine/rule-toggle-cli.js +76 -0
- package/dist/engine/skill-promoter.js +3 -6
- package/dist/forge/evidence-processor.js +10 -2
- package/dist/hooks/context-guard.js +72 -1
- package/dist/hooks/dangerous-patterns.json +3 -3
- package/dist/hooks/db-guard.js +18 -2
- package/dist/hooks/intent-classifier.js +1 -1
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/notepad-injector.js +1 -1
- package/dist/hooks/permission-handler.js +1 -1
- package/dist/hooks/post-tool-failure.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +6 -0
- package/dist/hooks/post-tool-use.js +94 -14
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.d.ts +7 -0
- package/dist/hooks/pre-tool-use.js +79 -5
- package/dist/hooks/rate-limiter.js +1 -1
- package/dist/hooks/secret-filter.d.ts +10 -0
- package/dist/hooks/secret-filter.js +21 -1
- package/dist/hooks/session-recovery.js +1 -1
- package/dist/hooks/shared/atomic-write.d.ts +8 -1
- package/dist/hooks/shared/atomic-write.js +17 -3
- package/dist/hooks/shared/command-parser.d.ts +44 -0
- package/dist/hooks/shared/command-parser.js +50 -0
- package/dist/hooks/shared/hook-response.d.ts +23 -2
- package/dist/hooks/shared/hook-response.js +48 -3
- package/dist/hooks/shared/safe-regex.d.ts +25 -0
- package/dist/hooks/shared/safe-regex.js +50 -0
- package/dist/hooks/shared/stop-triggers.d.ts +19 -0
- package/dist/hooks/shared/stop-triggers.js +19 -0
- package/dist/hooks/skill-injector.js +1 -1
- package/dist/hooks/slop-detector.js +2 -2
- package/dist/hooks/solution-injector.d.ts +9 -0
- package/dist/hooks/solution-injector.js +48 -5
- package/dist/hooks/stop-guard.d.ts +84 -0
- package/dist/hooks/stop-guard.js +606 -0
- package/dist/hooks/subagent-tracker.js +1 -1
- package/dist/i18n/index.js +3 -5
- package/dist/mcp/tools.js +19 -2
- package/dist/store/evidence-store.d.ts +15 -0
- package/dist/store/evidence-store.js +61 -1
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/rule-lifecycle.d.ts +23 -0
- package/dist/store/rule-lifecycle.js +63 -0
- package/dist/store/rule-store.d.ts +21 -0
- package/dist/store/rule-store.js +136 -8
- package/dist/store/types.d.ts +83 -0
- package/dist/store/types.js +7 -1
- package/hooks/hook-registry.json +1 -0
- package/hooks/hooks.json +6 -1
- package/package.json +11 -3
- package/plugin.json +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen init` CLI
|
|
3
|
+
*
|
|
4
|
+
* 빈 FORGEN_HOME (또는 기존에 starter 미설치 홈) 에 starter-pack 솔루션을
|
|
5
|
+
* 프로비저닝. npm install-g 시의 postinstall 이 하던 starter 배포 로직을 런타임
|
|
6
|
+
* CLI 로 노출해 다음 시나리오 지원:
|
|
7
|
+
* - `FORGEN_HOME=/tmp/fresh forgen init` — 격리 테스트 환경
|
|
8
|
+
* - CI pipeline 신규 컨테이너 프로비저닝
|
|
9
|
+
* - 사용자가 실수로 me/solutions 전부 삭제한 뒤 복구
|
|
10
|
+
*
|
|
11
|
+
* 보수적 정책: me/solutions 에 **≥5개 파일**이 이미 있으면 건너뜀 (사용자
|
|
12
|
+
* 실 축적물 보호). `--force` 플래그로 우회 가능. postinstall 의 installStarterPack
|
|
13
|
+
* 과 동일 규칙.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { ME_DIR } from './paths.js';
|
|
19
|
+
/** 패키지 루트의 starter-pack/solutions 디렉터리. */
|
|
20
|
+
function findStarterDir() {
|
|
21
|
+
// 런타임에 dist/core/init-cli.js — 패키지 루트는 상위 2단계
|
|
22
|
+
const distDir = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const pkgRoot = path.resolve(distDir, '..', '..');
|
|
24
|
+
const starterDir = path.join(pkgRoot, 'starter-pack', 'solutions');
|
|
25
|
+
return fs.existsSync(starterDir) ? starterDir : null;
|
|
26
|
+
}
|
|
27
|
+
export function initializeForgenHome(options = {}) {
|
|
28
|
+
const solutionsDir = path.join(ME_DIR, 'solutions');
|
|
29
|
+
const starterDir = findStarterDir();
|
|
30
|
+
if (!starterDir) {
|
|
31
|
+
return {
|
|
32
|
+
solutionsInstalled: 0,
|
|
33
|
+
solutionsSkippedExisting: 0,
|
|
34
|
+
solutionsDir,
|
|
35
|
+
starterDir: null,
|
|
36
|
+
skipped: true,
|
|
37
|
+
skipReason: 'starter-pack directory not found in package',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
let existing = 0;
|
|
41
|
+
if (fs.existsSync(solutionsDir)) {
|
|
42
|
+
existing = fs.readdirSync(solutionsDir).filter((f) => f.endsWith('.md')).length;
|
|
43
|
+
}
|
|
44
|
+
if (existing >= 5 && !options.force) {
|
|
45
|
+
return {
|
|
46
|
+
solutionsInstalled: 0,
|
|
47
|
+
solutionsSkippedExisting: existing,
|
|
48
|
+
solutionsDir,
|
|
49
|
+
starterDir,
|
|
50
|
+
skipped: true,
|
|
51
|
+
skipReason: `${existing} existing solutions (≥5) — use --force to overwrite`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
fs.mkdirSync(solutionsDir, { recursive: true });
|
|
55
|
+
const starterFiles = fs.readdirSync(starterDir).filter((f) => f.endsWith('.md'));
|
|
56
|
+
let installed = 0;
|
|
57
|
+
for (const file of starterFiles) {
|
|
58
|
+
const dest = path.join(solutionsDir, file);
|
|
59
|
+
if (!fs.existsSync(dest) || options.force) {
|
|
60
|
+
fs.cpSync(path.join(starterDir, file), dest);
|
|
61
|
+
installed++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
solutionsInstalled: installed,
|
|
66
|
+
solutionsSkippedExisting: existing,
|
|
67
|
+
solutionsDir,
|
|
68
|
+
starterDir,
|
|
69
|
+
skipped: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export async function handleInit(args) {
|
|
73
|
+
const force = args.includes('--force');
|
|
74
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
75
|
+
console.log(`
|
|
76
|
+
forgen init — starter-pack 프로비저닝 (기존 솔루션 보호)
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
forgen init Install starter-pack if solutions/ has < 5 files
|
|
80
|
+
forgen init --force Overwrite any existing starter files (idempotent)
|
|
81
|
+
FORGEN_HOME=... forgen init 새 홈에 격리 초기화
|
|
82
|
+
|
|
83
|
+
Starter pack = starter-* 로 시작하는 범용 개발 패턴 솔루션. 신규 사용자가
|
|
84
|
+
"compound recall" 효과를 첫날부터 체감할 수 있도록 설치 시 기본 제공되지만,
|
|
85
|
+
npm install-g 을 거치지 않은 격리/컨테이너 환경은 이 CLI 로 수동 배포.
|
|
86
|
+
`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const result = initializeForgenHome({ force });
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(' forgen init');
|
|
92
|
+
console.log(' ──────────');
|
|
93
|
+
console.log(` FORGEN_HOME ${path.dirname(result.solutionsDir)}`);
|
|
94
|
+
console.log(` starter-pack source ${result.starterDir ?? 'NOT FOUND'}`);
|
|
95
|
+
console.log(` existing solutions ${result.solutionsSkippedExisting}`);
|
|
96
|
+
console.log(` newly installed ${result.solutionsInstalled}`);
|
|
97
|
+
if (result.skipped) {
|
|
98
|
+
console.log(` status skipped — ${result.skipReason}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(` status ✓ initialized`);
|
|
102
|
+
}
|
|
103
|
+
console.log('');
|
|
104
|
+
}
|
package/dist/core/init.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { profileExists } from '../store/profile-store.js';
|
|
10
10
|
import { ensureV1Directories } from './v1-bootstrap.js';
|
|
11
|
+
import { initializeForgenHome } from './init-cli.js';
|
|
11
12
|
// ── CLI 핸들러 ──
|
|
12
13
|
export async function handleInit(_args) {
|
|
13
14
|
const cwd = process.cwd();
|
|
@@ -18,6 +19,22 @@ export async function handleInit(_args) {
|
|
|
18
19
|
// 프로젝트 .claude/rules 디렉토리 생성
|
|
19
20
|
const rulesDir = path.join(cwd, '.claude', 'rules');
|
|
20
21
|
fs.mkdirSync(rulesDir, { recursive: true });
|
|
22
|
+
// v0.4.1 (2026-04-24): starter-pack 프로비저닝 — 격리 홈 / 신규 FORGEN_HOME
|
|
23
|
+
// 에서 "신규 사용자 첫날 가치" 가 0이 되는 결함 해소. npm install-g 시의
|
|
24
|
+
// postinstall 이 하던 starter 배포를 런타임에서도 보장.
|
|
25
|
+
// 보수적: me/solutions 에 ≥5개면 skip — 기존 사용자 실 축적물 보호.
|
|
26
|
+
try {
|
|
27
|
+
const r = initializeForgenHome();
|
|
28
|
+
if (r.solutionsInstalled > 0) {
|
|
29
|
+
console.log(` ✓ Starter-pack: ${r.solutionsInstalled} solutions installed.`);
|
|
30
|
+
}
|
|
31
|
+
else if (r.skipped && r.solutionsSkippedExisting > 0) {
|
|
32
|
+
console.log(` • Starter-pack: skipped (${r.solutionsSkippedExisting} existing solutions).`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.log(` ⚠ Starter-pack install 실패: ${e.message}`);
|
|
37
|
+
}
|
|
21
38
|
// 프로필 존재 확인
|
|
22
39
|
if (profileExists()) {
|
|
23
40
|
console.log(' Profile already exists. Your personalization is active.');
|
package/dist/core/inspect-cli.js
CHANGED
|
@@ -78,7 +78,8 @@ export async function handleInspect(args) {
|
|
|
78
78
|
console.log('\n' + inspect.renderRules(rules) + '\n');
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
// R9-IA2: user-facing name is "corrections"; "evidence" kept as back-compat alias.
|
|
82
|
+
if (sub === 'corrections' || sub === 'evidence') {
|
|
82
83
|
const evidence = loadRecentEvidence(20);
|
|
83
84
|
console.log('\n' + inspect.renderEvidence(evidence) + '\n');
|
|
84
85
|
return;
|
|
@@ -92,9 +93,67 @@ export async function handleInspect(args) {
|
|
|
92
93
|
console.log('\n' + inspect.renderSession(sessions[0]) + '\n');
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
96
|
+
// R5-G1: 2AM 디버깅용 jsonl tail — violations/bypass/drift
|
|
97
|
+
if (sub === 'violations' || sub === 'bypass' || sub === 'drift') {
|
|
98
|
+
const limit = Number(args[args.indexOf('--last') + 1]) || 20;
|
|
99
|
+
const fileMap = {
|
|
100
|
+
violations: 'violations.jsonl',
|
|
101
|
+
bypass: 'bypass.jsonl',
|
|
102
|
+
drift: 'drift.jsonl',
|
|
103
|
+
};
|
|
104
|
+
const p = path.join(STATE_DIR, 'enforcement', fileMap[sub]);
|
|
105
|
+
if (!fs.existsSync(p)) {
|
|
106
|
+
console.log(`\n No ${sub} data (${p} not found).\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const lines = fs.readFileSync(p, 'utf-8').trim().split('\n').filter(Boolean);
|
|
110
|
+
const tail = lines.slice(-limit);
|
|
111
|
+
console.log(`\n ${sub} (last ${tail.length} of ${lines.length}):`);
|
|
112
|
+
// rule_id별 집계
|
|
113
|
+
const byRule = new Map();
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
try {
|
|
116
|
+
const entry = JSON.parse(line);
|
|
117
|
+
const rid = entry.rule_id ?? 'unknown';
|
|
118
|
+
byRule.set(rid, (byRule.get(rid) ?? 0) + 1);
|
|
119
|
+
}
|
|
120
|
+
catch { /* skip malformed */ }
|
|
121
|
+
}
|
|
122
|
+
if (byRule.size > 0) {
|
|
123
|
+
console.log(' Aggregate (rule_id → count):');
|
|
124
|
+
for (const [rid, count] of [...byRule.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10)) {
|
|
125
|
+
console.log(` ${rid.slice(0, 24).padEnd(24)} ${count}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// R7-U3: rule_id 전체 표시 + kind + source 분리 + resolve hint footer.
|
|
129
|
+
console.log('\n Recent (time — rule_id — kind@source — preview):');
|
|
130
|
+
for (const line of tail) {
|
|
131
|
+
try {
|
|
132
|
+
const e = JSON.parse(line);
|
|
133
|
+
const when = (e.at ?? '').slice(0, 19);
|
|
134
|
+
const rid = (e.rule_id ?? '-').slice(0, 24); // 8자→24자 (prefix match 가능 길이)
|
|
135
|
+
const kind = (e.kind ?? '-');
|
|
136
|
+
const source = (e.source ?? '-');
|
|
137
|
+
const preview = (e.message_preview ?? e.reason_preview ?? e.pattern_preview ?? '').slice(0, 60);
|
|
138
|
+
console.log(` ${when} ${rid.padEnd(24)} ${String(kind).padEnd(10)}@${String(source).padEnd(14)} ${preview}`);
|
|
139
|
+
}
|
|
140
|
+
catch { /* skip */ }
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
// R7-U3 footer: resolve hint
|
|
144
|
+
console.log(' Resolve:');
|
|
145
|
+
console.log(' Disable a rule: forgen suppress-rule <rule_id>');
|
|
146
|
+
console.log(' Re-enable: forgen activate-rule <rule_id>');
|
|
147
|
+
console.log(' Temp bypass turn: set FORGEN_USER_CONFIRMED=1 (audited)');
|
|
148
|
+
console.log('');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
95
151
|
console.log(` Usage:
|
|
96
|
-
forgen inspect profile
|
|
97
|
-
forgen inspect rules
|
|
98
|
-
forgen inspect
|
|
99
|
-
forgen inspect session
|
|
152
|
+
forgen inspect profile — 현재 profile 상태
|
|
153
|
+
forgen inspect rules — active/suppressed 규칙 목록
|
|
154
|
+
forgen inspect corrections — 최근 corrections / behavior 기록 (alias: evidence)
|
|
155
|
+
forgen inspect session — 현재/최근 세션 상태
|
|
156
|
+
forgen inspect violations [--last N] — 최근 block 기록
|
|
157
|
+
forgen inspect bypass [--last N] — 사용자 우회 기록
|
|
158
|
+
forgen inspect drift [--last N] — stuck-loop force-approve 기록`);
|
|
100
159
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen migrate` CLI
|
|
3
|
+
*
|
|
4
|
+
* 데이터 스키마 업그레이드를 1회성으로 돌리는 관리 명령.
|
|
5
|
+
* 현재 대상:
|
|
6
|
+
* - implicit-feedback: TEST-5 category 필드 백필 (type → category inference).
|
|
7
|
+
* 기본은 lazy (read 시점 백필) 이지만 집계/외부 도구가 raw jsonl 을 읽는
|
|
8
|
+
* 경우 영구 재기록이 필요.
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleMigrate(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen migrate` CLI
|
|
3
|
+
*
|
|
4
|
+
* 데이터 스키마 업그레이드를 1회성으로 돌리는 관리 명령.
|
|
5
|
+
* 현재 대상:
|
|
6
|
+
* - implicit-feedback: TEST-5 category 필드 백필 (type → category inference).
|
|
7
|
+
* 기본은 lazy (read 시점 백필) 이지만 집계/외부 도구가 raw jsonl 을 읽는
|
|
8
|
+
* 경우 영구 재기록이 필요.
|
|
9
|
+
*/
|
|
10
|
+
import { migrateImplicitFeedbackLog } from '../store/implicit-feedback-store.js';
|
|
11
|
+
const HELP = `
|
|
12
|
+
forgen migrate — one-shot schema migrations
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
forgen migrate implicit-feedback category 필드가 없는 레거시 엔트리 백필 + 재기록
|
|
16
|
+
forgen migrate all (현재는 implicit-feedback 과 동일)
|
|
17
|
+
forgen migrate --help 이 도움말
|
|
18
|
+
`;
|
|
19
|
+
export async function handleMigrate(args) {
|
|
20
|
+
const sub = args[0];
|
|
21
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
22
|
+
console.log(HELP);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (sub === 'implicit-feedback' || sub === 'all') {
|
|
26
|
+
console.log('[forgen migrate] implicit-feedback.jsonl 백필 시작...');
|
|
27
|
+
const { migrated, dropped } = migrateImplicitFeedbackLog();
|
|
28
|
+
console.log(`[forgen migrate] 백필 ${migrated}건, 드롭 ${dropped}건 — 재기록 완료.`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.error(`[forgen migrate] unknown target: ${sub}`);
|
|
32
|
+
console.error(HELP);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
export declare const CLAUDE_DIR: string;
|
|
3
3
|
/** ~/.claude/settings.json — Claude Code 설정 파일 */
|
|
4
4
|
export declare const SETTINGS_PATH: string;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* ~/.forgen/ — v1 하네스 홈 디렉토리.
|
|
7
|
+
*
|
|
8
|
+
* v0.4.1 (2026-04-24): FORGEN_HOME env 로 override 가능.
|
|
9
|
+
* 목적: CI/e2e 에서 격리된 fresh forgen 홈으로 "신규 사용자 시뮬" + 내 실 홈
|
|
10
|
+
* (2338+ 세션 축적물) 을 건드리지 않음. README/docs 에도 "테스트 격리" 섹션
|
|
11
|
+
* 으로 노출 예정.
|
|
12
|
+
*/
|
|
6
13
|
export declare const FORGEN_HOME: string;
|
|
7
14
|
/** ~/.forgen/me/ — 개인 공간 (v5.1: ~/.compound/ → ~/.forgen/ 통합) */
|
|
8
15
|
export declare const ME_DIR: string;
|
package/dist/core/paths.js
CHANGED
|
@@ -5,8 +5,17 @@ const HOME = os.homedir();
|
|
|
5
5
|
export const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
6
6
|
/** ~/.claude/settings.json — Claude Code 설정 파일 */
|
|
7
7
|
export const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* ~/.forgen/ — v1 하네스 홈 디렉토리.
|
|
10
|
+
*
|
|
11
|
+
* v0.4.1 (2026-04-24): FORGEN_HOME env 로 override 가능.
|
|
12
|
+
* 목적: CI/e2e 에서 격리된 fresh forgen 홈으로 "신규 사용자 시뮬" + 내 실 홈
|
|
13
|
+
* (2338+ 세션 축적물) 을 건드리지 않음. README/docs 에도 "테스트 격리" 섹션
|
|
14
|
+
* 으로 노출 예정.
|
|
15
|
+
*/
|
|
16
|
+
export const FORGEN_HOME = process.env.FORGEN_HOME
|
|
17
|
+
? path.resolve(process.env.FORGEN_HOME)
|
|
18
|
+
: path.join(HOME, '.forgen');
|
|
10
19
|
/** ~/.forgen/me/ — 개인 공간 (v5.1: ~/.compound/ → ~/.forgen/ 통합) */
|
|
11
20
|
export const ME_DIR = path.join(FORGEN_HOME, 'me');
|
|
12
21
|
/** ~/.forgen/me/philosophy.json — 개인 철학 */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen recall` CLI (H5)
|
|
3
|
+
*
|
|
4
|
+
* 최근 UserPromptSubmit 에서 매칭/surface 된 솔루션을 사용자에게 되짚어주는 명령.
|
|
5
|
+
*
|
|
6
|
+
* 목적: v0.4.0 에서 compound 솔루션이 8,000+ 번 recall 되었지만 사용자는 0건을
|
|
7
|
+
* 확인할 수 없었다. 이 CLI 는 `~/.forgen/state/implicit-feedback.jsonl` 과
|
|
8
|
+
* `~/.forgen/state/match-eval-log.jsonl` 을 읽어 "최근 어떤 지식이 붙었나" 를
|
|
9
|
+
* 1초 안에 보여준다. `--show` 플래그로 솔루션 본문 preview 까지.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* forgen recall 최근 10건 요약
|
|
13
|
+
* forgen recall --limit 20 최근 N건
|
|
14
|
+
* forgen recall --show 본문 preview 포함
|
|
15
|
+
* forgen recall --json JSON 출력 (script 연동용)
|
|
16
|
+
*/
|
|
17
|
+
interface RecallEntry {
|
|
18
|
+
at: string;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
solution: string;
|
|
21
|
+
match_score?: number;
|
|
22
|
+
}
|
|
23
|
+
/** H5: implicit-feedback.jsonl 에서 recommendation_surfaced 만 시간역순으로 추출. */
|
|
24
|
+
export declare function loadRecentRecalls(limit?: number): RecallEntry[];
|
|
25
|
+
export declare function handleRecall(args: string[]): Promise<void>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — `forgen recall` CLI (H5)
|
|
3
|
+
*
|
|
4
|
+
* 최근 UserPromptSubmit 에서 매칭/surface 된 솔루션을 사용자에게 되짚어주는 명령.
|
|
5
|
+
*
|
|
6
|
+
* 목적: v0.4.0 에서 compound 솔루션이 8,000+ 번 recall 되었지만 사용자는 0건을
|
|
7
|
+
* 확인할 수 없었다. 이 CLI 는 `~/.forgen/state/implicit-feedback.jsonl` 과
|
|
8
|
+
* `~/.forgen/state/match-eval-log.jsonl` 을 읽어 "최근 어떤 지식이 붙었나" 를
|
|
9
|
+
* 1초 안에 보여준다. `--show` 플래그로 솔루션 본문 preview 까지.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* forgen recall 최근 10건 요약
|
|
13
|
+
* forgen recall --limit 20 최근 N건
|
|
14
|
+
* forgen recall --show 본문 preview 포함
|
|
15
|
+
* forgen recall --json JSON 출력 (script 연동용)
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { STATE_DIR, ME_SOLUTIONS } from './paths.js';
|
|
20
|
+
function readJsonl(p) {
|
|
21
|
+
if (!fs.existsSync(p))
|
|
22
|
+
return [];
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
|
|
25
|
+
if (!line.trim())
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
out.push(JSON.parse(line));
|
|
29
|
+
}
|
|
30
|
+
catch { /* skip malformed */ }
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** H5: implicit-feedback.jsonl 에서 recommendation_surfaced 만 시간역순으로 추출. */
|
|
35
|
+
export function loadRecentRecalls(limit = 10) {
|
|
36
|
+
const entries = readJsonl(path.join(STATE_DIR, 'implicit-feedback.jsonl'));
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (e.type !== 'recommendation_surfaced')
|
|
40
|
+
continue;
|
|
41
|
+
if (typeof e.at !== 'string' || typeof e.solution !== 'string')
|
|
42
|
+
continue;
|
|
43
|
+
out.push({
|
|
44
|
+
at: e.at,
|
|
45
|
+
sessionId: typeof e.sessionId === 'string' ? e.sessionId : 'unknown',
|
|
46
|
+
solution: e.solution,
|
|
47
|
+
match_score: typeof e.match_score === 'number' ? e.match_score : undefined,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return out.sort((a, b) => b.at.localeCompare(a.at)).slice(0, limit);
|
|
51
|
+
}
|
|
52
|
+
/** 솔루션 body preview — frontmatter 뒤 첫 N줄. */
|
|
53
|
+
function readSolutionPreview(solutionName, maxLines = 8) {
|
|
54
|
+
const candidates = [
|
|
55
|
+
path.join(ME_SOLUTIONS, `${solutionName}.md`),
|
|
56
|
+
path.join(ME_SOLUTIONS, solutionName),
|
|
57
|
+
];
|
|
58
|
+
for (const p of candidates) {
|
|
59
|
+
if (!fs.existsSync(p))
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
63
|
+
// frontmatter block skip (--- ... ---)
|
|
64
|
+
const stripped = raw.replace(/^---[\s\S]*?---\n?/, '');
|
|
65
|
+
const lines = stripped.split('\n').filter((l) => l.length > 0).slice(0, maxLines);
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function parseArgs(args) {
|
|
75
|
+
let limit = 10;
|
|
76
|
+
let showBody = false;
|
|
77
|
+
let json = false;
|
|
78
|
+
for (let i = 0; i < args.length; i++) {
|
|
79
|
+
const a = args[i];
|
|
80
|
+
if (a === '--limit' && i + 1 < args.length) {
|
|
81
|
+
const n = Number(args[++i]);
|
|
82
|
+
if (Number.isFinite(n) && n > 0)
|
|
83
|
+
limit = Math.min(100, Math.floor(n));
|
|
84
|
+
}
|
|
85
|
+
else if (a === '--show' || a === '--body') {
|
|
86
|
+
showBody = true;
|
|
87
|
+
}
|
|
88
|
+
else if (a === '--json') {
|
|
89
|
+
json = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { limit, showBody, json };
|
|
93
|
+
}
|
|
94
|
+
export async function handleRecall(args) {
|
|
95
|
+
const { limit, showBody, json } = parseArgs(args);
|
|
96
|
+
const recalls = loadRecentRecalls(limit);
|
|
97
|
+
if (json) {
|
|
98
|
+
const payload = recalls.map((r) => ({
|
|
99
|
+
...r,
|
|
100
|
+
preview: showBody ? readSolutionPreview(r.solution) : undefined,
|
|
101
|
+
}));
|
|
102
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (recalls.length === 0) {
|
|
106
|
+
console.log(' (no recent recalls — run a session with compound hooks enabled)');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(` forgen recall — last ${recalls.length} surfaced solution${recalls.length === 1 ? '' : 's'}`);
|
|
111
|
+
console.log(' ─────────────────────────────────────────');
|
|
112
|
+
for (const r of recalls) {
|
|
113
|
+
const score = r.match_score !== undefined ? ` @${r.match_score.toFixed(2)}` : '';
|
|
114
|
+
console.log(` ${r.at.slice(0, 19).replace('T', ' ')} ${r.solution}${score}`);
|
|
115
|
+
if (showBody) {
|
|
116
|
+
const body = readSolutionPreview(r.solution);
|
|
117
|
+
if (body) {
|
|
118
|
+
for (const line of body.split('\n'))
|
|
119
|
+
console.log(` │ ${line}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — Recall Reference Detector (H4 완결)
|
|
3
|
+
*
|
|
4
|
+
* US-06 에서 `recommendation_surfaced` (주입 = Claude 컨텍스트에 보여졌다) 는
|
|
5
|
+
* emit 경로가 있지만, `recall_referenced` (Claude 가 실제로 참조/인용했다) 는
|
|
6
|
+
* enum 만 정의되고 emit 경로가 없었다. 이 결함 때문에 "채널은 뚫렸지만 활용은
|
|
7
|
+
* 측정 불가" 상태. 이 모듈이 그 측정 경로를 닫는다.
|
|
8
|
+
*
|
|
9
|
+
* 알고리즘:
|
|
10
|
+
* 1. Stop hook 에서 lastAssistantMessage 를 읽는다.
|
|
11
|
+
* 2. 현재 세션의 injection-cache 에서 최근 주입된 솔루션 목록을 가져온다.
|
|
12
|
+
* 3. 각 솔루션의 **name** 이 메시지 텍스트에 등장하면 참조한 것으로 간주.
|
|
13
|
+
* (tag 매칭은 false-positive 과다 — "협업" 같은 흔한 단어로 오매칭.)
|
|
14
|
+
* 4. 중복 emit 방지: injection-cache 엔트리에 `_referenced: true` 플래그 세팅.
|
|
15
|
+
*
|
|
16
|
+
* 순수 함수 설계 — Stop hook 이 inject-cache 를 읽고 쓰는 I/O 는 호출지에서.
|
|
17
|
+
*/
|
|
18
|
+
export interface InjectedSolutionEntry {
|
|
19
|
+
name: string;
|
|
20
|
+
identifiers?: string[];
|
|
21
|
+
tags?: string[];
|
|
22
|
+
status?: string;
|
|
23
|
+
injectedAt?: string;
|
|
24
|
+
_referenced?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface RecallReferenceDetection {
|
|
27
|
+
/** 이번 턴에 처음 참조가 감지된 솔루션 이름 목록. */
|
|
28
|
+
newlyReferenced: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 순수 판정 — text 안에 아직 참조 안 된 솔루션의 name / 고유 식별자 / 희귀 태그
|
|
32
|
+
* 조합이 등장하면 수집.
|
|
33
|
+
*
|
|
34
|
+
* v0.4.1 초기: name (slug kebab-case) literal 만 매칭 → Claude 가 content 만 인용
|
|
35
|
+
* 하고 slug 를 안 쓰면 측정 불가.
|
|
36
|
+
* v0.4.1 확장 (2026-04-24): identifier (함수/파일명 literal, >=4자) 또는 **복합
|
|
37
|
+
* 태그 2개 동시 등장** 도 weak reference 로 인정. false-positive 완화 위해:
|
|
38
|
+
* - identifier 는 길이 ≥4
|
|
39
|
+
* - tag 는 **복합 슬러그 (`-` 또는 `_` 포함)** 만 허용 + length ≥6
|
|
40
|
+
* → "tdd", "test", "workflow" 같은 일반 태그 단독 매칭은 제외
|
|
41
|
+
* - tag 매칭은 최소 2개 교차
|
|
42
|
+
*/
|
|
43
|
+
export declare function detectRecallReferences(text: string, injected: readonly InjectedSolutionEntry[]): RecallReferenceDetection;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — Recall Reference Detector (H4 완결)
|
|
3
|
+
*
|
|
4
|
+
* US-06 에서 `recommendation_surfaced` (주입 = Claude 컨텍스트에 보여졌다) 는
|
|
5
|
+
* emit 경로가 있지만, `recall_referenced` (Claude 가 실제로 참조/인용했다) 는
|
|
6
|
+
* enum 만 정의되고 emit 경로가 없었다. 이 결함 때문에 "채널은 뚫렸지만 활용은
|
|
7
|
+
* 측정 불가" 상태. 이 모듈이 그 측정 경로를 닫는다.
|
|
8
|
+
*
|
|
9
|
+
* 알고리즘:
|
|
10
|
+
* 1. Stop hook 에서 lastAssistantMessage 를 읽는다.
|
|
11
|
+
* 2. 현재 세션의 injection-cache 에서 최근 주입된 솔루션 목록을 가져온다.
|
|
12
|
+
* 3. 각 솔루션의 **name** 이 메시지 텍스트에 등장하면 참조한 것으로 간주.
|
|
13
|
+
* (tag 매칭은 false-positive 과다 — "협업" 같은 흔한 단어로 오매칭.)
|
|
14
|
+
* 4. 중복 emit 방지: injection-cache 엔트리에 `_referenced: true` 플래그 세팅.
|
|
15
|
+
*
|
|
16
|
+
* 순수 함수 설계 — Stop hook 이 inject-cache 를 읽고 쓰는 I/O 는 호출지에서.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* 순수 판정 — text 안에 아직 참조 안 된 솔루션의 name / 고유 식별자 / 희귀 태그
|
|
20
|
+
* 조합이 등장하면 수집.
|
|
21
|
+
*
|
|
22
|
+
* v0.4.1 초기: name (slug kebab-case) literal 만 매칭 → Claude 가 content 만 인용
|
|
23
|
+
* 하고 slug 를 안 쓰면 측정 불가.
|
|
24
|
+
* v0.4.1 확장 (2026-04-24): identifier (함수/파일명 literal, >=4자) 또는 **복합
|
|
25
|
+
* 태그 2개 동시 등장** 도 weak reference 로 인정. false-positive 완화 위해:
|
|
26
|
+
* - identifier 는 길이 ≥4
|
|
27
|
+
* - tag 는 **복합 슬러그 (`-` 또는 `_` 포함)** 만 허용 + length ≥6
|
|
28
|
+
* → "tdd", "test", "workflow" 같은 일반 태그 단독 매칭은 제외
|
|
29
|
+
* - tag 매칭은 최소 2개 교차
|
|
30
|
+
*/
|
|
31
|
+
export function detectRecallReferences(text, injected) {
|
|
32
|
+
if (!text || injected.length === 0)
|
|
33
|
+
return { newlyReferenced: [] };
|
|
34
|
+
const newlyReferenced = [];
|
|
35
|
+
for (const sol of injected) {
|
|
36
|
+
if (sol._referenced)
|
|
37
|
+
continue;
|
|
38
|
+
if (!sol.name || sol.name.length < 4)
|
|
39
|
+
continue;
|
|
40
|
+
let matched = false;
|
|
41
|
+
// 1순위: slug name 정확 매칭 (precision 최고)
|
|
42
|
+
if (text.includes(sol.name)) {
|
|
43
|
+
matched = true;
|
|
44
|
+
}
|
|
45
|
+
// 2순위: 고유 identifier (함수/파일명 literal) 매칭
|
|
46
|
+
if (!matched && sol.identifiers) {
|
|
47
|
+
for (const id of sol.identifiers) {
|
|
48
|
+
if (typeof id === 'string' && id.length >= 4 && text.includes(id)) {
|
|
49
|
+
matched = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 3순위: 복합-슬러그 태그 2개 이상 동시 등장 (일반 단어 단독은 제외)
|
|
55
|
+
if (!matched && sol.tags) {
|
|
56
|
+
const specificTags = sol.tags.filter((t) => typeof t === 'string' && t.length >= 6 && (t.includes('-') || t.includes('_')));
|
|
57
|
+
const hits = specificTags.filter((t) => text.includes(t));
|
|
58
|
+
if (hits.length >= 2)
|
|
59
|
+
matched = true;
|
|
60
|
+
}
|
|
61
|
+
if (matched)
|
|
62
|
+
newlyReferenced.push(sol.name);
|
|
63
|
+
}
|
|
64
|
+
return { newlyReferenced };
|
|
65
|
+
}
|
package/dist/core/state-gc.d.ts
CHANGED
|
@@ -23,6 +23,25 @@ export interface PruneOptions {
|
|
|
23
23
|
* deletion via `dryRun: false`.
|
|
24
24
|
*/
|
|
25
25
|
export declare function pruneState(opts?: PruneOptions): PruneReport;
|
|
26
|
+
/**
|
|
27
|
+
* ADR-002 T4 — daily rule decay scanner.
|
|
28
|
+
*
|
|
29
|
+
* `~/.forgen/me/rules` 전체를 훑어 `last_inject_at < now - decay_days` 인 active rule 을
|
|
30
|
+
* retire phase 로 전이시킨다. 실제 파일 삭제가 아니라 status='removed' + phase='retired'.
|
|
31
|
+
*
|
|
32
|
+
* 호출 지점: `forgen doctor --prune-state` 또는 `forgen lifecycle-scan --apply` 그리고
|
|
33
|
+
* 별도 cron/CI scheduler 에서도 호출 가능. dryRun=true 기본.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runDailyT4Decay(opts?: {
|
|
36
|
+
decayDays?: number;
|
|
37
|
+
dryRun?: boolean;
|
|
38
|
+
now?: number;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
scanned: number;
|
|
41
|
+
retired: number;
|
|
42
|
+
sample: string[];
|
|
43
|
+
dryRun: boolean;
|
|
44
|
+
}>;
|
|
26
45
|
/**
|
|
27
46
|
* Count session-scoped files in STATE_DIR without deleting. Used by doctor
|
|
28
47
|
* to surface a warning when the directory is bloated.
|
package/dist/core/state-gc.js
CHANGED
|
@@ -94,15 +94,59 @@ export function pruneState(opts = {}) {
|
|
|
94
94
|
// outcomes/*.jsonl: one file per session, session-scoped by design.
|
|
95
95
|
// These compound over time exactly like state session files.
|
|
96
96
|
const outcomes = pruneDir(outcomesDir, cutoff, dryRun, (n) => n.endsWith('.jsonl'));
|
|
97
|
+
// ADR-002 block-count directory — session-scoped per rule. F-M block-count GC.
|
|
98
|
+
const blockCountDir = path.join(stateDir, 'enforcement', 'block-count');
|
|
99
|
+
const blockCounters = pruneDir(blockCountDir, cutoff, dryRun, (n) => n.endsWith('.json'));
|
|
97
100
|
return {
|
|
98
|
-
scanned: state.scanned + outcomes.scanned,
|
|
99
|
-
pruned: state.pruned + outcomes.pruned,
|
|
100
|
-
bytesFreed: state.bytes + outcomes.bytes,
|
|
101
|
+
scanned: state.scanned + outcomes.scanned + blockCounters.scanned,
|
|
102
|
+
pruned: state.pruned + outcomes.pruned + blockCounters.pruned,
|
|
103
|
+
bytesFreed: state.bytes + outcomes.bytes + blockCounters.bytes,
|
|
101
104
|
retentionDays: Math.round(retentionMs / (24 * 60 * 60 * 1000)),
|
|
102
105
|
dryRun,
|
|
103
|
-
sample: [...state.sample, ...outcomes.sample].slice(0, 20),
|
|
106
|
+
sample: [...state.sample, ...outcomes.sample, ...blockCounters.sample].slice(0, 20),
|
|
104
107
|
};
|
|
105
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* ADR-002 T4 — daily rule decay scanner.
|
|
111
|
+
*
|
|
112
|
+
* `~/.forgen/me/rules` 전체를 훑어 `last_inject_at < now - decay_days` 인 active rule 을
|
|
113
|
+
* retire phase 로 전이시킨다. 실제 파일 삭제가 아니라 status='removed' + phase='retired'.
|
|
114
|
+
*
|
|
115
|
+
* 호출 지점: `forgen doctor --prune-state` 또는 `forgen lifecycle-scan --apply` 그리고
|
|
116
|
+
* 별도 cron/CI scheduler 에서도 호출 가능. dryRun=true 기본.
|
|
117
|
+
*/
|
|
118
|
+
export async function runDailyT4Decay(opts = {}) {
|
|
119
|
+
const decayDays = opts.decayDays ?? 90;
|
|
120
|
+
const dryRun = opts.dryRun ?? true;
|
|
121
|
+
const now = opts.now ?? Date.now();
|
|
122
|
+
try {
|
|
123
|
+
const [{ loadAllRules, saveRule }, { detect: detectT4 }, { collectAllSignals }, { appendLifecycleEvents }, { foldEvents }] = await Promise.all([
|
|
124
|
+
import('../store/rule-store.js'),
|
|
125
|
+
import('../engine/lifecycle/trigger-t4-decay.js'),
|
|
126
|
+
import('../engine/lifecycle/signals.js'),
|
|
127
|
+
import('../engine/lifecycle/meta-reclassifier.js'),
|
|
128
|
+
import('../engine/lifecycle/orchestrator.js'),
|
|
129
|
+
]);
|
|
130
|
+
const rules = loadAllRules();
|
|
131
|
+
const signals = collectAllSignals(rules, { now });
|
|
132
|
+
const events = detectT4({ rules, signals, decay_days: decayDays, ts: now });
|
|
133
|
+
const report = { scanned: rules.length, retired: events.length, sample: events.map((e) => e.rule_id.slice(0, 8)), dryRun };
|
|
134
|
+
if (!dryRun && events.length > 0) {
|
|
135
|
+
const folded = foldEvents(rules, events, now);
|
|
136
|
+
for (const [id, updated] of folded.entries()) {
|
|
137
|
+
const original = rules.find((r) => r.rule_id === id);
|
|
138
|
+
if (!original || updated === original)
|
|
139
|
+
continue;
|
|
140
|
+
saveRule(updated);
|
|
141
|
+
}
|
|
142
|
+
appendLifecycleEvents(events, now);
|
|
143
|
+
}
|
|
144
|
+
return report;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { scanned: 0, retired: 0, sample: [], dryRun };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
106
150
|
/**
|
|
107
151
|
* Count session-scoped files in STATE_DIR without deleting. Used by doctor
|
|
108
152
|
* to surface a warning when the directory is bloated.
|