@wooojin/forgen 0.2.1 → 0.3.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 (145) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.ko.md +25 -14
  3. package/README.md +61 -17
  4. package/agents/analyst.md +48 -4
  5. package/agents/architect.md +39 -4
  6. package/agents/code-reviewer.md +107 -77
  7. package/agents/critic.md +47 -4
  8. package/agents/debugger.md +46 -4
  9. package/agents/designer.md +40 -4
  10. package/agents/executor.md +112 -30
  11. package/agents/explore.md +45 -5
  12. package/agents/git-master.md +48 -4
  13. package/agents/planner.md +121 -18
  14. package/agents/solution-evolver.md +115 -0
  15. package/agents/test-engineer.md +58 -4
  16. package/agents/verifier.md +92 -77
  17. package/commands/architecture-decision.md +127 -258
  18. package/commands/calibrate.md +225 -0
  19. package/commands/code-review.md +163 -178
  20. package/commands/compound.md +127 -68
  21. package/commands/deep-interview.md +212 -110
  22. package/commands/docker.md +68 -178
  23. package/commands/forge-loop.md +215 -0
  24. package/commands/learn.md +231 -0
  25. package/commands/retro.md +215 -0
  26. package/commands/ship.md +277 -0
  27. package/dist/cli.js +25 -9
  28. package/dist/core/auto-compound-runner.js +14 -0
  29. package/dist/core/config-injector.d.ts +2 -1
  30. package/dist/core/config-injector.js +2 -1
  31. package/dist/core/dashboard.d.ts +17 -0
  32. package/dist/core/dashboard.js +158 -2
  33. package/dist/core/harness.d.ts +6 -1
  34. package/dist/core/harness.js +75 -19
  35. package/dist/core/paths.d.ts +31 -1
  36. package/dist/core/paths.js +43 -2
  37. package/dist/core/spawn.d.ts +3 -2
  38. package/dist/core/spawn.js +27 -8
  39. package/dist/core/types.d.ts +34 -0
  40. package/dist/engine/compound-lifecycle.d.ts +4 -3
  41. package/dist/engine/compound-lifecycle.js +91 -46
  42. package/dist/engine/learn-cli.d.ts +1 -0
  43. package/dist/engine/learn-cli.js +182 -0
  44. package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
  45. package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
  46. package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
  47. package/dist/engine/meta-learning/extraction-tuner.js +99 -0
  48. package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
  49. package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
  50. package/dist/engine/meta-learning/runner.d.ts +14 -0
  51. package/dist/engine/meta-learning/runner.js +90 -0
  52. package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
  53. package/dist/engine/meta-learning/scope-promoter.js +84 -0
  54. package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
  55. package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
  56. package/dist/engine/meta-learning/types.d.ts +114 -0
  57. package/dist/engine/meta-learning/types.js +43 -0
  58. package/dist/engine/solution-candidate.d.ts +30 -0
  59. package/dist/engine/solution-candidate.js +124 -0
  60. package/dist/engine/solution-fitness.d.ts +52 -0
  61. package/dist/engine/solution-fitness.js +95 -0
  62. package/dist/engine/solution-fixup.d.ts +30 -0
  63. package/dist/engine/solution-fixup.js +116 -0
  64. package/dist/engine/solution-format.d.ts +10 -2
  65. package/dist/engine/solution-format.js +287 -57
  66. package/dist/engine/solution-index.d.ts +1 -1
  67. package/dist/engine/solution-index.js +10 -0
  68. package/dist/engine/solution-matcher.d.ts +7 -1
  69. package/dist/engine/solution-matcher.js +137 -37
  70. package/dist/engine/solution-outcomes.d.ts +70 -0
  71. package/dist/engine/solution-outcomes.js +242 -0
  72. package/dist/engine/solution-quarantine.d.ts +36 -0
  73. package/dist/engine/solution-quarantine.js +172 -0
  74. package/dist/engine/solution-weakness.d.ts +45 -0
  75. package/dist/engine/solution-weakness.js +225 -0
  76. package/dist/engine/solution-writer.d.ts +5 -0
  77. package/dist/engine/solution-writer.js +18 -0
  78. package/dist/fgx.js +12 -8
  79. package/dist/hooks/context-guard.d.ts +5 -0
  80. package/dist/hooks/context-guard.js +118 -2
  81. package/dist/hooks/hooks-generator.d.ts +3 -0
  82. package/dist/hooks/hooks-generator.js +23 -6
  83. package/dist/hooks/keyword-detector.js +16 -100
  84. package/dist/hooks/post-tool-failure.js +7 -0
  85. package/dist/hooks/skill-injector.d.ts +4 -3
  86. package/dist/hooks/skill-injector.js +6 -4
  87. package/dist/hooks/solution-injector.js +20 -0
  88. package/dist/host/codex-adapter.d.ts +10 -0
  89. package/dist/host/codex-adapter.js +154 -0
  90. package/dist/mcp/solution-reader.d.ts +5 -5
  91. package/dist/mcp/solution-reader.js +34 -24
  92. package/dist/mcp/tools.js +8 -0
  93. package/dist/services/session.d.ts +19 -0
  94. package/dist/services/session.js +62 -0
  95. package/hooks/hooks.json +2 -2
  96. package/package.json +2 -1
  97. package/skills/architecture-decision/SKILL.md +113 -257
  98. package/skills/calibrate/SKILL.md +207 -0
  99. package/skills/code-review/SKILL.md +151 -178
  100. package/skills/compound/SKILL.md +126 -68
  101. package/skills/deep-interview/SKILL.md +210 -110
  102. package/skills/docker/SKILL.md +57 -179
  103. package/skills/forge-loop/SKILL.md +198 -0
  104. package/skills/learn/SKILL.md +216 -0
  105. package/skills/retro/SKILL.md +199 -0
  106. package/skills/ship/SKILL.md +259 -0
  107. package/agents/code-simplifier.md +0 -197
  108. package/agents/performance-reviewer.md +0 -172
  109. package/agents/qa-tester.md +0 -158
  110. package/agents/refactoring-expert.md +0 -168
  111. package/agents/scientist.md +0 -144
  112. package/agents/security-reviewer.md +0 -137
  113. package/agents/writer.md +0 -184
  114. package/commands/api-design.md +0 -268
  115. package/commands/ci-cd.md +0 -270
  116. package/commands/database.md +0 -263
  117. package/commands/debug-detective.md +0 -99
  118. package/commands/documentation.md +0 -276
  119. package/commands/ecomode.md +0 -51
  120. package/commands/frontend.md +0 -271
  121. package/commands/git-master.md +0 -90
  122. package/commands/incident-response.md +0 -292
  123. package/commands/migrate.md +0 -101
  124. package/commands/performance.md +0 -288
  125. package/commands/refactor.md +0 -105
  126. package/commands/security-review.md +0 -288
  127. package/commands/specify.md +0 -128
  128. package/commands/tdd.md +0 -183
  129. package/commands/testing-strategy.md +0 -265
  130. package/skills/api-design/SKILL.md +0 -262
  131. package/skills/ci-cd/SKILL.md +0 -264
  132. package/skills/database/SKILL.md +0 -257
  133. package/skills/debug-detective/SKILL.md +0 -95
  134. package/skills/documentation/SKILL.md +0 -270
  135. package/skills/ecomode/SKILL.md +0 -46
  136. package/skills/frontend/SKILL.md +0 -265
  137. package/skills/git-master/SKILL.md +0 -86
  138. package/skills/incident-response/SKILL.md +0 -286
  139. package/skills/migrate/SKILL.md +0 -96
  140. package/skills/performance/SKILL.md +0 -282
  141. package/skills/refactor/SKILL.md +0 -100
  142. package/skills/security-review/SKILL.md +0 -282
  143. package/skills/specify/SKILL.md +0 -122
  144. package/skills/tdd/SKILL.md +0 -178
  145. package/skills/testing-strategy/SKILL.md +0 -260
