@wooojin/forgen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +20 -0
- package/CHANGELOG.md +353 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +21 -0
- package/README.ja.md +469 -0
- package/README.ko.md +469 -0
- package/README.md +483 -0
- package/README.zh.md +469 -0
- package/agents/analyst.md +98 -0
- package/agents/architect.md +62 -0
- package/agents/code-reviewer.md +120 -0
- package/agents/code-simplifier.md +197 -0
- package/agents/critic.md +70 -0
- package/agents/debugger.md +117 -0
- package/agents/designer.md +131 -0
- package/agents/executor.md +54 -0
- package/agents/explore.md +145 -0
- package/agents/git-master.md +212 -0
- package/agents/performance-reviewer.md +172 -0
- package/agents/planner.md +29 -0
- package/agents/qa-tester.md +158 -0
- package/agents/refactoring-expert.md +168 -0
- package/agents/scientist.md +144 -0
- package/agents/security-reviewer.md +137 -0
- package/agents/test-engineer.md +153 -0
- package/agents/verifier.md +133 -0
- package/agents/writer.md +184 -0
- package/commands/api-design.md +268 -0
- package/commands/architecture-decision.md +314 -0
- package/commands/ci-cd.md +270 -0
- package/commands/code-review.md +233 -0
- package/commands/compound.md +117 -0
- package/commands/database.md +263 -0
- package/commands/debug-detective.md +99 -0
- package/commands/docker.md +274 -0
- package/commands/documentation.md +276 -0
- package/commands/ecomode.md +51 -0
- package/commands/frontend.md +271 -0
- package/commands/git-master.md +90 -0
- package/commands/incident-response.md +292 -0
- package/commands/migrate.md +101 -0
- package/commands/performance.md +288 -0
- package/commands/refactor.md +105 -0
- package/commands/security-review.md +288 -0
- package/commands/tdd.md +183 -0
- package/commands/testing-strategy.md +265 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/core/auto-compound-runner.d.ts +12 -0
- package/dist/core/auto-compound-runner.js +460 -0
- package/dist/core/config-hooks.d.ts +10 -0
- package/dist/core/config-hooks.js +112 -0
- package/dist/core/config-injector.d.ts +50 -0
- package/dist/core/config-injector.js +455 -0
- package/dist/core/doctor.d.ts +1 -0
- package/dist/core/doctor.js +163 -0
- package/dist/core/errors.d.ts +81 -0
- package/dist/core/errors.js +133 -0
- package/dist/core/global-config.d.ts +43 -0
- package/dist/core/global-config.js +25 -0
- package/dist/core/harness.d.ts +24 -0
- package/dist/core/harness.js +621 -0
- package/dist/core/init.d.ts +7 -0
- package/dist/core/init.js +37 -0
- package/dist/core/inspect-cli.d.ts +7 -0
- package/dist/core/inspect-cli.js +47 -0
- package/dist/core/legacy-detector.d.ts +33 -0
- package/dist/core/legacy-detector.js +66 -0
- package/dist/core/logger.d.ts +34 -0
- package/dist/core/logger.js +121 -0
- package/dist/core/mcp-config.d.ts +44 -0
- package/dist/core/mcp-config.js +177 -0
- package/dist/core/notepad.d.ts +31 -0
- package/dist/core/notepad.js +88 -0
- package/dist/core/paths.d.ts +85 -0
- package/dist/core/paths.js +101 -0
- package/dist/core/plugin-detector.d.ts +44 -0
- package/dist/core/plugin-detector.js +226 -0
- package/dist/core/runtime-detector.d.ts +8 -0
- package/dist/core/runtime-detector.js +49 -0
- package/dist/core/scope-resolver.d.ts +8 -0
- package/dist/core/scope-resolver.js +45 -0
- package/dist/core/session-logger.d.ts +6 -0
- package/dist/core/session-logger.js +111 -0
- package/dist/core/session-store.d.ts +28 -0
- package/dist/core/session-store.js +218 -0
- package/dist/core/settings-lock.d.ts +18 -0
- package/dist/core/settings-lock.js +125 -0
- package/dist/core/spawn.d.ts +3 -0
- package/dist/core/spawn.js +135 -0
- package/dist/core/types.d.ts +108 -0
- package/dist/core/types.js +1 -0
- package/dist/core/uninstall.d.ts +4 -0
- package/dist/core/uninstall.js +307 -0
- package/dist/core/v1-bootstrap.d.ts +26 -0
- package/dist/core/v1-bootstrap.js +155 -0
- package/dist/engine/compound-cli.d.ts +24 -0
- package/dist/engine/compound-cli.js +250 -0
- package/dist/engine/compound-extractor.d.ts +68 -0
- package/dist/engine/compound-extractor.js +860 -0
- package/dist/engine/compound-lifecycle.d.ts +32 -0
- package/dist/engine/compound-lifecycle.js +305 -0
- package/dist/engine/compound-loop.d.ts +32 -0
- package/dist/engine/compound-loop.js +511 -0
- package/dist/engine/match-eval-log.d.ts +139 -0
- package/dist/engine/match-eval-log.js +270 -0
- package/dist/engine/phrase-blocklist.d.ts +119 -0
- package/dist/engine/phrase-blocklist.js +208 -0
- package/dist/engine/skill-promoter.d.ts +20 -0
- package/dist/engine/skill-promoter.js +115 -0
- package/dist/engine/solution-format.d.ts +160 -0
- package/dist/engine/solution-format.js +432 -0
- package/dist/engine/solution-index.d.ts +13 -0
- package/dist/engine/solution-index.js +252 -0
- package/dist/engine/solution-matcher.d.ts +364 -0
- package/dist/engine/solution-matcher.js +656 -0
- package/dist/engine/solution-writer.d.ts +76 -0
- package/dist/engine/solution-writer.js +157 -0
- package/dist/engine/term-matcher.d.ts +81 -0
- package/dist/engine/term-matcher.js +268 -0
- package/dist/engine/term-normalizer.d.ts +116 -0
- package/dist/engine/term-normalizer.js +171 -0
- package/dist/fgx.d.ts +6 -0
- package/dist/fgx.js +42 -0
- package/dist/forge/cli.d.ts +11 -0
- package/dist/forge/cli.js +100 -0
- package/dist/forge/evidence-processor.d.ts +21 -0
- package/dist/forge/evidence-processor.js +87 -0
- package/dist/forge/mismatch-detector.d.ts +44 -0
- package/dist/forge/mismatch-detector.js +83 -0
- package/dist/forge/onboarding-cli.d.ts +6 -0
- package/dist/forge/onboarding-cli.js +89 -0
- package/dist/forge/onboarding.d.ts +25 -0
- package/dist/forge/onboarding.js +122 -0
- package/dist/hooks/compound-reflection.d.ts +45 -0
- package/dist/hooks/compound-reflection.js +82 -0
- package/dist/hooks/context-guard.d.ts +24 -0
- package/dist/hooks/context-guard.js +156 -0
- package/dist/hooks/dangerous-patterns.json +18 -0
- package/dist/hooks/db-guard.d.ts +17 -0
- package/dist/hooks/db-guard.js +105 -0
- package/dist/hooks/hook-config.d.ts +29 -0
- package/dist/hooks/hook-config.js +92 -0
- package/dist/hooks/hook-registry.d.ts +43 -0
- package/dist/hooks/hook-registry.js +31 -0
- package/dist/hooks/hooks-generator.d.ts +49 -0
- package/dist/hooks/hooks-generator.js +99 -0
- package/dist/hooks/intent-classifier.d.ts +12 -0
- package/dist/hooks/intent-classifier.js +62 -0
- package/dist/hooks/keyword-detector.d.ts +25 -0
- package/dist/hooks/keyword-detector.js +389 -0
- package/dist/hooks/notepad-injector.d.ts +18 -0
- package/dist/hooks/notepad-injector.js +51 -0
- package/dist/hooks/permission-handler.d.ts +14 -0
- package/dist/hooks/permission-handler.js +114 -0
- package/dist/hooks/post-tool-failure.d.ts +11 -0
- package/dist/hooks/post-tool-failure.js +118 -0
- package/dist/hooks/post-tool-handlers.d.ts +17 -0
- package/dist/hooks/post-tool-handlers.js +115 -0
- package/dist/hooks/post-tool-use.d.ts +29 -0
- package/dist/hooks/post-tool-use.js +151 -0
- package/dist/hooks/pre-compact.d.ts +10 -0
- package/dist/hooks/pre-compact.js +165 -0
- package/dist/hooks/pre-tool-use.d.ts +31 -0
- package/dist/hooks/pre-tool-use.js +325 -0
- package/dist/hooks/prompt-injection-filter.d.ts +56 -0
- package/dist/hooks/prompt-injection-filter.js +287 -0
- package/dist/hooks/rate-limiter.d.ts +21 -0
- package/dist/hooks/rate-limiter.js +86 -0
- package/dist/hooks/secret-filter.d.ts +14 -0
- package/dist/hooks/secret-filter.js +65 -0
- package/dist/hooks/session-recovery.d.ts +27 -0
- package/dist/hooks/session-recovery.js +406 -0
- package/dist/hooks/shared/atomic-write.d.ts +41 -0
- package/dist/hooks/shared/atomic-write.js +148 -0
- package/dist/hooks/shared/context-budget.d.ts +37 -0
- package/dist/hooks/shared/context-budget.js +45 -0
- package/dist/hooks/shared/file-lock.d.ts +56 -0
- package/dist/hooks/shared/file-lock.js +253 -0
- package/dist/hooks/shared/hook-response.d.ts +33 -0
- package/dist/hooks/shared/hook-response.js +62 -0
- package/dist/hooks/shared/injection-caps.d.ts +39 -0
- package/dist/hooks/shared/injection-caps.js +52 -0
- package/dist/hooks/shared/plugin-signal.d.ts +23 -0
- package/dist/hooks/shared/plugin-signal.js +104 -0
- package/dist/hooks/shared/read-stdin.d.ts +8 -0
- package/dist/hooks/shared/read-stdin.js +63 -0
- package/dist/hooks/shared/sanitize-id.d.ts +7 -0
- package/dist/hooks/shared/sanitize-id.js +9 -0
- package/dist/hooks/shared/sanitize.d.ts +7 -0
- package/dist/hooks/shared/sanitize.js +22 -0
- package/dist/hooks/skill-injector.d.ts +38 -0
- package/dist/hooks/skill-injector.js +285 -0
- package/dist/hooks/slop-detector.d.ts +18 -0
- package/dist/hooks/slop-detector.js +93 -0
- package/dist/hooks/solution-injector.d.ts +58 -0
- package/dist/hooks/solution-injector.js +436 -0
- package/dist/hooks/subagent-tracker.d.ts +10 -0
- package/dist/hooks/subagent-tracker.js +90 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +224 -0
- package/dist/lib.d.ts +14 -0
- package/dist/lib.js +14 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +40 -0
- package/dist/mcp/solution-reader.d.ts +90 -0
- package/dist/mcp/solution-reader.js +273 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +302 -0
- package/dist/preset/facet-catalog.d.ts +17 -0
- package/dist/preset/facet-catalog.js +46 -0
- package/dist/preset/preset-manager.d.ts +31 -0
- package/dist/preset/preset-manager.js +111 -0
- package/dist/renderer/inspect-renderer.d.ts +11 -0
- package/dist/renderer/inspect-renderer.js +123 -0
- package/dist/renderer/rule-renderer.d.ts +18 -0
- package/dist/renderer/rule-renderer.js +159 -0
- package/dist/store/evidence-store.d.ts +23 -0
- package/dist/store/evidence-store.js +58 -0
- package/dist/store/profile-store.d.ts +12 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/recommendation-store.d.ts +22 -0
- package/dist/store/recommendation-store.js +64 -0
- package/dist/store/rule-store.d.ts +22 -0
- package/dist/store/rule-store.js +62 -0
- package/dist/store/session-state-store.d.ts +11 -0
- package/dist/store/session-state-store.js +44 -0
- package/dist/store/types.d.ts +159 -0
- package/dist/store/types.js +7 -0
- package/hooks/hook-registry.json +21 -0
- package/hooks/hooks.json +185 -0
- package/package.json +89 -0
- package/plugin.json +20 -0
- package/scripts/postinstall.js +826 -0
- package/skills/api-design/SKILL.md +262 -0
- package/skills/architecture-decision/SKILL.md +309 -0
- package/skills/ci-cd/SKILL.md +264 -0
- package/skills/code-review/SKILL.md +228 -0
- package/skills/compound/SKILL.md +101 -0
- package/skills/database/SKILL.md +257 -0
- package/skills/debug-detective/SKILL.md +95 -0
- package/skills/docker/SKILL.md +268 -0
- package/skills/documentation/SKILL.md +270 -0
- package/skills/ecomode/SKILL.md +46 -0
- package/skills/frontend/SKILL.md +265 -0
- package/skills/git-master/SKILL.md +86 -0
- package/skills/incident-response/SKILL.md +286 -0
- package/skills/migrate/SKILL.md +96 -0
- package/skills/performance/SKILL.md +282 -0
- package/skills/refactor/SKILL.md +100 -0
- package/skills/security-review/SKILL.md +282 -0
- package/skills/tdd/SKILL.md +178 -0
- package/skills/testing-strategy/SKILL.md +260 -0
- package/starter-pack/solutions/starter-api-error-responses.md +37 -0
- package/starter-pack/solutions/starter-async-patterns.md +40 -0
- package/starter-pack/solutions/starter-caching-strategy.md +40 -0
- package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
- package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
- package/starter-pack/solutions/starter-dependency-injection.md +40 -0
- package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
- package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
- package/starter-pack/solutions/starter-input-validation.md +40 -0
- package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
- package/starter-pack/solutions/starter-refactor-safely.md +38 -0
- package/starter-pack/solutions/starter-secret-management.md +37 -0
- package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
- package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
- package/starter-pack/solutions/starter-typescript-strict-types.md +39 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ME_SOLUTIONS, ME_RULES } from '../core/paths.js';
|
|
4
|
+
import { resolveScope } from '../core/scope-resolver.js';
|
|
5
|
+
import { serializeSolutionV3, extractTags, DEFAULT_EVIDENCE, slugify } from './solution-format.js';
|
|
6
|
+
/** 키워드 기반으로 인사이트를 개인/팀으로 자동 분류 */
|
|
7
|
+
export function classifyInsight(title, content) {
|
|
8
|
+
const teamKeywords = [
|
|
9
|
+
'API', 'DB', 'database', 'migration', 'schema', 'deploy', 'CI', 'CD',
|
|
10
|
+
'security', 'auth', 'permission', 'error handling', 'logging', 'monitoring',
|
|
11
|
+
'convention', 'standard', 'guideline', 'rule', 'pattern', 'architecture',
|
|
12
|
+
'naming', 'structure', 'review', 'test strategy', 'documentation',
|
|
13
|
+
'에러 처리', '네이밍', '규칙', '규약', '표준', '패턴', '보안', '인증',
|
|
14
|
+
'배포', '마이그레이션', '아키텍처', '로깅', '모니터링', '구조',
|
|
15
|
+
];
|
|
16
|
+
const personalKeywords = [
|
|
17
|
+
'shortcut', 'preference', 'my style', 'editor', 'workflow tip',
|
|
18
|
+
'vim', 'vscode', 'alias', 'snippet', 'dotfile',
|
|
19
|
+
'단축키', '내 스타일', '편의', '습관',
|
|
20
|
+
];
|
|
21
|
+
const text = `${title} ${content}`.toLowerCase();
|
|
22
|
+
const teamScore = teamKeywords.filter(kw => text.includes(kw.toLowerCase())).length;
|
|
23
|
+
const personalScore = personalKeywords.filter(kw => text.includes(kw.toLowerCase())).length;
|
|
24
|
+
if (teamScore > personalScore) {
|
|
25
|
+
return { classification: 'team', reason: `team pattern (${teamScore} keyword matches)` };
|
|
26
|
+
}
|
|
27
|
+
if (personalScore > teamScore) {
|
|
28
|
+
return { classification: 'personal', reason: `personal style (${personalScore} keyword matches)` };
|
|
29
|
+
}
|
|
30
|
+
return { classification: 'personal', reason: 'default (personal)' };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compound Loop — 이미 추출된 인사이트를 저장
|
|
34
|
+
*/
|
|
35
|
+
export async function runCompoundLoop(cwd, insights) {
|
|
36
|
+
const saved = [];
|
|
37
|
+
const skipped = [];
|
|
38
|
+
const scope = resolveScope(cwd);
|
|
39
|
+
for (const insight of insights) {
|
|
40
|
+
try {
|
|
41
|
+
const destPath = getDestPath(insight, scope.team?.name);
|
|
42
|
+
if (!destPath) {
|
|
43
|
+
skipped.push(`${insight.title}: cannot determine save path`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// 중복 체크
|
|
47
|
+
if (fs.existsSync(destPath)) {
|
|
48
|
+
skipped.push(`${insight.title}: already exists`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// 디렉토리 생성
|
|
52
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
53
|
+
// 파일 저장
|
|
54
|
+
const fileContent = formatInsight(insight);
|
|
55
|
+
fs.writeFileSync(destPath, fileContent);
|
|
56
|
+
saved.push(`${insight.scope}/${insight.type}: ${insight.title}`);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
skipped.push(`${insight.title}: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { saved, skipped };
|
|
63
|
+
}
|
|
64
|
+
function getDestPath(insight, _teamPackName) {
|
|
65
|
+
const fileName = `${slugify(insight.title)}.md`;
|
|
66
|
+
if (insight.scope === 'me') {
|
|
67
|
+
const dir = insight.type === 'rule' || insight.type === 'convention'
|
|
68
|
+
? ME_RULES
|
|
69
|
+
: ME_SOLUTIONS;
|
|
70
|
+
return path.join(dir, fileName);
|
|
71
|
+
}
|
|
72
|
+
// v1: 팀 scope 제거 — 모든 인사이트를 개인으로 저장
|
|
73
|
+
const dir = insight.type === 'rule' || insight.type === 'convention'
|
|
74
|
+
? ME_RULES
|
|
75
|
+
: ME_SOLUTIONS;
|
|
76
|
+
return path.join(dir, fileName);
|
|
77
|
+
}
|
|
78
|
+
/** Map v1 CompoundInsight type to v3 SolutionType */
|
|
79
|
+
function mapInsightType(type) {
|
|
80
|
+
switch (type) {
|
|
81
|
+
case 'solution': return 'pattern';
|
|
82
|
+
case 'pattern': return 'pattern';
|
|
83
|
+
case 'rule': return 'decision';
|
|
84
|
+
case 'convention': return 'decision';
|
|
85
|
+
default: return 'pattern';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Infer identifiers from title and content for Code Reflection matching */
|
|
89
|
+
function inferIdentifiers(title, content) {
|
|
90
|
+
const text = `${title} ${content}`;
|
|
91
|
+
// Extract PascalCase words (likely class/component names)
|
|
92
|
+
const pascalCase = text.match(/\b[A-Z][a-zA-Z0-9]{3,}\b/g) ?? [];
|
|
93
|
+
// Extract camelCase words starting with lowercase (likely function names)
|
|
94
|
+
const camelCase = text.match(/\b[a-z][a-zA-Z0-9]{3,}(?=[A-Z])\w*/g) ?? [];
|
|
95
|
+
// Extract quoted strings that look like identifiers
|
|
96
|
+
const quoted = text.match(/['"`]([a-zA-Z][a-zA-Z0-9-]{3,})['"`]/g)?.map(s => s.slice(1, -1)) ?? [];
|
|
97
|
+
const all = [...new Set([...pascalCase, ...camelCase, ...quoted])]
|
|
98
|
+
.filter(id => id.length >= 4 && id.length <= 50);
|
|
99
|
+
return all.slice(0, 10); // max 10 identifiers
|
|
100
|
+
}
|
|
101
|
+
function formatInsight(insight) {
|
|
102
|
+
const today = new Date().toISOString().split('T')[0];
|
|
103
|
+
const solution = {
|
|
104
|
+
frontmatter: {
|
|
105
|
+
name: slugify(insight.title),
|
|
106
|
+
version: 1,
|
|
107
|
+
status: 'candidate',
|
|
108
|
+
confidence: 0.5,
|
|
109
|
+
type: mapInsightType(insight.type),
|
|
110
|
+
scope: insight.scope,
|
|
111
|
+
tags: extractTags(`${insight.title} ${insight.content}`),
|
|
112
|
+
identifiers: inferIdentifiers(insight.title, insight.content),
|
|
113
|
+
evidence: { ...DEFAULT_EVIDENCE },
|
|
114
|
+
created: today,
|
|
115
|
+
updated: today,
|
|
116
|
+
supersedes: null,
|
|
117
|
+
extractedBy: insight.source === 'manual' ? 'manual' : 'auto',
|
|
118
|
+
},
|
|
119
|
+
context: '',
|
|
120
|
+
content: insight.content,
|
|
121
|
+
};
|
|
122
|
+
return serializeSolutionV3(solution);
|
|
123
|
+
}
|
|
124
|
+
// slugify is imported from solution-format.ts (single source of truth)
|
|
125
|
+
/** 팀 제안으로 저장 (.compound/proposals/) */
|
|
126
|
+
export function saveTeamProposals(insights, cwd) {
|
|
127
|
+
const proposalsDir = path.join(cwd, '.compound', 'proposals');
|
|
128
|
+
fs.mkdirSync(proposalsDir, { recursive: true });
|
|
129
|
+
const date = new Date().toISOString().split('T')[0];
|
|
130
|
+
const filename = `${date}-${Date.now()}.json`;
|
|
131
|
+
fs.writeFileSync(path.join(proposalsDir, filename), JSON.stringify(insights, null, 2));
|
|
132
|
+
}
|
|
133
|
+
/** .compound/proposals/ 에서 제안 파일 로드 */
|
|
134
|
+
export function loadProposals(proposalsDir) {
|
|
135
|
+
if (!fs.existsSync(proposalsDir))
|
|
136
|
+
return [];
|
|
137
|
+
const files = fs.readdirSync(proposalsDir).filter(f => f.endsWith('.json'));
|
|
138
|
+
const all = [];
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(path.join(proposalsDir, file), 'utf-8');
|
|
142
|
+
const parsed = JSON.parse(content);
|
|
143
|
+
if (Array.isArray(parsed)) {
|
|
144
|
+
all.push(...parsed);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// skip malformed files
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return all;
|
|
152
|
+
}
|
|
153
|
+
/** 제안 파일 정리 */
|
|
154
|
+
export function cleanProposals(proposalsDir) {
|
|
155
|
+
if (!fs.existsSync(proposalsDir))
|
|
156
|
+
return;
|
|
157
|
+
const files = fs.readdirSync(proposalsDir).filter(f => f.endsWith('.json'));
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
fs.unlinkSync(path.join(proposalsDir, file));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** CLI 핸들러: forgen compound */
|
|
163
|
+
export async function handleCompound(args) {
|
|
164
|
+
const cwd = process.cwd();
|
|
165
|
+
const scope = resolveScope(cwd);
|
|
166
|
+
// --help 처리
|
|
167
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
168
|
+
console.log(`
|
|
169
|
+
Usage: forgen compound [options]
|
|
170
|
+
|
|
171
|
+
Default:
|
|
172
|
+
forgen compound Preview auto analysis from recent session/code changes
|
|
173
|
+
forgen compound --save Persist previewed insights
|
|
174
|
+
|
|
175
|
+
Manual add:
|
|
176
|
+
forgen compound --solution "title" "content"
|
|
177
|
+
forgen compound --rule "title" "content"
|
|
178
|
+
forgen compound --convention "title" "content"
|
|
179
|
+
forgen compound --to team Save to team scope
|
|
180
|
+
|
|
181
|
+
Inspect & manage:
|
|
182
|
+
forgen compound list List saved entries (solutions and rules)
|
|
183
|
+
forgen compound inspect <name> Show saved entry details
|
|
184
|
+
forgen compound remove <name> Remove a saved entry
|
|
185
|
+
forgen compound clean-stale Retire solutions from removed extractors (C4 cleanup)
|
|
186
|
+
forgen compound rollback --since 2026-03-20
|
|
187
|
+
Rollback unused auto-extracted solutions since date
|
|
188
|
+
|
|
189
|
+
Lifecycle:
|
|
190
|
+
forgen compound --lifecycle Run promotion/demotion/circuit-breaker check
|
|
191
|
+
forgen compound --verify <name> Manually promote solution to verified
|
|
192
|
+
|
|
193
|
+
Auto-extraction:
|
|
194
|
+
forgen compound --pause-auto Pause auto-extraction
|
|
195
|
+
forgen compound --resume-auto Resume auto-extraction
|
|
196
|
+
|
|
197
|
+
Interactive:
|
|
198
|
+
forgen compound interactive
|
|
199
|
+
`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// --pause-auto / --resume-auto
|
|
203
|
+
if (args.includes('--pause-auto') || args.includes('pause-auto')) {
|
|
204
|
+
const { pauseExtraction } = await import('./compound-extractor.js');
|
|
205
|
+
pauseExtraction();
|
|
206
|
+
console.log(' 자동 추출이 중단되었습니다. resume-auto로 재개할 수 있습니다.\n');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (args.includes('--resume-auto') || args.includes('resume-auto')) {
|
|
210
|
+
const { resumeExtraction } = await import('./compound-extractor.js');
|
|
211
|
+
resumeExtraction();
|
|
212
|
+
console.log(' 자동 추출이 재개되었습니다.\n');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// --- lifecycle command ---
|
|
216
|
+
if (args.includes('--lifecycle') || args.includes('lifecycle')) {
|
|
217
|
+
const { runLifecycleCheck } = await import('./compound-lifecycle.js');
|
|
218
|
+
const result = runLifecycleCheck();
|
|
219
|
+
console.log('\n Compound Lifecycle Check\n');
|
|
220
|
+
if (result.promoted.length) {
|
|
221
|
+
console.log(' Promoted:');
|
|
222
|
+
for (const p of result.promoted)
|
|
223
|
+
console.log(` ↑ ${p}`);
|
|
224
|
+
}
|
|
225
|
+
if (result.demoted.length) {
|
|
226
|
+
console.log(' Demoted:');
|
|
227
|
+
for (const d of result.demoted)
|
|
228
|
+
console.log(` ↓ ${d}`);
|
|
229
|
+
}
|
|
230
|
+
if (result.retired.length) {
|
|
231
|
+
console.log(' Retired:');
|
|
232
|
+
for (const r of result.retired)
|
|
233
|
+
console.log(` ✗ ${r}`);
|
|
234
|
+
}
|
|
235
|
+
if (result.contradictions.length) {
|
|
236
|
+
console.log(' Contradictions:');
|
|
237
|
+
for (const c of result.contradictions)
|
|
238
|
+
console.log(` ⚠ ${c}`);
|
|
239
|
+
}
|
|
240
|
+
if (!result.promoted.length && !result.demoted.length && !result.retired.length && !result.contradictions.length) {
|
|
241
|
+
console.log(' No lifecycle changes needed.\n');
|
|
242
|
+
}
|
|
243
|
+
console.log();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// --- verify command ---
|
|
247
|
+
if (args.includes('--verify')) {
|
|
248
|
+
const nameIdx = args.indexOf('--verify') + 1;
|
|
249
|
+
const name = args[nameIdx];
|
|
250
|
+
if (!name || name.startsWith('--')) {
|
|
251
|
+
console.log(' Usage: forgen compound --verify <solution-name>\n');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const { verifySolution } = await import('./compound-lifecycle.js');
|
|
255
|
+
if (verifySolution(name)) {
|
|
256
|
+
console.log(` ✓ "${name}" verified 상태로 승격됨\n`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
console.log(` ✗ "${name}" 솔루션을 찾을 수 없거나 업데이트 실패\n`);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// --- list command ---
|
|
264
|
+
if (args.includes('list') || args.includes('--list')) {
|
|
265
|
+
const { listSolutions } = await import('./compound-cli.js');
|
|
266
|
+
listSolutions();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// --- inspect command ---
|
|
270
|
+
if (args.includes('inspect') || args.includes('--inspect')) {
|
|
271
|
+
const nameIdx = Math.max(args.indexOf('inspect'), args.indexOf('--inspect')) + 1;
|
|
272
|
+
const name = args[nameIdx];
|
|
273
|
+
if (!name || name.startsWith('--')) {
|
|
274
|
+
console.log(' Usage: forgen compound inspect <solution-name>\n');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const { inspectSolution } = await import('./compound-cli.js');
|
|
278
|
+
inspectSolution(name);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// --- clean-stale command (M-3 migration) ---
|
|
282
|
+
if (args.includes('clean-stale') || args.includes('--clean-stale')) {
|
|
283
|
+
const { cleanStaleSolutions } = await import('./compound-cli.js');
|
|
284
|
+
cleanStaleSolutions();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// --- remove command ---
|
|
288
|
+
if (args.includes('remove') || args.includes('--remove')) {
|
|
289
|
+
const nameIdx = Math.max(args.indexOf('remove'), args.indexOf('--remove')) + 1;
|
|
290
|
+
const name = args[nameIdx];
|
|
291
|
+
if (!name || name.startsWith('--')) {
|
|
292
|
+
console.log(' Usage: forgen compound remove <solution-name>\n');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const { removeSolution } = await import('./compound-cli.js');
|
|
296
|
+
removeSolution(name);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// --- retag command ---
|
|
300
|
+
if (args.includes('retag') || args.includes('--retag')) {
|
|
301
|
+
const { retagSolutions } = await import('./compound-cli.js');
|
|
302
|
+
retagSolutions();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// --- rollback command ---
|
|
306
|
+
if (args.includes('rollback') || args.includes('--rollback')) {
|
|
307
|
+
const sinceIdx = args.indexOf('--since');
|
|
308
|
+
const since = sinceIdx !== -1 ? args[sinceIdx + 1] : undefined;
|
|
309
|
+
if (!since) {
|
|
310
|
+
console.log(' Usage: forgen compound rollback --since 2026-03-20\n');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const { rollbackSolutions } = await import('./compound-cli.js');
|
|
314
|
+
rollbackSolutions(since);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// --- explicit interactive command ---
|
|
318
|
+
if (args.includes('interactive') || args.includes('--interactive')) {
|
|
319
|
+
await interactiveCompound(cwd, scope);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// --- preview-first default mode ---
|
|
323
|
+
if (args.length === 0) {
|
|
324
|
+
const { previewExtraction } = await import('./compound-extractor.js');
|
|
325
|
+
const result = await previewExtraction(cwd);
|
|
326
|
+
console.log('\n Compound Preview\n');
|
|
327
|
+
console.log(` Scope: ${scope.summary}`);
|
|
328
|
+
console.log();
|
|
329
|
+
if (result.preview.length === 0) {
|
|
330
|
+
console.log(` No auto-analysis preview available${result.reason ? `: ${result.reason}` : '.'}`);
|
|
331
|
+
console.log(' Run `forgen compound --save` after meaningful code changes, or `forgen compound interactive` for manual capture.\n');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
console.log(' Preview only — nothing was saved.\n');
|
|
335
|
+
for (const [index, insight] of result.preview.entries()) {
|
|
336
|
+
console.log(` ${index + 1}. [${insight.type}] ${insight.name}`);
|
|
337
|
+
console.log(` ${insight.content.split('\n')[0]}`);
|
|
338
|
+
}
|
|
339
|
+
if (result.skipped.length > 0) {
|
|
340
|
+
console.log('\n Skipped:');
|
|
341
|
+
for (const entry of result.skipped.slice(0, 5)) {
|
|
342
|
+
console.log(` - ${entry}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
console.log('\n Run `forgen compound --save` to persist this preview.\n');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// --- auto save mode ---
|
|
349
|
+
if (args.includes('--save')) {
|
|
350
|
+
const { runExtraction } = await import('./compound-extractor.js');
|
|
351
|
+
const sessionId = `compound-cli-${Date.now()}`;
|
|
352
|
+
const result = await runExtraction(cwd, sessionId);
|
|
353
|
+
console.log('\n Compound Save\n');
|
|
354
|
+
console.log(` Scope: ${scope.summary}`);
|
|
355
|
+
console.log();
|
|
356
|
+
if (result.extracted.length === 0 && result.skipped.length === 0) {
|
|
357
|
+
console.log(` No insights saved${result.reason ? `: ${result.reason}` : '.'}\n`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
for (const saved of result.extracted) {
|
|
361
|
+
console.log(` ✓ Saved: ${saved}`);
|
|
362
|
+
}
|
|
363
|
+
for (const skipped of result.skipped) {
|
|
364
|
+
console.log(` ─ Skipped: ${skipped}`);
|
|
365
|
+
}
|
|
366
|
+
if (result.reason) {
|
|
367
|
+
console.log(` Reason: ${result.reason}`);
|
|
368
|
+
}
|
|
369
|
+
console.log();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// 인자가 없거나 알 수 없는 플래그만 있으면 수동 추가/interactive가 아닌 것으로 간주
|
|
373
|
+
const knownFlags = [
|
|
374
|
+
'--solution', '--rule', '--convention', '--pattern', '--to', '--pause-auto', '--resume-auto',
|
|
375
|
+
'--lifecycle', '--verify', '--save', '--interactive',
|
|
376
|
+
'list', 'inspect', 'remove', 'rollback', 'retag', 'lifecycle',
|
|
377
|
+
'--list', '--inspect', '--remove', '--rollback', '--retag', '--since', 'interactive',
|
|
378
|
+
];
|
|
379
|
+
const hasTypeFlag = knownFlags.some(f => args.includes(f));
|
|
380
|
+
if (!hasTypeFlag) {
|
|
381
|
+
console.log(' Unknown compound arguments. Run `forgen compound --help` for usage.\n');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
console.log('\n Compound Loop — Accumulating insights\n');
|
|
385
|
+
console.log(` Scope: ${scope.summary}`);
|
|
386
|
+
console.log();
|
|
387
|
+
// 수동 인사이트 추가
|
|
388
|
+
const type = args.includes('--solution') ? 'solution'
|
|
389
|
+
: args.includes('--rule') ? 'rule'
|
|
390
|
+
: args.includes('--convention') ? 'convention'
|
|
391
|
+
: 'pattern';
|
|
392
|
+
const scopeTarget = args.includes('--to')
|
|
393
|
+
? (args[args.indexOf('--to') + 1] === 'team' ? 'team' : 'me')
|
|
394
|
+
: 'me';
|
|
395
|
+
// --solution/--rule 다음 인자들이 제목과 내용 (-- 접두사 인자 필터)
|
|
396
|
+
const typeFlag = `--${type}`;
|
|
397
|
+
const flagIdx = args.indexOf(typeFlag);
|
|
398
|
+
const positionalArgs = args.slice(flagIdx + 1).filter(a => !a.startsWith('--'));
|
|
399
|
+
const title = positionalArgs[0];
|
|
400
|
+
const content = positionalArgs.slice(1).join(' ');
|
|
401
|
+
if (!title) {
|
|
402
|
+
console.log(' A title is required.');
|
|
403
|
+
console.log(' Usage: forgen compound --solution "title" "content"');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const { classification, reason } = classifyInsight(title, content || title);
|
|
407
|
+
const insight = {
|
|
408
|
+
id: `c-${Date.now()}`,
|
|
409
|
+
type,
|
|
410
|
+
title,
|
|
411
|
+
content: content || title,
|
|
412
|
+
scope: scopeTarget,
|
|
413
|
+
classification,
|
|
414
|
+
reason,
|
|
415
|
+
source: 'manual',
|
|
416
|
+
};
|
|
417
|
+
const result = await runCompoundLoop(cwd, [insight]);
|
|
418
|
+
for (const s of result.saved) {
|
|
419
|
+
console.log(` ✓ Saved: ${s}`);
|
|
420
|
+
}
|
|
421
|
+
for (const s of result.skipped) {
|
|
422
|
+
console.log(` ─ Skipped: ${s}`);
|
|
423
|
+
}
|
|
424
|
+
console.log();
|
|
425
|
+
}
|
|
426
|
+
async function interactiveCompound(cwd, scope) {
|
|
427
|
+
console.log("\n Forgen Compound — Today's insights\n");
|
|
428
|
+
console.log(` Scope: ${scope.summary}`);
|
|
429
|
+
console.log();
|
|
430
|
+
// Non-interactive mode: 대화 없이 안내만 출력
|
|
431
|
+
if (!process.stdin.isTTY) {
|
|
432
|
+
console.log(' Non-interactive environment. Add insights via manual mode.\n');
|
|
433
|
+
console.log(' Usage:');
|
|
434
|
+
console.log(' forgen compound --solution "title" "content"');
|
|
435
|
+
console.log(' forgen compound --rule "title" "content"');
|
|
436
|
+
console.log(' forgen compound --convention "title" "content"');
|
|
437
|
+
console.log(' forgen compound --to team Save to team scope\n');
|
|
438
|
+
console.log(' Interactive mode: run `forgen compound interactive` in a TTY environment\n');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const readline = await import('node:readline');
|
|
442
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
443
|
+
const prompt = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
444
|
+
const insights = [];
|
|
445
|
+
console.log(' Enter insights. Press enter on empty line to finish.\n');
|
|
446
|
+
let idx = 1;
|
|
447
|
+
while (true) {
|
|
448
|
+
const title = await prompt(` [${idx}] Title (empty=quit): `);
|
|
449
|
+
if (!title.trim())
|
|
450
|
+
break;
|
|
451
|
+
const content = await prompt(' Content: ');
|
|
452
|
+
const typeChoiceStr = await prompt(' Type (1=solution 2=rule 3=convention 4=pattern) [1]: ');
|
|
453
|
+
const typeMap = { '1': 'solution', '2': 'rule', '3': 'convention', '4': 'pattern' };
|
|
454
|
+
const insightType = typeMap[typeChoiceStr.trim()] ?? 'solution';
|
|
455
|
+
const { classification, reason } = classifyInsight(title, content);
|
|
456
|
+
const insight = {
|
|
457
|
+
id: `c-${Date.now()}-${idx}`,
|
|
458
|
+
type: insightType,
|
|
459
|
+
title: title.trim(),
|
|
460
|
+
content: content.trim(),
|
|
461
|
+
classification,
|
|
462
|
+
reason,
|
|
463
|
+
scope: classification === 'team' ? 'team' : 'me',
|
|
464
|
+
source: 'manual',
|
|
465
|
+
};
|
|
466
|
+
insights.push(insight);
|
|
467
|
+
const icon = classification === 'team' ? '👥' : '👤';
|
|
468
|
+
console.log(` → ${icon} ${classification} (${reason})\n`);
|
|
469
|
+
idx++;
|
|
470
|
+
}
|
|
471
|
+
if (insights.length === 0) {
|
|
472
|
+
console.log(' No insights.\n');
|
|
473
|
+
rl.close();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
// Show summary and let user adjust
|
|
477
|
+
console.log('\n ── Classification results ──\n');
|
|
478
|
+
for (let i = 0; i < insights.length; i++) {
|
|
479
|
+
const ins = insights[i];
|
|
480
|
+
const icon = ins.classification === 'team' ? '👥 Team' : '👤 Personal';
|
|
481
|
+
console.log(` ${i + 1}. [${icon}] ${ins.title}`);
|
|
482
|
+
}
|
|
483
|
+
console.log('\n Enter number to toggle classification (e.g. 2=team→personal), enter to confirm');
|
|
484
|
+
const changes = await prompt(' > ');
|
|
485
|
+
if (changes.trim()) {
|
|
486
|
+
for (const num of changes.split(/[,\s]+/)) {
|
|
487
|
+
const changeIdx = parseInt(num, 10) - 1;
|
|
488
|
+
if (changeIdx >= 0 && changeIdx < insights.length) {
|
|
489
|
+
insights[changeIdx].classification = insights[changeIdx].classification === 'team' ? 'personal' : 'team';
|
|
490
|
+
insights[changeIdx].scope = insights[changeIdx].classification === 'team' ? 'team' : 'me';
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Save — runCompoundLoop을 통해 타입별 올바른 경로에 저장
|
|
495
|
+
const personal = insights.filter(i => i.classification === 'personal');
|
|
496
|
+
const team = insights.filter(i => i.classification === 'team');
|
|
497
|
+
if (personal.length > 0) {
|
|
498
|
+
const result = await runCompoundLoop(cwd, personal);
|
|
499
|
+
for (const s of result.saved)
|
|
500
|
+
console.log(`\n ✓ Saved: ${s}`);
|
|
501
|
+
for (const s of result.skipped)
|
|
502
|
+
console.log(` ─ Skipped: ${s}`);
|
|
503
|
+
}
|
|
504
|
+
// Save team to .compound/proposals/ (for later propose)
|
|
505
|
+
if (team.length > 0) {
|
|
506
|
+
saveTeamProposals(team, cwd);
|
|
507
|
+
console.log(` ✓ ${team.length} team rule candidate(s) saved (.compound/proposals/)`);
|
|
508
|
+
console.log(' → Run forgen propose to share with the team.\n');
|
|
509
|
+
}
|
|
510
|
+
rl.close();
|
|
511
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match eval log — JSONL ranking-decision writer (T3 of the Round 3 plan).
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists:
|
|
5
|
+
* The bootstrap evaluator (`evaluateSolutionMatcher`) measures matcher
|
|
6
|
+
* quality against a labeled fixture, but production traffic is open-ended.
|
|
7
|
+
* T2 hoisted query normalization out of the per-solution loop, which is
|
|
8
|
+
* fast, but it also hid the "what did we actually rank, and why?" signal
|
|
9
|
+
* from offline review. This module appends a single JSONL line per matcher
|
|
10
|
+
* call capturing the normalized query, the top candidates with their
|
|
11
|
+
* matched terms, and which ones the caller ultimately surfaced.
|
|
12
|
+
*
|
|
13
|
+
* The target consumer is offline analysis: a reviewer can tail or grep
|
|
14
|
+
* the file to spot systematic recall misses or spurious matches without
|
|
15
|
+
* instrumenting production.
|
|
16
|
+
*
|
|
17
|
+
* Privacy posture (T3 security review fix):
|
|
18
|
+
* The raw user prompt is NEVER written to disk. Instead, we store a
|
|
19
|
+
* short SHA-256 prefix (`rawQueryHash`) plus character length
|
|
20
|
+
* (`rawQueryLen`). This keeps dedup and "was the prompt substantial"
|
|
21
|
+
* signals available for offline analysis while eliminating the PII /
|
|
22
|
+
* API-key / credential leakage risk of persisting raw prompts in
|
|
23
|
+
* `~/.forgen/state/match-eval-log.jsonl`. The `normalizedQuery` array
|
|
24
|
+
* already carries the matching-signal payload and is safe to persist
|
|
25
|
+
* because it only contains short tag tokens (never the full prompt).
|
|
26
|
+
*
|
|
27
|
+
* Operational principles:
|
|
28
|
+
* 1. **Off the critical path.** Never throw; never block. A failed write
|
|
29
|
+
* is silently swallowed — the hook must continue to return its
|
|
30
|
+
* solutions even if the log is misconfigured, read-only, or full.
|
|
31
|
+
* 2. **Bounded record size.** Candidates are capped at 5 (the matcher's
|
|
32
|
+
* own top-5 cap). `normalizedQuery` is capped at 64 terms. Each
|
|
33
|
+
* candidate's `matchedTerms` is capped at 16. Worst-case record ≈
|
|
34
|
+
* 2KB, which stays under Linux PIPE_BUF=4096 for safe concurrent
|
|
35
|
+
* appends on local filesystems.
|
|
36
|
+
* 3. **Symlink defense.** `fs.openSync` with `O_NOFOLLOW` refuses to
|
|
37
|
+
* follow a symlink at the log path. Without this guard, an attacker
|
|
38
|
+
* with write access to `~/.forgen/state/` could redirect appends to
|
|
39
|
+
* `~/.ssh/authorized_keys`, `~/.bashrc`, or other sensitive files.
|
|
40
|
+
* 4. **File-lock for concurrency.** Uses `withFileLockSync` to serialize
|
|
41
|
+
* concurrent writers. macOS PIPE_BUF=512 is smaller than the worst-
|
|
42
|
+
* case record size so POSIX atomic append alone isn't enough.
|
|
43
|
+
* 5. **Opt-out via env, fail-closed on invalid config.**
|
|
44
|
+
* `FORGEN_MATCH_EVAL_LOG=off|disabled|0|false|no` disables entirely.
|
|
45
|
+
* `FORGEN_MATCH_EVAL_LOG_SAMPLE=<float 0..1>` samples. An invalid
|
|
46
|
+
* sample value (NaN, out of range, whitespace) falls back to 0
|
|
47
|
+
* (skip) rather than 1 (log everything) — fail-closed for privacy.
|
|
48
|
+
* 6. **File size cap.** `readMatchEvalLog` refuses to parse files
|
|
49
|
+
* larger than 50 MB to prevent OOM in the offline analyzer. Callers
|
|
50
|
+
* are responsible for rotating the log externally.
|
|
51
|
+
*/
|
|
52
|
+
/** Environment variable controlling log enable/disable. */
|
|
53
|
+
export declare const MATCH_EVAL_LOG_ENV = "FORGEN_MATCH_EVAL_LOG";
|
|
54
|
+
/** Environment variable controlling sample rate (0.0 – 1.0). */
|
|
55
|
+
export declare const MATCH_EVAL_LOG_SAMPLE_ENV = "FORGEN_MATCH_EVAL_LOG_SAMPLE";
|
|
56
|
+
/**
|
|
57
|
+
* Single ranking decision captured at matcher call time.
|
|
58
|
+
*
|
|
59
|
+
* Rationale for each field:
|
|
60
|
+
* - `source`: distinguishes the hook path (`solution-injector`) from the
|
|
61
|
+
* MCP path (`solution-reader.searchSolutions`). They have different
|
|
62
|
+
* query shapes and the log should support filtering by origin.
|
|
63
|
+
* - `rawQueryHash`: first 16 hex chars of SHA-256 over the user prompt.
|
|
64
|
+
* Enables dedup ("this query shape recurred") without persisting the
|
|
65
|
+
* prompt text. NOT cryptographically reversible — only useful for
|
|
66
|
+
* grouping identical queries in offline analysis.
|
|
67
|
+
* - `rawQueryLen`: character count of the original prompt. A rough
|
|
68
|
+
* "was this a substantial query?" signal that helps triage.
|
|
69
|
+
* - `normalizedQuery`: the output of `defaultNormalizer.normalizeTerms`
|
|
70
|
+
* over `extractTags(rawQuery)`. This is what actually drove matching,
|
|
71
|
+
* so it's the most important piece for debugging ranking surprises.
|
|
72
|
+
* Only short tag tokens — safe to persist.
|
|
73
|
+
* - `candidates`: top-N ranked solutions with relevance and matched
|
|
74
|
+
* terms. Bounded by `MAX_CANDIDATES_LOGGED`.
|
|
75
|
+
* - `rankedTopN`: the names of the top-N solutions the CALLER RECEIVED
|
|
76
|
+
* from the matcher at the time of logging. This is the pre-filter top
|
|
77
|
+
* (hook path) or post-`limit` top (MCP path). Caller-side budget /
|
|
78
|
+
* experiment / disjoint filtering happens AFTER logging and is not
|
|
79
|
+
* captured here — by design, this field records what the matcher
|
|
80
|
+
* returned, not what the hook ultimately injected.
|
|
81
|
+
* - `ts`: ISO 8601 timestamp. Always set by the logger, never by the
|
|
82
|
+
* caller — prevents clock injection from polluting the log.
|
|
83
|
+
*/
|
|
84
|
+
export interface MatchEvalLogRecord {
|
|
85
|
+
source: 'hook' | 'mcp';
|
|
86
|
+
rawQueryHash: string;
|
|
87
|
+
rawQueryLen: number;
|
|
88
|
+
normalizedQuery: string[];
|
|
89
|
+
candidates: Array<{
|
|
90
|
+
name: string;
|
|
91
|
+
relevance: number;
|
|
92
|
+
matchedTerms: string[];
|
|
93
|
+
}>;
|
|
94
|
+
rankedTopN: string[];
|
|
95
|
+
ts: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Caller payload. `ts` and `rawQueryHash`/`rawQueryLen` are derived by
|
|
99
|
+
* the logger from the caller-supplied `rawQuery`. `rawQuery` itself is
|
|
100
|
+
* consumed in-process only and never written to disk.
|
|
101
|
+
*/
|
|
102
|
+
export interface MatchEvalLogInput {
|
|
103
|
+
source: 'hook' | 'mcp';
|
|
104
|
+
/** Raw user prompt. Hashed + length-captured, never persisted. */
|
|
105
|
+
rawQuery: string;
|
|
106
|
+
normalizedQuery: string[];
|
|
107
|
+
candidates: Array<{
|
|
108
|
+
name: string;
|
|
109
|
+
relevance: number;
|
|
110
|
+
matchedTerms: string[];
|
|
111
|
+
}>;
|
|
112
|
+
/**
|
|
113
|
+
* Top-N by relevance that the matcher returned to the caller at log
|
|
114
|
+
* time. See `MatchEvalLogRecord.rankedTopN` for semantics — this is
|
|
115
|
+
* NOT the post-filter "actually injected" set.
|
|
116
|
+
*/
|
|
117
|
+
rankedTopN: string[];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Append a single ranking decision to the match-eval-log JSONL file.
|
|
121
|
+
*
|
|
122
|
+
* Fail-open: any error is caught and debug-logged. Callers can invoke
|
|
123
|
+
* this without guarding — the logger will never bubble an exception into
|
|
124
|
+
* the hook critical path.
|
|
125
|
+
*/
|
|
126
|
+
export declare function logMatchDecision(input: MatchEvalLogInput): void;
|
|
127
|
+
/**
|
|
128
|
+
* Read all records from the match-eval-log file. Intended for tests and
|
|
129
|
+
* offline analysis tools; NOT for hot-path use.
|
|
130
|
+
*
|
|
131
|
+
* Malformed lines (non-JSON, missing required fields, wrong shape) are
|
|
132
|
+
* silently skipped — preserves the debug value of the rest of the file
|
|
133
|
+
* if one entry gets corrupted by a partial write or tool error.
|
|
134
|
+
*
|
|
135
|
+
* DoS guard: refuses to read files larger than `MAX_LOG_FILE_SIZE_BYTES`
|
|
136
|
+
* to prevent OOM when a long-running log grows unbounded. Returns [] in
|
|
137
|
+
* that case and debug-logs the skip.
|
|
138
|
+
*/
|
|
139
|
+
export declare function readMatchEvalLog(): MatchEvalLogRecord[];
|