@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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — MCP Solution Reader
|
|
3
|
+
*
|
|
4
|
+
* MCP 도구 핸들러를 위한 비즈니스 로직 파사드.
|
|
5
|
+
* 기존 solution-index, solution-matcher, solution-format 모듈을 조합하여
|
|
6
|
+
* 검색/목록/읽기/통계 기능을 제공합니다.
|
|
7
|
+
*
|
|
8
|
+
* 설계 결정:
|
|
9
|
+
* - MCP 도구 핸들러가 직접 fs/path를 다루지 않도록 격리
|
|
10
|
+
* - Hook injection(push)과 독립: 세션 캐시/버짓 적용 안 함
|
|
11
|
+
* - compound-read는 전문 반환 (축약 없음), compound-search는 요약만
|
|
12
|
+
* - prompt injection 필터는 동일하게 적용 (보안 일관성)
|
|
13
|
+
* - 인덱스 캐시는 isIndexStale()에 의존 (resetIndexCache 미사용)
|
|
14
|
+
* → 디렉토리 mtime이 변하지 않으면 캐시 재사용 (성능)
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { ME_SOLUTIONS, PACKS_DIR } from '../core/paths.js';
|
|
19
|
+
import { getOrBuildIndex } from '../engine/solution-index.js';
|
|
20
|
+
import { extractTags, expandCompoundTags, expandQueryBigrams } from '../engine/solution-format.js';
|
|
21
|
+
import { parseSolutionV3 } from '../engine/solution-format.js';
|
|
22
|
+
import { maskBlockedTokens } from '../engine/phrase-blocklist.js';
|
|
23
|
+
import { mutateSolutionFile } from '../engine/solution-writer.js';
|
|
24
|
+
import { calculateRelevance, shouldRejectByR4T3Rules } from '../engine/solution-matcher.js';
|
|
25
|
+
import { defaultNormalizer } from '../engine/term-normalizer.js';
|
|
26
|
+
import { logMatchDecision } from '../engine/match-eval-log.js';
|
|
27
|
+
import { filterSolutionContent } from '../hooks/prompt-injection-filter.js';
|
|
28
|
+
// ── 디렉토리 해석 ──
|
|
29
|
+
/**
|
|
30
|
+
* 기본 솔루션 디렉토리 목록 생성.
|
|
31
|
+
* MCP 서버에서 cwd를 전달받으면 project 스코프도 포함.
|
|
32
|
+
*/
|
|
33
|
+
export function defaultSolutionDirs(cwd) {
|
|
34
|
+
const dirs = [
|
|
35
|
+
{ dir: ME_SOLUTIONS, scope: 'me' },
|
|
36
|
+
];
|
|
37
|
+
// 팩 디렉토리 스캔 — 하위에 solutions/ 디렉토리가 있는 팩만 포함
|
|
38
|
+
// PR2c-2 (M7 fix): readdirSync 결과를 정렬해 결정적 순서 보장.
|
|
39
|
+
// 정렬 안 하면 같은 팩 집합도 파일시스템 순서에 따라 다른 cache key/precedence
|
|
40
|
+
// 가 생겨 LRU와 인덱스 결정성이 깨진다.
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(PACKS_DIR)) {
|
|
43
|
+
const packEntries = fs.readdirSync(PACKS_DIR).sort();
|
|
44
|
+
for (const entry of packEntries) {
|
|
45
|
+
const solDir = path.join(PACKS_DIR, entry, 'solutions');
|
|
46
|
+
if (fs.existsSync(solDir)) {
|
|
47
|
+
dirs.push({ dir: solDir, scope: 'team' });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// 팩 디렉토리 접근 실패는 무시
|
|
54
|
+
}
|
|
55
|
+
if (cwd) {
|
|
56
|
+
dirs.push({ dir: path.join(cwd, '.compound', 'solutions'), scope: 'project' });
|
|
57
|
+
}
|
|
58
|
+
return dirs;
|
|
59
|
+
}
|
|
60
|
+
// ── 검색 ──
|
|
61
|
+
/**
|
|
62
|
+
* 쿼리 텍스트로 솔루션을 검색합니다.
|
|
63
|
+
* 태그 기반 Jaccard 매칭 + confidence 가중치.
|
|
64
|
+
* Hook injection과 달리 세션 캐시/버짓 없이 순수 검색.
|
|
65
|
+
*
|
|
66
|
+
* 인덱스 캐시: getOrBuildIndex() 내부의 isIndexStale()이
|
|
67
|
+
* 디렉토리 mtime을 비교하여 변경 시에만 재구축합니다.
|
|
68
|
+
*/
|
|
69
|
+
export function searchSolutions(query, options) {
|
|
70
|
+
const dirs = options?.dirs ?? defaultSolutionDirs();
|
|
71
|
+
const limit = options?.limit ?? 10;
|
|
72
|
+
const index = getOrBuildIndex(dirs);
|
|
73
|
+
const queryTagsRaw = extractTags(query);
|
|
74
|
+
if (queryTagsRaw.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
// R4-T2: mask query tokens that belong to blocked English compounds
|
|
77
|
+
// ("performance review", "system architecture", etc.) BEFORE bigram
|
|
78
|
+
// expansion or canonical normalization runs. See `phrase-blocklist.ts`
|
|
79
|
+
// for the rationale and `solution-matcher.rankCandidates` for the
|
|
80
|
+
// mirror of this same step in the hook path. If every prompt tag
|
|
81
|
+
// gets masked, the query carries no dev-context signal and we return
|
|
82
|
+
// an empty result list.
|
|
83
|
+
const queryTags = maskBlockedTokens(query.toLowerCase(), queryTagsRaw);
|
|
84
|
+
if (queryTags.length === 0)
|
|
85
|
+
return [];
|
|
86
|
+
// T2: normalize query ONCE outside the per-entry loop and reuse for every
|
|
87
|
+
// calculateRelevance call. Matches the same hot-path optimization in
|
|
88
|
+
// solution-matcher.rankCandidates.
|
|
89
|
+
//
|
|
90
|
+
// R4-T1: layer adjacent-token bigram expansion BEFORE canonical
|
|
91
|
+
// normalization so compound query phrases like "api keys" produce the
|
|
92
|
+
// `api-key`/`apikey`/`api-keys` candidates that hit hyphenated solution
|
|
93
|
+
// tags via direct intersection. This mirrors the same change in
|
|
94
|
+
// solution-matcher.rankCandidates so hook and MCP paths stay consistent.
|
|
95
|
+
const queryTagsWithBigrams = expandQueryBigrams(queryTags);
|
|
96
|
+
const normalizedPromptTags = defaultNormalizer.normalizeTerms(queryTagsWithBigrams);
|
|
97
|
+
const results = [];
|
|
98
|
+
for (const entry of index.entries) {
|
|
99
|
+
if (options?.type && entry.type !== options.type)
|
|
100
|
+
continue;
|
|
101
|
+
if (options?.status && entry.status !== options.status)
|
|
102
|
+
continue;
|
|
103
|
+
// R4-T1: solution-side compound expansion. See the matching block in
|
|
104
|
+
// solution-matcher.rankCandidates for the rationale (separation of
|
|
105
|
+
// matching set from Jaccard union for normalization stability).
|
|
106
|
+
const entryTagsExpanded = expandCompoundTags(entry.tags);
|
|
107
|
+
// R4-T2: pass `queryTags` (already masked above) so the union
|
|
108
|
+
// denominator inside calculateRelevance uses the post-mask set, in
|
|
109
|
+
// sync with the matching step.
|
|
110
|
+
const result = calculateRelevance(queryTags, entry.tags, entry.confidence, { normalizedPromptTags, solutionTagsExpanded: entryTagsExpanded });
|
|
111
|
+
// 태그 매칭 + 이름 매칭: 솔루션 이름에 쿼리 단어가 포함되면 boost
|
|
112
|
+
// Compute name match FIRST so R4-T3 cannot silently drop a candidate
|
|
113
|
+
// with strong name-match evidence.
|
|
114
|
+
const nameWords = entry.name.toLowerCase().split(/[-_]/);
|
|
115
|
+
const nameMatchCount = queryTags.filter(t => nameWords.includes(t)).length;
|
|
116
|
+
const nameBoost = nameMatchCount * 0.1;
|
|
117
|
+
// R4-T3: orchestration-layer specificity guards (mirror of the
|
|
118
|
+
// matching block in solution-matcher.rankCandidates). Reject single-
|
|
119
|
+
// tag matches that lack a corroborating signal. Name-match hits are
|
|
120
|
+
// the MCP-path equivalent of the hook path's identifier boost and
|
|
121
|
+
// bypass the R4-T3 gate — a nameMatchCount > 0 is strong evidence.
|
|
122
|
+
let tagRelevance = result.relevance;
|
|
123
|
+
let tagMatches = result.matchedTags;
|
|
124
|
+
if (nameMatchCount === 0
|
|
125
|
+
&& tagMatches.length > 0
|
|
126
|
+
&& shouldRejectByR4T3Rules(queryTags, tagMatches)) {
|
|
127
|
+
tagRelevance = 0;
|
|
128
|
+
tagMatches = [];
|
|
129
|
+
}
|
|
130
|
+
if (tagMatches.length === 0 && nameMatchCount === 0)
|
|
131
|
+
continue;
|
|
132
|
+
results.push({
|
|
133
|
+
name: entry.name,
|
|
134
|
+
status: entry.status,
|
|
135
|
+
confidence: entry.confidence,
|
|
136
|
+
type: entry.type,
|
|
137
|
+
scope: entry.scope,
|
|
138
|
+
tags: entry.tags,
|
|
139
|
+
relevance: tagRelevance + nameBoost,
|
|
140
|
+
matchedTags: [...tagMatches, ...queryTags.filter(t => nameWords.includes(t) && !tagMatches.includes(t))],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
results.sort((a, b) => b.relevance - a.relevance);
|
|
144
|
+
const top = results.slice(0, limit);
|
|
145
|
+
// T3: ranking-decision log for offline analysis. Fail-open via logger's
|
|
146
|
+
// own try/catch. `source: 'mcp'` distinguishes this from the hook path.
|
|
147
|
+
// `rankedTopN` here equals the post-`limit` slice the caller receives —
|
|
148
|
+
// MCP path has no further filtering, so for this source ranked == returned.
|
|
149
|
+
// Skip when limit is 0 (caller doesn't care about results).
|
|
150
|
+
if (limit > 0) {
|
|
151
|
+
logMatchDecision({
|
|
152
|
+
source: 'mcp',
|
|
153
|
+
rawQuery: query,
|
|
154
|
+
normalizedQuery: normalizedPromptTags,
|
|
155
|
+
candidates: top.map(r => ({
|
|
156
|
+
name: r.name,
|
|
157
|
+
relevance: r.relevance,
|
|
158
|
+
matchedTerms: r.matchedTags,
|
|
159
|
+
})),
|
|
160
|
+
rankedTopN: top.slice(0, 5).map(r => r.name),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return top;
|
|
164
|
+
}
|
|
165
|
+
// ── 목록 ──
|
|
166
|
+
/** 솔루션 요약 목록을 반환합니다 (필터/정렬 지원). */
|
|
167
|
+
export function listSolutions(options) {
|
|
168
|
+
const dirs = options?.dirs ?? defaultSolutionDirs();
|
|
169
|
+
const index = getOrBuildIndex(dirs);
|
|
170
|
+
let entries = index.entries.map(e => ({
|
|
171
|
+
name: e.name,
|
|
172
|
+
status: e.status,
|
|
173
|
+
confidence: e.confidence,
|
|
174
|
+
type: e.type,
|
|
175
|
+
scope: e.scope,
|
|
176
|
+
tags: e.tags,
|
|
177
|
+
}));
|
|
178
|
+
if (options?.status)
|
|
179
|
+
entries = entries.filter(e => e.status === options.status);
|
|
180
|
+
if (options?.type)
|
|
181
|
+
entries = entries.filter(e => e.type === options.type);
|
|
182
|
+
if (options?.scope)
|
|
183
|
+
entries = entries.filter(e => e.scope === options.scope);
|
|
184
|
+
const sort = options?.sort ?? 'confidence';
|
|
185
|
+
if (sort === 'confidence') {
|
|
186
|
+
entries.sort((a, b) => b.confidence - a.confidence);
|
|
187
|
+
}
|
|
188
|
+
else if (sort === 'name') {
|
|
189
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
190
|
+
}
|
|
191
|
+
return entries;
|
|
192
|
+
}
|
|
193
|
+
// ── 읽기 ──
|
|
194
|
+
/**
|
|
195
|
+
* 이름으로 솔루션 전문을 읽습니다.
|
|
196
|
+
*
|
|
197
|
+
* MCP는 온디맨드이므로 truncation 없이 전문을 반환합니다.
|
|
198
|
+
* 이것이 hook injection(1500자 캡)과의 핵심 차이입니다.
|
|
199
|
+
* Prompt injection 필터만 적용합니다.
|
|
200
|
+
*/
|
|
201
|
+
export function readSolution(name, options) {
|
|
202
|
+
const dirs = options?.dirs ?? defaultSolutionDirs();
|
|
203
|
+
const index = getOrBuildIndex(dirs);
|
|
204
|
+
const entry = index.entries.find(e => e.name === name);
|
|
205
|
+
if (!entry)
|
|
206
|
+
return null;
|
|
207
|
+
let fileContent;
|
|
208
|
+
try {
|
|
209
|
+
// Security: symlink을 통한 임의 파일 읽기 방지
|
|
210
|
+
const fstat = fs.lstatSync(entry.filePath);
|
|
211
|
+
if (fstat.isSymbolicLink())
|
|
212
|
+
return null;
|
|
213
|
+
// Safety: 비정상적으로 큰 파일 거부 (100KB)
|
|
214
|
+
if (fstat.size > 100 * 1024)
|
|
215
|
+
return null;
|
|
216
|
+
fileContent = fs.readFileSync(entry.filePath, 'utf-8');
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const parsed = parseSolutionV3(fileContent);
|
|
222
|
+
if (!parsed)
|
|
223
|
+
return null;
|
|
224
|
+
// 보안: prompt injection 필터
|
|
225
|
+
const contentFilter = filterSolutionContent(parsed.content);
|
|
226
|
+
if (contentFilter.verdict === 'block')
|
|
227
|
+
return null;
|
|
228
|
+
const contextFilter = filterSolutionContent(parsed.context);
|
|
229
|
+
if (contextFilter.verdict === 'block')
|
|
230
|
+
return null;
|
|
231
|
+
// Pull(MCP) 경로: evidence에 기여 — sessions + reflected 카운트 증가
|
|
232
|
+
// PR2b: solution-writer.mutateSolutionFile로 통합. lock + fresh re-read로 race 방지.
|
|
233
|
+
if (!options?.skipEvidence) {
|
|
234
|
+
mutateSolutionFile(entry.filePath, sol => {
|
|
235
|
+
sol.frontmatter.evidence.sessions += 1;
|
|
236
|
+
sol.frontmatter.evidence.reflected += 1;
|
|
237
|
+
return true;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
name: entry.name,
|
|
242
|
+
status: entry.status,
|
|
243
|
+
confidence: entry.confidence,
|
|
244
|
+
type: entry.type,
|
|
245
|
+
scope: entry.scope,
|
|
246
|
+
tags: entry.tags,
|
|
247
|
+
identifiers: entry.identifiers,
|
|
248
|
+
context: contextFilter.sanitized,
|
|
249
|
+
content: contentFilter.sanitized,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// ── 통계 ──
|
|
253
|
+
/** 솔루션 통계 (status별, type별, scope별 카운트). */
|
|
254
|
+
export function getSolutionStats(options) {
|
|
255
|
+
const dirs = options?.dirs ?? defaultSolutionDirs();
|
|
256
|
+
const index = getOrBuildIndex(dirs);
|
|
257
|
+
const stats = {
|
|
258
|
+
total: index.entries.length,
|
|
259
|
+
// retired는 인덱스에서 제외되므로 항상 0 (solution-index.ts:73)
|
|
260
|
+
byStatus: { experiment: 0, candidate: 0, verified: 0, mature: 0, retired: 0 },
|
|
261
|
+
byType: { pattern: 0, solution: 0, decision: 0, troubleshoot: 0, 'anti-pattern': 0, convention: 0 },
|
|
262
|
+
byScope: { me: 0, team: 0, project: 0 },
|
|
263
|
+
};
|
|
264
|
+
for (const entry of index.entries) {
|
|
265
|
+
if (entry.status in stats.byStatus)
|
|
266
|
+
stats.byStatus[entry.status]++;
|
|
267
|
+
if (entry.type in stats.byType)
|
|
268
|
+
stats.byType[entry.type]++;
|
|
269
|
+
if (entry.scope in stats.byScope)
|
|
270
|
+
stats.byScope[entry.scope]++;
|
|
271
|
+
}
|
|
272
|
+
return stats;
|
|
273
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — MCP Tool Definitions
|
|
3
|
+
*
|
|
4
|
+
* 4개 도구를 McpServer에 등록합니다:
|
|
5
|
+
* - compound-search: 태그 기반 솔루션 검색
|
|
6
|
+
* - compound-list: 필터/정렬된 솔루션 목록
|
|
7
|
+
* - compound-read: 솔루션 전문 읽기
|
|
8
|
+
* - compound-stats: 통계 요약
|
|
9
|
+
*
|
|
10
|
+
* 설계 결정:
|
|
11
|
+
* - 각 도구는 solution-reader.ts의 순수 함수를 호출
|
|
12
|
+
* - cwd는 환경변수 COMPOUND_CWD에서 읽음 (Claude Code가 전달)
|
|
13
|
+
* - MCP SDK registerTool API + zod 스키마로 입력 검증
|
|
14
|
+
*/
|
|
15
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
+
export declare function registerTools(server: McpServer): void;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen — MCP Tool Definitions
|
|
3
|
+
*
|
|
4
|
+
* 4개 도구를 McpServer에 등록합니다:
|
|
5
|
+
* - compound-search: 태그 기반 솔루션 검색
|
|
6
|
+
* - compound-list: 필터/정렬된 솔루션 목록
|
|
7
|
+
* - compound-read: 솔루션 전문 읽기
|
|
8
|
+
* - compound-stats: 통계 요약
|
|
9
|
+
*
|
|
10
|
+
* 설계 결정:
|
|
11
|
+
* - 각 도구는 solution-reader.ts의 순수 함수를 호출
|
|
12
|
+
* - cwd는 환경변수 COMPOUND_CWD에서 읽음 (Claude Code가 전달)
|
|
13
|
+
* - MCP SDK registerTool API + zod 스키마로 입력 검증
|
|
14
|
+
*/
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { searchSolutions, listSolutions, readSolution, getSolutionStats, defaultSolutionDirs, } from './solution-reader.js';
|
|
17
|
+
import { processCorrection } from '../forge/evidence-processor.js';
|
|
18
|
+
function getCwd() {
|
|
19
|
+
return process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? undefined;
|
|
20
|
+
}
|
|
21
|
+
export function registerTools(server) {
|
|
22
|
+
// ── compound-search ──
|
|
23
|
+
server.registerTool('compound-search', {
|
|
24
|
+
description: 'Search accumulated compound knowledge (solutions, patterns) by query. Returns relevant matches ranked by tag-based similarity. When multiple results are returned, provide a brief summary of findings.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
query: z.string().describe('Search query — keywords, tech names, or problem description'),
|
|
27
|
+
type: z.enum(['pattern', 'solution', 'decision', 'troubleshoot', 'anti-pattern', 'convention']).optional()
|
|
28
|
+
.describe('Filter by solution type'),
|
|
29
|
+
status: z.enum(['experiment', 'candidate', 'verified', 'mature']).optional()
|
|
30
|
+
.describe('Filter by lifecycle status'),
|
|
31
|
+
limit: z.number().min(1).max(20).optional()
|
|
32
|
+
.describe('Max results to return (default: 10)'),
|
|
33
|
+
},
|
|
34
|
+
annotations: { readOnlyHint: true },
|
|
35
|
+
}, async ({ query, type, status, limit }) => {
|
|
36
|
+
const results = searchSolutions(query, {
|
|
37
|
+
dirs: defaultSolutionDirs(getCwd()),
|
|
38
|
+
type,
|
|
39
|
+
status,
|
|
40
|
+
limit,
|
|
41
|
+
});
|
|
42
|
+
if (results.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: 'No matching solutions found.',
|
|
47
|
+
}],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const text = results.map((r, i) => {
|
|
51
|
+
let snippet = '';
|
|
52
|
+
if (i < 5) {
|
|
53
|
+
try {
|
|
54
|
+
const full = readSolution(r.name, { dirs: defaultSolutionDirs(getCwd()), skipEvidence: true });
|
|
55
|
+
if (full?.content) {
|
|
56
|
+
const firstLines = full.content
|
|
57
|
+
.split('\n')
|
|
58
|
+
.filter(l => l.trim().length > 0)
|
|
59
|
+
.slice(0, 2)
|
|
60
|
+
.join(' ')
|
|
61
|
+
.slice(0, 150);
|
|
62
|
+
snippet = `\n Preview: ${firstLines}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch { /* skip snippet on error */ }
|
|
66
|
+
}
|
|
67
|
+
return (`${i + 1}. **${r.name}** (${r.status}, confidence: ${r.confidence.toFixed(2)})\n` +
|
|
68
|
+
` Type: ${r.type} | Scope: ${r.scope} | Relevance: ${r.relevance.toFixed(3)}\n` +
|
|
69
|
+
` Tags: ${r.tags.join(', ')}\n` +
|
|
70
|
+
` Matched: ${r.matchedTags.join(', ')}` +
|
|
71
|
+
snippet);
|
|
72
|
+
}).join('\n\n');
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: `Found ${results.length} matching solution(s):\n\n${text}`,
|
|
77
|
+
}],
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
// ── compound-list ──
|
|
81
|
+
server.registerTool('compound-list', {
|
|
82
|
+
description: 'List all accumulated compound solutions with optional filtering and sorting.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
status: z.enum(['experiment', 'candidate', 'verified', 'mature']).optional()
|
|
85
|
+
.describe('Filter by lifecycle status'),
|
|
86
|
+
type: z.enum(['pattern', 'solution', 'decision', 'troubleshoot', 'anti-pattern', 'convention']).optional()
|
|
87
|
+
.describe('Filter by solution type'),
|
|
88
|
+
scope: z.enum(['me', 'team', 'project']).optional()
|
|
89
|
+
.describe('Filter by scope'),
|
|
90
|
+
sort: z.enum(['confidence', 'updated', 'name']).optional()
|
|
91
|
+
.describe('Sort order (default: confidence)'),
|
|
92
|
+
},
|
|
93
|
+
annotations: { readOnlyHint: true },
|
|
94
|
+
}, async ({ status, type, scope, sort }) => {
|
|
95
|
+
const results = listSolutions({
|
|
96
|
+
dirs: defaultSolutionDirs(getCwd()),
|
|
97
|
+
status,
|
|
98
|
+
type,
|
|
99
|
+
scope,
|
|
100
|
+
sort,
|
|
101
|
+
});
|
|
102
|
+
if (results.length === 0) {
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: 'text',
|
|
106
|
+
text: 'No solutions found.',
|
|
107
|
+
}],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const text = results.map(r => `- **${r.name}** [${r.status}] confidence: ${r.confidence.toFixed(2)} | ${r.type} | ${r.scope} | tags: ${r.tags.join(', ')}`).join('\n');
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: `${results.length} solution(s):\n\n${text}`,
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
// ── compound-read ──
|
|
119
|
+
server.registerTool('compound-read', {
|
|
120
|
+
description: 'Read the full content of a specific compound solution by name. Use compound-search or compound-list first to find the name.',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
name: z.string().describe('Solution name (slug) to read'),
|
|
123
|
+
},
|
|
124
|
+
annotations: { readOnlyHint: false },
|
|
125
|
+
}, async ({ name }) => {
|
|
126
|
+
const result = readSolution(name, {
|
|
127
|
+
dirs: defaultSolutionDirs(getCwd()),
|
|
128
|
+
});
|
|
129
|
+
if (!result) {
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: `Solution "${name}" not found or filtered by security policy.`,
|
|
134
|
+
}],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const header = `# ${result.name}\n` +
|
|
138
|
+
`Status: ${result.status} | Confidence: ${result.confidence.toFixed(2)} | Type: ${result.type} | Scope: ${result.scope}\n` +
|
|
139
|
+
`Tags: ${result.tags.join(', ')}\n` +
|
|
140
|
+
(result.identifiers.length > 0 ? `Identifiers: ${result.identifiers.join(', ')}\n` : '');
|
|
141
|
+
const body = (result.context ? `\n## Context\n${result.context}\n` : '') +
|
|
142
|
+
`\n## Content\n${result.content}`;
|
|
143
|
+
return {
|
|
144
|
+
content: [{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: header + body,
|
|
147
|
+
}],
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
// ── compound-stats ──
|
|
151
|
+
server.registerTool('compound-stats', {
|
|
152
|
+
description: 'Get overview statistics of accumulated compound knowledge (total count, breakdown by status/type/scope).',
|
|
153
|
+
annotations: { readOnlyHint: true },
|
|
154
|
+
}, async () => {
|
|
155
|
+
const stats = getSolutionStats({
|
|
156
|
+
dirs: defaultSolutionDirs(getCwd()),
|
|
157
|
+
});
|
|
158
|
+
const lines = [
|
|
159
|
+
`Total solutions: ${stats.total}`,
|
|
160
|
+
'',
|
|
161
|
+
'By status:',
|
|
162
|
+
...Object.entries(stats.byStatus)
|
|
163
|
+
.filter(([, count]) => count > 0)
|
|
164
|
+
.map(([status, count]) => ` ${status}: ${count}`),
|
|
165
|
+
'',
|
|
166
|
+
'By type:',
|
|
167
|
+
...Object.entries(stats.byType)
|
|
168
|
+
.filter(([, count]) => count > 0)
|
|
169
|
+
.map(([type, count]) => ` ${type}: ${count}`),
|
|
170
|
+
'',
|
|
171
|
+
'By scope:',
|
|
172
|
+
...Object.entries(stats.byScope)
|
|
173
|
+
.filter(([, count]) => count > 0)
|
|
174
|
+
.map(([scope, count]) => ` ${scope}: ${count}`),
|
|
175
|
+
];
|
|
176
|
+
return {
|
|
177
|
+
content: [{
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: lines.join('\n'),
|
|
180
|
+
}],
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
// ── session-search ──
|
|
184
|
+
server.registerTool('session-search', {
|
|
185
|
+
description: 'Search past session conversations by keyword. Returns matching messages from previous Claude Code sessions. When presenting results, summarize key findings for the user.',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
query: z.string().describe('Search query — keywords to find in past conversations'),
|
|
188
|
+
limit: z.number().min(1).max(20).optional()
|
|
189
|
+
.describe('Max results to return (default: 10)'),
|
|
190
|
+
},
|
|
191
|
+
annotations: { readOnlyHint: true },
|
|
192
|
+
}, async ({ query, limit }) => {
|
|
193
|
+
try {
|
|
194
|
+
const { searchSessions, extractContextWindow } = await import('../core/session-store.js');
|
|
195
|
+
const results = searchSessions(query, limit ?? 10);
|
|
196
|
+
if (results.length === 0) {
|
|
197
|
+
return {
|
|
198
|
+
content: [{
|
|
199
|
+
type: 'text',
|
|
200
|
+
text: 'No matching messages found in past sessions.',
|
|
201
|
+
}],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// 세션별 그루핑 (세션당 최대 3메시지)
|
|
205
|
+
const grouped = new Map();
|
|
206
|
+
for (const r of results) {
|
|
207
|
+
const group = grouped.get(r.sessionId) ?? [];
|
|
208
|
+
if (group.length < 3) {
|
|
209
|
+
group.push(r);
|
|
210
|
+
grouped.set(r.sessionId, group);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const sessionBlocks = [];
|
|
214
|
+
let msgIndex = 1;
|
|
215
|
+
for (const [sessionId, msgs] of grouped) {
|
|
216
|
+
const first = msgs[0];
|
|
217
|
+
const date = first.timestamp ? first.timestamp.slice(0, 10) : 'unknown date';
|
|
218
|
+
const project = first.cwd ? first.cwd.split('/').pop() ?? first.cwd : 'unknown project';
|
|
219
|
+
const msgLines = msgs.map(r => {
|
|
220
|
+
const snippet = extractContextWindow(r.content, r.tokens);
|
|
221
|
+
return ` ${msgIndex++}. [${r.role}] ${snippet}`;
|
|
222
|
+
});
|
|
223
|
+
sessionBlocks.push(`Session: ${sessionId.slice(0, 8)} | Date: ${date} | Project: ${project}\n` +
|
|
224
|
+
msgLines.join('\n'));
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
content: [{
|
|
228
|
+
type: 'text',
|
|
229
|
+
text: `Found ${results.length} matching message(s) across ${grouped.size} session(s):\n\n${sessionBlocks.join('\n\n')}`,
|
|
230
|
+
}],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
return {
|
|
235
|
+
content: [{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: 'Session search unavailable (requires Node.js 22+ with SQLite support).',
|
|
238
|
+
}],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
// ── correction-record ──
|
|
243
|
+
server.registerTool('correction-record', {
|
|
244
|
+
description: [
|
|
245
|
+
'Record a user correction as structured evidence.',
|
|
246
|
+
'Call this when the user explicitly corrects your behavior (e.g., "don\'t do X", "always do Y", "fix this now").',
|
|
247
|
+
'This creates an Evidence record and optionally a temporary session Rule.',
|
|
248
|
+
'',
|
|
249
|
+
'kind values:',
|
|
250
|
+
' fix-now — immediate fix needed, creates a session-scoped temporary rule',
|
|
251
|
+
' prefer-from-now — long-term preference, records evidence for future promotion',
|
|
252
|
+
' avoid-this — strong avoidance, creates a strong temporary rule',
|
|
253
|
+
].join('\n'),
|
|
254
|
+
inputSchema: {
|
|
255
|
+
session_id: z.string().describe('Current session ID'),
|
|
256
|
+
kind: z.enum(['fix-now', 'prefer-from-now', 'avoid-this'])
|
|
257
|
+
.describe('Correction type: fix-now (immediate), prefer-from-now (long-term), avoid-this (strong avoidance)'),
|
|
258
|
+
message: z.string().describe('What the user wants changed — the correction in natural language'),
|
|
259
|
+
target: z.string().describe('What is being corrected — the specific behavior, pattern, or output'),
|
|
260
|
+
axis_hint: z.enum(['quality_safety', 'autonomy', 'judgment_philosophy', 'communication_style']).nullable()
|
|
261
|
+
.describe('Which personalization axis this correction relates to (null if unclear)'),
|
|
262
|
+
},
|
|
263
|
+
}, async ({ session_id, kind, message, target, axis_hint }) => {
|
|
264
|
+
try {
|
|
265
|
+
// v1 session_id를 환경변수에서 가져옴 (하네스가 설정)
|
|
266
|
+
const effectiveSessionId = session_id || process.env.FORGEN_SESSION_ID || 'unknown';
|
|
267
|
+
const result = processCorrection({
|
|
268
|
+
session_id: effectiveSessionId,
|
|
269
|
+
kind: kind,
|
|
270
|
+
message,
|
|
271
|
+
target,
|
|
272
|
+
axis_hint: axis_hint,
|
|
273
|
+
});
|
|
274
|
+
const lines = [
|
|
275
|
+
`Evidence recorded: ${result.evidence_event_id}`,
|
|
276
|
+
];
|
|
277
|
+
if (result.temporary_rule) {
|
|
278
|
+
lines.push(`Temporary rule created: "${result.temporary_rule.policy}" (${result.temporary_rule.strength}, scope: ${result.temporary_rule.scope})`);
|
|
279
|
+
}
|
|
280
|
+
if (result.recompose_required) {
|
|
281
|
+
lines.push('Session recomposition recommended — the temporary rule should be applied to current session behavior.');
|
|
282
|
+
}
|
|
283
|
+
if (result.promotion_candidate) {
|
|
284
|
+
lines.push('This correction is a candidate for long-term rule promotion at session end.');
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
content: [{
|
|
288
|
+
type: 'text',
|
|
289
|
+
text: lines.join('\n'),
|
|
290
|
+
}],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: 'text',
|
|
297
|
+
text: `Failed to record correction: ${e instanceof Error ? e.message : String(e)}`,
|
|
298
|
+
}],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v1 — Facet Catalog
|
|
3
|
+
*
|
|
4
|
+
* Authoritative source: docs/plans/2026-04-03-forgen-facet-catalog.md
|
|
5
|
+
* 모든 facet 값 범위: 0.0 ~ 1.0
|
|
6
|
+
*/
|
|
7
|
+
import type { QualityPack, AutonomyPack, JudgmentPack, CommunicationPack, QualityFacets, AutonomyFacets, JudgmentFacets, CommunicationFacets } from '../store/types.js';
|
|
8
|
+
export declare const QUALITY_CENTROIDS: Record<QualityPack, QualityFacets>;
|
|
9
|
+
export declare const AUTONOMY_CENTROIDS: Record<AutonomyPack, AutonomyFacets>;
|
|
10
|
+
export declare const JUDGMENT_CENTROIDS: Record<JudgmentPack, JudgmentFacets>;
|
|
11
|
+
export declare const COMMUNICATION_CENTROIDS: Record<CommunicationPack, CommunicationFacets>;
|
|
12
|
+
export declare const DEFAULT_JUDGMENT_FACETS: JudgmentFacets;
|
|
13
|
+
export declare const DEFAULT_COMMUNICATION_FACETS: CommunicationFacets;
|
|
14
|
+
export declare function qualityCentroid(pack: QualityPack): QualityFacets;
|
|
15
|
+
export declare function autonomyCentroid(pack: AutonomyPack): AutonomyFacets;
|
|
16
|
+
export declare function judgmentCentroid(pack: JudgmentPack): JudgmentFacets;
|
|
17
|
+
export declare function communicationCentroid(pack: CommunicationPack): CommunicationFacets;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v1 — Facet Catalog
|
|
3
|
+
*
|
|
4
|
+
* Authoritative source: docs/plans/2026-04-03-forgen-facet-catalog.md
|
|
5
|
+
* 모든 facet 값 범위: 0.0 ~ 1.0
|
|
6
|
+
*/
|
|
7
|
+
// ── Quality centroids ──
|
|
8
|
+
export const QUALITY_CENTROIDS = {
|
|
9
|
+
'보수형': { verification_depth: 0.90, stop_threshold: 0.85, change_conservatism: 0.80 },
|
|
10
|
+
'균형형': { verification_depth: 0.60, stop_threshold: 0.55, change_conservatism: 0.55 },
|
|
11
|
+
'속도형': { verification_depth: 0.35, stop_threshold: 0.20, change_conservatism: 0.30 },
|
|
12
|
+
};
|
|
13
|
+
// ── Autonomy centroids ──
|
|
14
|
+
export const AUTONOMY_CENTROIDS = {
|
|
15
|
+
'확인 우선형': { confirmation_independence: 0.15, assumption_tolerance: 0.30, scope_expansion_tolerance: 0.35, approval_threshold: 0.25 },
|
|
16
|
+
'균형형': { confirmation_independence: 0.50, assumption_tolerance: 0.55, scope_expansion_tolerance: 0.55, approval_threshold: 0.60 },
|
|
17
|
+
'자율 실행형': { confirmation_independence: 0.80, assumption_tolerance: 0.85, scope_expansion_tolerance: 0.90, approval_threshold: 0.90 },
|
|
18
|
+
};
|
|
19
|
+
// ── Judgment centroids ──
|
|
20
|
+
export const JUDGMENT_CENTROIDS = {
|
|
21
|
+
'최소변경형': { minimal_change_bias: 0.85, abstraction_bias: 0.20, evidence_first_bias: 0.80 },
|
|
22
|
+
'균형형': { minimal_change_bias: 0.50, abstraction_bias: 0.50, evidence_first_bias: 0.50 },
|
|
23
|
+
'구조적접근형': { minimal_change_bias: 0.20, abstraction_bias: 0.85, evidence_first_bias: 0.70 },
|
|
24
|
+
};
|
|
25
|
+
// ── Communication centroids ──
|
|
26
|
+
export const COMMUNICATION_CENTROIDS = {
|
|
27
|
+
'간결형': { verbosity: 0.15, structure: 0.70, teaching_bias: 0.20 },
|
|
28
|
+
'균형형': { verbosity: 0.50, structure: 0.50, teaching_bias: 0.50 },
|
|
29
|
+
'상세형': { verbosity: 0.85, structure: 0.80, teaching_bias: 0.80 },
|
|
30
|
+
};
|
|
31
|
+
// ── Defaults (backward compat) ──
|
|
32
|
+
export const DEFAULT_JUDGMENT_FACETS = JUDGMENT_CENTROIDS['균형형'];
|
|
33
|
+
export const DEFAULT_COMMUNICATION_FACETS = COMMUNICATION_CENTROIDS['균형형'];
|
|
34
|
+
// ── Utilities ──
|
|
35
|
+
export function qualityCentroid(pack) {
|
|
36
|
+
return { ...QUALITY_CENTROIDS[pack] };
|
|
37
|
+
}
|
|
38
|
+
export function autonomyCentroid(pack) {
|
|
39
|
+
return { ...AUTONOMY_CENTROIDS[pack] };
|
|
40
|
+
}
|
|
41
|
+
export function judgmentCentroid(pack) {
|
|
42
|
+
return { ...JUDGMENT_CENTROIDS[pack] };
|
|
43
|
+
}
|
|
44
|
+
export function communicationCentroid(pack) {
|
|
45
|
+
return { ...COMMUNICATION_CENTROIDS[pack] };
|
|
46
|
+
}
|