@@ -0,0 +1,116 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+ import { DEFAULT_EVIDENCE } from './solution-format.js';
5
+ import { diagnoseFromRawContent } from './solution-quarantine.js';
6
+ import { createLogger } from '../core/logger.js';
7
+ const log = createLogger('solution-fixup');
8
+ /**
9
+ * Attempt to repair known-safe frontmatter defects.
10
+ *
11
+ * Handled defects (pre-0.3.1 schema drift, observed on 5 auto-extracted
12
+ * solutions from 2026-04-10):
13
+ * - `extractedBy` missing → add `extractedBy: auto`
14
+ * - `evidence` block missing → add `DEFAULT_EVIDENCE`
15
+ *
16
+ * All other validation errors (bad scope, non-numeric confidence, etc.)
17
+ * are surfaced in `remaining_errors` and the file is left untouched —
18
+ * those require human judgement, not a mechanical default.
19
+ *
20
+ * `dryRun: true` (default) reports what would change without writing.
21
+ */
22
+ export function fixupSolutions(solutionsDir, opts = {}) {
23
+ const dryRun = opts.dryRun !== false;
24
+ const result = { scanned: 0, fixed: 0, untouched: 0, unfixable: 0, reports: [] };
25
+ if (!fs.existsSync(solutionsDir))
26
+ return result;
27
+ const files = fs.readdirSync(solutionsDir).filter((f) => f.endsWith('.md'));
28
+ for (const file of files) {
29
+ const filePath = path.join(solutionsDir, file);
30
+ result.scanned++;
31
+ let content;
32
+ try {
33
+ content = fs.readFileSync(filePath, 'utf-8');
34
+ }
35
+ catch {
36
+ result.unfixable++;
37
+ continue;
38
+ }
39
+ const errors = diagnoseFromRawContent(content);
40
+ if (errors.length === 0) {
41
+ result.untouched++;
42
+ continue;
43
+ }
44
+ const fix = tryFix(content, errors);
45
+ result.reports.push({
46
+ path: filePath,
47
+ changed: fix.changed,
48
+ added: fix.added,
49
+ remaining_errors: fix.remaining,
50
+ });
51
+ if (fix.changed && fix.remaining.length === 0) {
52
+ if (!dryRun) {
53
+ try {
54
+ fs.writeFileSync(filePath, fix.content);
55
+ log.debug(`fixed: ${filePath} (${fix.added.join(', ')})`);
56
+ }
57
+ catch (e) {
58
+ log.debug(`write failed: ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
59
+ result.unfixable++;
60
+ continue;
61
+ }
62
+ }
63
+ result.fixed++;
64
+ }
65
+ else {
66
+ result.unfixable++;
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+ function tryFix(content, initialErrors) {
72
+ const trimmed = content.trimStart();
73
+ const added = [];
74
+ if (!trimmed.startsWith('---')) {
75
+ return { changed: false, added, remaining: initialErrors, content };
76
+ }
77
+ const endIdx = trimmed.indexOf('---', 3);
78
+ if (endIdx === -1) {
79
+ return { changed: false, added, remaining: initialErrors, content };
80
+ }
81
+ const leadingWs = content.slice(0, content.length - trimmed.length);
82
+ const fmRaw = trimmed.slice(3, endIdx);
83
+ const body = trimmed.slice(endIdx + 3);
84
+ let fm;
85
+ try {
86
+ const parsed = yaml.load(fmRaw, { schema: yaml.JSON_SCHEMA });
87
+ if (parsed == null || typeof parsed !== 'object') {
88
+ return { changed: false, added, remaining: initialErrors, content };
89
+ }
90
+ fm = parsed;
91
+ }
92
+ catch {
93
+ return { changed: false, added, remaining: initialErrors, content };
94
+ }
95
+ if (fm.extractedBy !== 'auto' && fm.extractedBy !== 'manual') {
96
+ fm.extractedBy = 'auto';
97
+ added.push('extractedBy: auto');
98
+ }
99
+ if (fm.evidence == null || typeof fm.evidence !== 'object') {
100
+ fm.evidence = { ...DEFAULT_EVIDENCE };
101
+ added.push('evidence: default');
102
+ }
103
+ if (fm.supersedes === undefined) {
104
+ fm.supersedes = null;
105
+ added.push('supersedes: null');
106
+ }
107
+ const newFmRaw = yaml.dump(fm, { lineWidth: 120, noRefs: true, sortKeys: false });
108
+ const rebuilt = `${leadingWs}---\n${newFmRaw}---${body}`;
109
+ const remaining = diagnoseFromRawContent(rebuilt);
110
+ return {
111
+ changed: added.length > 0,
112
+ added,
113
+ remaining,
114
+ content: rebuilt,
115
+ };
116
+ }
@@ -13,7 +13,7 @@ export interface SolutionFrontmatter {
13
13
  status: SolutionStatus;
14
14
  confidence: number;
15
15
  type: SolutionType;
16
- scope: 'me' | 'team' | 'project';
16
+ scope: 'me' | 'team' | 'project' | 'universal';
17
17
  tags: string[];
18
18
  identifiers: string[];
19
19
  evidence: SolutionEvidence;
@@ -33,7 +33,7 @@ export interface SolutionIndexEntry {
33
33
  status: SolutionStatus;
34
34
  confidence: number;
35
35
  type: SolutionType;
36
- scope: 'me' | 'team' | 'project';
36
+ scope: 'me' | 'team' | 'project' | 'universal';
37
37
  tags: string[];
38
38
  /**
39
39
  * Pre-expanded tag set, computed at index build time via the term normalizer.
@@ -60,6 +60,14 @@ export declare const DEFAULT_EVIDENCE: SolutionEvidence;
60
60
  export declare function slugify(text: string): string;
61
61
  /** Runtime type guard for SolutionFrontmatter */
62
62
  export declare function validateFrontmatter(fm: unknown): fm is SolutionFrontmatter;
63
+ /**
64
+ * Return a list of validation errors for a parsed frontmatter object.
65
+ *
66
+ * Empty array = valid. Non-empty = each entry describes one missing/wrong
67
+ * field. Callers that only need a boolean should use `validateFrontmatter`.
68
+ * Slow path (quarantine logging) uses this to produce actionable diagnostics.
69
+ */
70
+ export declare function diagnoseFrontmatter(fm: unknown): string[];
63
71
  /** Parse YAML frontmatter from solution file content */
64
72
  export declare function parseFrontmatterOnly(content: string): SolutionFrontmatter | null;
65
73
  /** Parse a full V3 solution file into its components */
@@ -1,9 +1,26 @@
1
1
  import yaml from 'js-yaml';
2
2
  export const DEFAULT_EVIDENCE = {
3
- injected: 0, reflected: 0, negative: 0, sessions: 0, reExtracted: 0,
3
+ injected: 0,
4
+ reflected: 0,
5
+ negative: 0,
6
+ sessions: 0,
7
+ reExtracted: 0,
4
8
  };
5
- const VALID_STATUSES = ['experiment', 'candidate', 'verified', 'mature', 'retired'];
6
- const VALID_TYPES = ['pattern', 'solution', 'decision', 'troubleshoot', 'anti-pattern', 'convention'];
9
+ const VALID_STATUSES = [
10
+ 'experiment',
11
+ 'candidate',
12
+ 'verified',
13
+ 'mature',
14
+ 'retired',
15
+ ];
16
+ const VALID_TYPES = [
17
+ 'pattern',
18
+ 'solution',
19
+ 'decision',
20
+ 'troubleshoot',
21
+ 'anti-pattern',
22
+ 'convention',
23
+ ];
7
24
  // ── Helpers ──
8
25
  export function slugify(text) {
9
26
  const slug = text
@@ -18,43 +35,58 @@ export function slugify(text) {
18
35
  // ── Validation ──
19
36
  /** Runtime type guard for SolutionFrontmatter */
20
37
  export function validateFrontmatter(fm) {
21
- if (fm == null || typeof fm !== 'object')
22
- return false;
38
+ return diagnoseFrontmatter(fm).length === 0;
39
+ }
40
+ /**
41
+ * Return a list of validation errors for a parsed frontmatter object.
42
+ *
43
+ * Empty array = valid. Non-empty = each entry describes one missing/wrong
44
+ * field. Callers that only need a boolean should use `validateFrontmatter`.
45
+ * Slow path (quarantine logging) uses this to produce actionable diagnostics.
46
+ */
47
+ export function diagnoseFrontmatter(fm) {
48
+ const errors = [];
49
+ if (fm == null || typeof fm !== 'object') {
50
+ errors.push('frontmatter is not an object');
51
+ return errors;
52
+ }
23
53
  const o = fm;
24
54
  if (typeof o.name !== 'string')
25
- return false;
55
+ errors.push('name: must be string');
26
56
  if (typeof o.version !== 'number' || o.version <= 0)
27
- return false;
57
+ errors.push('version: must be positive number');
28
58
  if (typeof o.status !== 'string' || !VALID_STATUSES.includes(o.status))
29
- return false;
59
+ errors.push(`status: must be one of ${VALID_STATUSES.join('|')}`);
30
60
  if (typeof o.confidence !== 'number' || o.confidence < 0 || o.confidence > 1)
31
- return false;
61
+ errors.push('confidence: must be number in [0,1]');
32
62
  if (typeof o.type !== 'string' || !VALID_TYPES.includes(o.type))
33
- return false;
34
- if (o.scope !== 'me' && o.scope !== 'team' && o.scope !== 'project')
35
- return false;
63
+ errors.push(`type: must be one of ${VALID_TYPES.join('|')}`);
64
+ if (o.scope !== 'me' && o.scope !== 'team' && o.scope !== 'project' && o.scope !== 'universal')
65
+ errors.push('scope: must be me|team|project|universal');
36
66
  if (!Array.isArray(o.tags) || !o.tags.every((t) => typeof t === 'string'))
37
- return false;
67
+ errors.push('tags: must be string[]');
38
68
  if (!Array.isArray(o.identifiers) || !o.identifiers.every((t) => typeof t === 'string'))
39
- return false;
69
+ errors.push('identifiers: must be string[]');
40
70
  if (typeof o.created !== 'string')
41
- return false;
71
+ errors.push('created: must be string');
42
72
  if (typeof o.updated !== 'string')
43
- return false;
73
+ errors.push('updated: must be string');
44
74
  if (o.supersedes !== null && typeof o.supersedes !== 'string')
45
- return false;
75
+ errors.push('supersedes: must be string or null');
46
76
  if (o.extractedBy !== 'auto' && o.extractedBy !== 'manual')
47
- return false;
48
- // evidence
49
- if (o.evidence == null || typeof o.evidence !== 'object')
50
- return false;
51
- const ev = o.evidence;
52
- const evFields = ['injected', 'reflected', 'negative', 'sessions', 'reExtracted'];
53
- for (const f of evFields) {
54
- if (typeof ev[f] !== 'number')
55
- return false;
77
+ errors.push('extractedBy: missing or not auto|manual');
78
+ if (o.evidence == null || typeof o.evidence !== 'object') {
79
+ errors.push('evidence: block missing');
56
80
  }
57
- return true;
81
+ else {
82
+ const ev = o.evidence;
83
+ const evFields = ['injected', 'reflected', 'negative', 'sessions', 'reExtracted'];
84
+ for (const f of evFields) {
85
+ if (typeof ev[f] !== 'number')
86
+ errors.push(`evidence.${f}: must be number`);
87
+ }
88
+ }
89
+ return errors;
58
90
  }
59
91
  // ── Parsing ──
60
92
  /** Parse YAML frontmatter from solution file content */
@@ -122,7 +154,11 @@ export function parseSolutionV3(content) {
122
154
  // ── Serialization ──
123
155
  /** Serialize a SolutionV3 to a markdown string with YAML frontmatter */
124
156
  export function serializeSolutionV3(solution) {
125
- const yamlStr = yaml.dump(solution.frontmatter, { lineWidth: -1, quotingType: '"', schema: yaml.JSON_SCHEMA });
157
+ const yamlStr = yaml.dump(solution.frontmatter, {
158
+ lineWidth: -1,
159
+ quotingType: '"',
160
+ schema: yaml.JSON_SCHEMA,
161
+ });
126
162
  return `---\n${yamlStr}---\n\n## Context\n${solution.context}\n\n## Content\n${solution.content}\n`;
127
163
  }
128
164
  // ── Format Detection ──
@@ -149,34 +185,207 @@ export function isV1Format(content) {
149
185
  /** 한국어 불용어 — 태그로 의미 없는 일반 단어 */
150
186
  const KO_STOPWORDS = new Set([
151
187
  // 일반 불용어
152
- '적용', '패턴', '모든', '같은', '발견', '다른', '사용', '경우', '위해',
153
- '통해', '대한', '이후', '때문', '하는', '있는', '없는', '되는', '관련',
154
- '해야', '하고', '있다', '없다', '한다', '이런', '그런', '저런', '매우',
155
- '항상', '모두', '각각', '대해', '여러', '시작', '그것', '이것', '저것',
156
- '아주', '정말', '너무', '많이', '자주', '가장', '먼저', '이미', '아직',
157
- '그냥', '바로', '다시', '함께', '위한', '따라', '부분', '전체', '방법',
158
- '내용', '결과', '문제', '시점', '설정', '작업', '확인', '수행', '처리',
159
- '기본', '추가', '변경', '제거', '포함', '생성', '실행', '완료', '필요',
188
+ '적용',
189
+ '패턴',
190
+ '모든',
191
+ '같은',
192
+ '발견',
193
+ '다른',
194
+ '사용',
195
+ '경우',
196
+ '위해',
197
+ '통해',
198
+ '대한',
199
+ '이후',
200
+ '때문',
201
+ '하는',
202
+ '있는',
203
+ '없는',
204
+ '되는',
205
+ '관련',
206
+ '해야',
207
+ '하고',
208
+ '있다',
209
+ '없다',
210
+ '한다',
211
+ '이런',
212
+ '그런',
213
+ '저런',
214
+ '매우',
215
+ '항상',
216
+ '모두',
217
+ '각각',
218
+ '대해',
219
+ '여러',
220
+ '시작',
221
+ '그것',
222
+ '이것',
223
+ '저것',
224
+ '아주',
225
+ '정말',
226
+ '너무',
227
+ '많이',
228
+ '자주',
229
+ '가장',
230
+ '먼저',
231
+ '이미',
232
+ '아직',
233
+ '그냥',
234
+ '바로',
235
+ '다시',
236
+ '함께',
237
+ '위한',
238
+ '따라',
239
+ '부분',
240
+ '전체',
241
+ '방법',
242
+ '내용',
243
+ '결과',
244
+ '문제',
245
+ '시점',
246
+ '설정',
247
+ '작업',
248
+ '확인',
249
+ '수행',
250
+ '처리',
251
+ '기본',
252
+ '추가',
253
+ '변경',
254
+ '제거',
255
+ '포함',
256
+ '생성',
257
+ '실행',
258
+ '완료',
259
+ '필요',
160
260
  // 조사/어미/접속사 — Jaccard 분모 희석 방지
161
- '에서', '으로', '에게', '에는', '에도', '까지', '부터', '보다', '처럼',
162
- '만큼', '대로', '밖에', '뿐만', '이나', '이고', '이면', '이라', '인데',
163
- '했는데', '됐는데', '있으면', '없으면', '하면', '되면', '하지', '되지',
164
- '하며', '되며', '에서의', '으로의', '라는', '라고', '이라고', '때문에',
165
- '아니라', '하지만', '그러나', '그래서', '따라서', '그리고', '그러면',
166
- '만약', '비록', '하여', '않고', '않은', '않는', '해서', '해도', '해야',
261
+ '에서',
262
+ '으로',
263
+ '에게',
264
+ '에는',
265
+ '에도',
266
+ '까지',
267
+ '부터',
268
+ '보다',
269
+ '처럼',
270
+ '만큼',
271
+ '대로',
272
+ '밖에',
273
+ '뿐만',
274
+ '이나',
275
+ '이고',
276
+ '이면',
277
+ '이라',
278
+ '인데',
279
+ '했는데',
280
+ '됐는데',
281
+ '있으면',
282
+ '없으면',
283
+ '하면',
284
+ '되면',
285
+ '하지',
286
+ '되지',
287
+ '하며',
288
+ '되며',
289
+ '에서의',
290
+ '으로의',
291
+ '라는',
292
+ '라고',
293
+ '이라고',
294
+ '때문에',
295
+ '아니라',
296
+ '하지만',
297
+ '그러나',
298
+ '그래서',
299
+ '따라서',
300
+ '그리고',
301
+ '그러면',
302
+ '만약',
303
+ '비록',
304
+ '하여',
305
+ '않고',
306
+ '않은',
307
+ '않는',
308
+ '해서',
309
+ '해도',
310
+ '해야',
167
311
  // 일반 동사/형용사 어간 — 의미 없는 고빈도 단어
168
- '가능', '상태', '이유', '방지', '의존', '의존성', '즉시', '원칙', '근거',
169
- '수정', '제안', '기능', '구현', '구조', '단계', '목적', '상황', '조건',
170
- '규칙', '동작', '활성', '비활성', '원래', '현재', '이전', '다음', '최종',
312
+ '가능',
313
+ '상태',
314
+ '이유',
315
+ '방지',
316
+ '의존',
317
+ '의존성',
318
+ '즉시',
319
+ '원칙',
320
+ '근거',
321
+ '수정',
322
+ '제안',
323
+ '기능',
324
+ '구현',
325
+ '구조',
326
+ '단계',
327
+ '목적',
328
+ '상황',
329
+ '조건',
330
+ '규칙',
331
+ '동작',
332
+ '활성',
333
+ '비활성',
334
+ '원래',
335
+ '현재',
336
+ '이전',
337
+ '다음',
338
+ '최종',
171
339
  ]);
172
340
  /** 영어 불용어 */
173
341
  const EN_STOPWORDS = new Set([
174
- 'the', 'and', 'for', 'that', 'this', 'with', 'from', 'are', 'was',
175
- 'were', 'been', 'have', 'has', 'had', 'not', 'but', 'all', 'can',
176
- 'will', 'use', 'used', 'using', 'when', 'each', 'which', 'their',
177
- 'also', 'into', 'more', 'some', 'than', 'other', 'should', 'would',
178
- 'could', 'about', 'after', 'before', 'between', 'does', 'only',
179
- 'across', 'just', 'detected', 'based', 'sessions', 'prompts',
342
+ 'the',
343
+ 'and',
344
+ 'for',
345
+ 'that',
346
+ 'this',
347
+ 'with',
348
+ 'from',
349
+ 'are',
350
+ 'was',
351
+ 'were',
352
+ 'been',
353
+ 'have',
354
+ 'has',
355
+ 'had',
356
+ 'not',
357
+ 'but',
358
+ 'all',
359
+ 'can',
360
+ 'will',
361
+ 'use',
362
+ 'used',
363
+ 'using',
364
+ 'when',
365
+ 'each',
366
+ 'which',
367
+ 'their',
368
+ 'also',
369
+ 'into',
370
+ 'more',
371
+ 'some',
372
+ 'than',
373
+ 'other',
374
+ 'should',
375
+ 'would',
376
+ 'could',
377
+ 'about',
378
+ 'after',
379
+ 'before',
380
+ 'between',
381
+ 'does',
382
+ 'only',
383
+ 'across',
384
+ 'just',
385
+ 'detected',
386
+ 'based',
387
+ 'sessions',
388
+ 'prompts',
180
389
  ]);
181
390
  /** 한국어 일반 조사/어미 — strip 대상 (긴 것부터 매칭)
182
391
  *
@@ -189,9 +398,32 @@ const EN_STOPWORDS = new Set([
189
398
  * term-matcher의 `KO_VERBAL_SUFFIXES`에 따로 둔다.
190
399
  */
191
400
  export const KO_SUFFIXES = [
192
- '했습니다', '있습니다', '합니다', '입니다', '됩니다',
193
- '에서', '까지', '으로', '하는', '하고', '했다', '된다', '한다',
194
- '', '를', '이', '가', '은', '는', '의', '에', '와', '과', '도', '만', '로',
401
+ '했습니다',
402
+ '있습니다',
403
+ '합니다',
404
+ '입니다',
405
+ '됩니다',
406
+ '에서',
407
+ '까지',
408
+ '으로',
409
+ '하는',
410
+ '하고',
411
+ '했다',
412
+ '된다',
413
+ '한다',
414
+ '을',
415
+ '를',
416
+ '이',
417
+ '가',
418
+ '은',
419
+ '는',
420
+ '의',
421
+ '에',
422
+ '와',
423
+ '과',
424
+ '도',
425
+ '만',
426
+ '로',
195
427
  ];
196
428
  export function stripKoSuffix(word) {
197
429
  for (const suffix of KO_SUFFIXES) {
@@ -220,9 +452,7 @@ const MAX_TAGS = 8;
220
452
  * a fresh `ROUND3_BASELINE` measurement on every downstream PR.
221
453
  */
222
454
  export function extractTags(text) {
223
- const cleaned = text
224
- .toLowerCase()
225
- .replace(/[^가-힣a-z0-9\s]/g, ' ');
455
+ const cleaned = text.toLowerCase().replace(/[^가-힣a-z0-9\s]/g, ' ');
226
456
  const words = cleaned.split(/\s+/).filter(Boolean);
227
457
  const freq = new Map();
228
458
  for (const w of words) {
@@ -1,7 +1,7 @@
1
1
  import type { SolutionIndexEntry } from './solution-format.js';
2
2
  export interface SolutionDirConfig {
3
3
  dir: string;
4
- scope: 'me' | 'team' | 'project';
4
+ scope: 'me' | 'team' | 'project' | 'universal';
5
5
  }
6
6
  export interface SolutionIndex {
7
7
  entries: SolutionIndexEntry[];
@@ -5,6 +5,7 @@ import { defaultNormalizer } from './term-normalizer.js';
5
5
  import { withFileLockSync } from '../hooks/shared/file-lock.js';
6
6
  import { atomicWriteText } from '../hooks/shared/atomic-write.js';
7
7
  import { createLogger } from '../core/logger.js';
8
+ import { recordQuarantine, diagnoseFromRawContent } from './solution-quarantine.js';
8
9
  const log = createLogger('solution-index');
9
10
  /**
10
11
  * Cache keyed by an order-preserving directory signature.
@@ -155,6 +156,15 @@ function buildIndex(dirs) {
155
156
  const fm = parseFrontmatterOnly(content);
156
157
  if (!fm) {
157
158
  droppedMalformed++;
159
+ // Slow-path diagnosis: re-parse YAML to produce actionable errors,
160
+ // then persist to ~/.forgen/state/solution-quarantine.jsonl so the
161
+ // file is visible to `forgen doctor` instead of silently dead.
162
+ // Best-effort: quarantine writes must never throw.
163
+ try {
164
+ const errors = diagnoseFromRawContent(content);
165
+ recordQuarantine(filePath, errors);
166
+ }
167
+ catch { /* ignore */ }
158
168
  log.debug(`dropped (malformed frontmatter): ${filePath}`);
159
169
  continue;
160
170
  }
@@ -32,7 +32,7 @@ export declare function tagWeight(tag: string): number;
32
32
  export interface SolutionMatch {
33
33
  name: string;
34
34
  path: string;
35
- scope: 'me' | 'team' | 'project';
35
+ scope: 'me' | 'team' | 'project' | 'universal';
36
36
  relevance: number;
37
37
  summary: string;
38
38
  status: SolutionStatus;
@@ -65,6 +65,12 @@ export interface CalculateRelevanceOptions {
65
65
  solutionTagsExpanded?: string[];
66
66
  /** Average document (solution) tag count for BM25 normalization. Defaults to 6. */
67
67
  avgDocLength?: number;
68
+ /** Meta-learning: dynamic ensemble weights (sum must equal 1.0). Defaults to {tfidf:0.5, bm25:0.3, bigram:0.2}. */
69
+ ensembleWeights?: {
70
+ tfidf: number;
71
+ bm25: number;
72
+ bigram: number;
73
+ };
68
74
  }
69
75
  export declare function calculateRelevance(promptTags: string[], solutionTags: string[], confidence: number, options?: CalculateRelevanceOptions): {
70
76
  relevance: number;