@wooojin/forgen 0.4.0 → 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.
Files changed (76) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +30 -0
  3. package/README.ja.md +58 -1
  4. package/README.ko.md +58 -1
  5. package/README.md +83 -15
  6. package/README.zh.md +26 -0
  7. package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
  8. package/dist/checks/conclusion-verification-ratio.js +86 -0
  9. package/dist/checks/fact-vs-agreement.d.ts +47 -0
  10. package/dist/checks/fact-vs-agreement.js +92 -0
  11. package/dist/checks/self-score-deflation.d.ts +38 -0
  12. package/dist/checks/self-score-deflation.js +108 -0
  13. package/dist/cli.js +22 -2
  14. package/dist/core/auto-compound-runner.js +75 -11
  15. package/dist/core/dashboard.js +9 -2
  16. package/dist/core/doctor.js +26 -5
  17. package/dist/core/extraction-notice.d.ts +18 -0
  18. package/dist/core/extraction-notice.js +64 -0
  19. package/dist/core/init-cli.d.ts +26 -0
  20. package/dist/core/init-cli.js +104 -0
  21. package/dist/core/init.js +17 -0
  22. package/dist/core/inspect-cli.js +1 -2
  23. package/dist/core/migrate-cli.d.ts +10 -0
  24. package/dist/core/migrate-cli.js +34 -0
  25. package/dist/core/paths.d.ts +8 -1
  26. package/dist/core/paths.js +11 -2
  27. package/dist/core/recall-cli.d.ts +26 -0
  28. package/dist/core/recall-cli.js +125 -0
  29. package/dist/core/recall-reference-detector.d.ts +43 -0
  30. package/dist/core/recall-reference-detector.js +65 -0
  31. package/dist/core/stats-cli.d.ts +21 -0
  32. package/dist/core/stats-cli.js +121 -10
  33. package/dist/core/uninstall.js +2 -1
  34. package/dist/engine/compound-cli.js +1 -0
  35. package/dist/engine/compound-export.js +8 -3
  36. package/dist/engine/learn-cli.js +1 -4
  37. package/dist/engine/lifecycle/lifecycle-cli.js +4 -4
  38. package/dist/engine/lifecycle/meta-reclassifier.js +3 -3
  39. package/dist/engine/lifecycle/orchestrator.js +2 -2
  40. package/dist/engine/lifecycle/signals.js +6 -6
  41. package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
  42. package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
  43. package/dist/engine/skill-promoter.js +3 -6
  44. package/dist/hooks/context-guard.js +1 -1
  45. package/dist/hooks/dangerous-patterns.json +3 -3
  46. package/dist/hooks/db-guard.js +18 -2
  47. package/dist/hooks/intent-classifier.js +1 -1
  48. package/dist/hooks/keyword-detector.js +1 -1
  49. package/dist/hooks/notepad-injector.js +1 -1
  50. package/dist/hooks/permission-handler.js +1 -1
  51. package/dist/hooks/post-tool-failure.js +1 -1
  52. package/dist/hooks/post-tool-use.d.ts +6 -0
  53. package/dist/hooks/post-tool-use.js +37 -19
  54. package/dist/hooks/pre-compact.js +1 -1
  55. package/dist/hooks/pre-tool-use.d.ts +7 -0
  56. package/dist/hooks/pre-tool-use.js +24 -6
  57. package/dist/hooks/rate-limiter.js +1 -1
  58. package/dist/hooks/secret-filter.js +1 -1
  59. package/dist/hooks/session-recovery.js +1 -1
  60. package/dist/hooks/shared/command-parser.d.ts +44 -0
  61. package/dist/hooks/shared/command-parser.js +50 -0
  62. package/dist/hooks/shared/hook-response.d.ts +12 -2
  63. package/dist/hooks/shared/hook-response.js +30 -3
  64. package/dist/hooks/skill-injector.js +1 -1
  65. package/dist/hooks/slop-detector.js +2 -2
  66. package/dist/hooks/solution-injector.d.ts +9 -0
  67. package/dist/hooks/solution-injector.js +48 -5
  68. package/dist/hooks/stop-guard.js +137 -13
  69. package/dist/hooks/subagent-tracker.js +1 -1
  70. package/dist/i18n/index.js +3 -5
  71. package/dist/store/evidence-store.js +11 -0
  72. package/dist/store/implicit-feedback-store.d.ts +59 -0
  73. package/dist/store/implicit-feedback-store.js +153 -0
  74. package/dist/store/rule-store.js +8 -0
  75. package/package.json +2 -2
  76. 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.');
@@ -5,7 +5,6 @@
5
5
  * Authoritative: docs/plans/2026-04-03-forgen-rule-renderer-spec.md §6
6
6
  */
7
7
  import * as fs from 'node:fs';
8
- import * as os from 'node:os';
9
8
  import * as path from 'node:path';
10
9
  import { loadProfile } from '../store/profile-store.js';
11
10
  import { loadAllRules, loadActiveRules } from '../store/rule-store.js';
@@ -102,7 +101,7 @@ export async function handleInspect(args) {
102
101
  bypass: 'bypass.jsonl',
103
102
  drift: 'drift.jsonl',
104
103
  };
105
- const p = path.join(os.homedir(), '.forgen', 'state', 'enforcement', fileMap[sub]);
104
+ const p = path.join(STATE_DIR, 'enforcement', fileMap[sub]);
106
105
  if (!fs.existsSync(p)) {
107
106
  console.log(`\n No ${sub} data (${p} not found).\n`);
108
107
  return;
@@ -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
+ }
@@ -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
- /** ~/.forgen/ — v1 하네스 홈 디렉토리 */
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;
@@ -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
- /** ~/.forgen/ — v1 하네스 홈 디렉토리 */
9
- export const FORGEN_HOME = path.join(HOME, '.forgen');
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
+ }
@@ -9,6 +9,27 @@ export interface StatsSnapshot {
9
9
  drift7d: number;
10
10
  retired7d: number;
11
11
  lastExtraction: string;
12
+ /**
13
+ * H3 / v0.4.1 — assist 축 가시화. enforcement(block/violation) 는 이미 표시되지만
14
+ * assist(recall hit, surface, extraction) 는 v0.4.0 에서 8,000+ 번 작동했음에도
15
+ * 사용자에게 0건 노출되었다. 오늘 기준 숫자로 "지금 학습되고 있다" 를 surface.
16
+ */
17
+ assistToday: {
18
+ recallHits: number;
19
+ surfaced: number;
20
+ referenced: number;
21
+ extractedToday: number;
22
+ };
23
+ /**
24
+ * v0.4.1 철학 고도화 지표 — forge-profile.json 이 실제로 학습됐는지 한눈에.
25
+ * 값이 있으면 가시화, 없으면 undefined.
26
+ */
27
+ philosophy?: {
28
+ basePacks: string[];
29
+ trustPolicy: string;
30
+ axisScores: Record<string, number>;
31
+ lastReclassification: string | null;
32
+ };
12
33
  }
13
34
  export declare function computeStats(): StatsSnapshot;
14
35
  export declare function renderStats(s: StatsSnapshot): string;