@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.
Files changed (268) hide show
  1. package/.claude-plugin/plugin.json +20 -0
  2. package/CHANGELOG.md +353 -0
  3. package/CONTRIBUTING.md +98 -0
  4. package/LICENSE +21 -0
  5. package/README.ja.md +469 -0
  6. package/README.ko.md +469 -0
  7. package/README.md +483 -0
  8. package/README.zh.md +469 -0
  9. package/agents/analyst.md +98 -0
  10. package/agents/architect.md +62 -0
  11. package/agents/code-reviewer.md +120 -0
  12. package/agents/code-simplifier.md +197 -0
  13. package/agents/critic.md +70 -0
  14. package/agents/debugger.md +117 -0
  15. package/agents/designer.md +131 -0
  16. package/agents/executor.md +54 -0
  17. package/agents/explore.md +145 -0
  18. package/agents/git-master.md +212 -0
  19. package/agents/performance-reviewer.md +172 -0
  20. package/agents/planner.md +29 -0
  21. package/agents/qa-tester.md +158 -0
  22. package/agents/refactoring-expert.md +168 -0
  23. package/agents/scientist.md +144 -0
  24. package/agents/security-reviewer.md +137 -0
  25. package/agents/test-engineer.md +153 -0
  26. package/agents/verifier.md +133 -0
  27. package/agents/writer.md +184 -0
  28. package/commands/api-design.md +268 -0
  29. package/commands/architecture-decision.md +314 -0
  30. package/commands/ci-cd.md +270 -0
  31. package/commands/code-review.md +233 -0
  32. package/commands/compound.md +117 -0
  33. package/commands/database.md +263 -0
  34. package/commands/debug-detective.md +99 -0
  35. package/commands/docker.md +274 -0
  36. package/commands/documentation.md +276 -0
  37. package/commands/ecomode.md +51 -0
  38. package/commands/frontend.md +271 -0
  39. package/commands/git-master.md +90 -0
  40. package/commands/incident-response.md +292 -0
  41. package/commands/migrate.md +101 -0
  42. package/commands/performance.md +288 -0
  43. package/commands/refactor.md +105 -0
  44. package/commands/security-review.md +288 -0
  45. package/commands/tdd.md +183 -0
  46. package/commands/testing-strategy.md +265 -0
  47. package/dist/cli.d.ts +2 -0
  48. package/dist/cli.js +295 -0
  49. package/dist/core/auto-compound-runner.d.ts +12 -0
  50. package/dist/core/auto-compound-runner.js +460 -0
  51. package/dist/core/config-hooks.d.ts +10 -0
  52. package/dist/core/config-hooks.js +112 -0
  53. package/dist/core/config-injector.d.ts +50 -0
  54. package/dist/core/config-injector.js +455 -0
  55. package/dist/core/doctor.d.ts +1 -0
  56. package/dist/core/doctor.js +163 -0
  57. package/dist/core/errors.d.ts +81 -0
  58. package/dist/core/errors.js +133 -0
  59. package/dist/core/global-config.d.ts +43 -0
  60. package/dist/core/global-config.js +25 -0
  61. package/dist/core/harness.d.ts +24 -0
  62. package/dist/core/harness.js +621 -0
  63. package/dist/core/init.d.ts +7 -0
  64. package/dist/core/init.js +37 -0
  65. package/dist/core/inspect-cli.d.ts +7 -0
  66. package/dist/core/inspect-cli.js +47 -0
  67. package/dist/core/legacy-detector.d.ts +33 -0
  68. package/dist/core/legacy-detector.js +66 -0
  69. package/dist/core/logger.d.ts +34 -0
  70. package/dist/core/logger.js +121 -0
  71. package/dist/core/mcp-config.d.ts +44 -0
  72. package/dist/core/mcp-config.js +177 -0
  73. package/dist/core/notepad.d.ts +31 -0
  74. package/dist/core/notepad.js +88 -0
  75. package/dist/core/paths.d.ts +85 -0
  76. package/dist/core/paths.js +101 -0
  77. package/dist/core/plugin-detector.d.ts +44 -0
  78. package/dist/core/plugin-detector.js +226 -0
  79. package/dist/core/runtime-detector.d.ts +8 -0
  80. package/dist/core/runtime-detector.js +49 -0
  81. package/dist/core/scope-resolver.d.ts +8 -0
  82. package/dist/core/scope-resolver.js +45 -0
  83. package/dist/core/session-logger.d.ts +6 -0
  84. package/dist/core/session-logger.js +111 -0
  85. package/dist/core/session-store.d.ts +28 -0
  86. package/dist/core/session-store.js +218 -0
  87. package/dist/core/settings-lock.d.ts +18 -0
  88. package/dist/core/settings-lock.js +125 -0
  89. package/dist/core/spawn.d.ts +3 -0
  90. package/dist/core/spawn.js +135 -0
  91. package/dist/core/types.d.ts +108 -0
  92. package/dist/core/types.js +1 -0
  93. package/dist/core/uninstall.d.ts +4 -0
  94. package/dist/core/uninstall.js +307 -0
  95. package/dist/core/v1-bootstrap.d.ts +26 -0
  96. package/dist/core/v1-bootstrap.js +155 -0
  97. package/dist/engine/compound-cli.d.ts +24 -0
  98. package/dist/engine/compound-cli.js +250 -0
  99. package/dist/engine/compound-extractor.d.ts +68 -0
  100. package/dist/engine/compound-extractor.js +860 -0
  101. package/dist/engine/compound-lifecycle.d.ts +32 -0
  102. package/dist/engine/compound-lifecycle.js +305 -0
  103. package/dist/engine/compound-loop.d.ts +32 -0
  104. package/dist/engine/compound-loop.js +511 -0
  105. package/dist/engine/match-eval-log.d.ts +139 -0
  106. package/dist/engine/match-eval-log.js +270 -0
  107. package/dist/engine/phrase-blocklist.d.ts +119 -0
  108. package/dist/engine/phrase-blocklist.js +208 -0
  109. package/dist/engine/skill-promoter.d.ts +20 -0
  110. package/dist/engine/skill-promoter.js +115 -0
  111. package/dist/engine/solution-format.d.ts +160 -0
  112. package/dist/engine/solution-format.js +432 -0
  113. package/dist/engine/solution-index.d.ts +13 -0
  114. package/dist/engine/solution-index.js +252 -0
  115. package/dist/engine/solution-matcher.d.ts +364 -0
  116. package/dist/engine/solution-matcher.js +656 -0
  117. package/dist/engine/solution-writer.d.ts +76 -0
  118. package/dist/engine/solution-writer.js +157 -0
  119. package/dist/engine/term-matcher.d.ts +81 -0
  120. package/dist/engine/term-matcher.js +268 -0
  121. package/dist/engine/term-normalizer.d.ts +116 -0
  122. package/dist/engine/term-normalizer.js +171 -0
  123. package/dist/fgx.d.ts +6 -0
  124. package/dist/fgx.js +42 -0
  125. package/dist/forge/cli.d.ts +11 -0
  126. package/dist/forge/cli.js +100 -0
  127. package/dist/forge/evidence-processor.d.ts +21 -0
  128. package/dist/forge/evidence-processor.js +87 -0
  129. package/dist/forge/mismatch-detector.d.ts +44 -0
  130. package/dist/forge/mismatch-detector.js +83 -0
  131. package/dist/forge/onboarding-cli.d.ts +6 -0
  132. package/dist/forge/onboarding-cli.js +89 -0
  133. package/dist/forge/onboarding.d.ts +25 -0
  134. package/dist/forge/onboarding.js +122 -0
  135. package/dist/hooks/compound-reflection.d.ts +45 -0
  136. package/dist/hooks/compound-reflection.js +82 -0
  137. package/dist/hooks/context-guard.d.ts +24 -0
  138. package/dist/hooks/context-guard.js +156 -0
  139. package/dist/hooks/dangerous-patterns.json +18 -0
  140. package/dist/hooks/db-guard.d.ts +17 -0
  141. package/dist/hooks/db-guard.js +105 -0
  142. package/dist/hooks/hook-config.d.ts +29 -0
  143. package/dist/hooks/hook-config.js +92 -0
  144. package/dist/hooks/hook-registry.d.ts +43 -0
  145. package/dist/hooks/hook-registry.js +31 -0
  146. package/dist/hooks/hooks-generator.d.ts +49 -0
  147. package/dist/hooks/hooks-generator.js +99 -0
  148. package/dist/hooks/intent-classifier.d.ts +12 -0
  149. package/dist/hooks/intent-classifier.js +62 -0
  150. package/dist/hooks/keyword-detector.d.ts +25 -0
  151. package/dist/hooks/keyword-detector.js +389 -0
  152. package/dist/hooks/notepad-injector.d.ts +18 -0
  153. package/dist/hooks/notepad-injector.js +51 -0
  154. package/dist/hooks/permission-handler.d.ts +14 -0
  155. package/dist/hooks/permission-handler.js +114 -0
  156. package/dist/hooks/post-tool-failure.d.ts +11 -0
  157. package/dist/hooks/post-tool-failure.js +118 -0
  158. package/dist/hooks/post-tool-handlers.d.ts +17 -0
  159. package/dist/hooks/post-tool-handlers.js +115 -0
  160. package/dist/hooks/post-tool-use.d.ts +29 -0
  161. package/dist/hooks/post-tool-use.js +151 -0
  162. package/dist/hooks/pre-compact.d.ts +10 -0
  163. package/dist/hooks/pre-compact.js +165 -0
  164. package/dist/hooks/pre-tool-use.d.ts +31 -0
  165. package/dist/hooks/pre-tool-use.js +325 -0
  166. package/dist/hooks/prompt-injection-filter.d.ts +56 -0
  167. package/dist/hooks/prompt-injection-filter.js +287 -0
  168. package/dist/hooks/rate-limiter.d.ts +21 -0
  169. package/dist/hooks/rate-limiter.js +86 -0
  170. package/dist/hooks/secret-filter.d.ts +14 -0
  171. package/dist/hooks/secret-filter.js +65 -0
  172. package/dist/hooks/session-recovery.d.ts +27 -0
  173. package/dist/hooks/session-recovery.js +406 -0
  174. package/dist/hooks/shared/atomic-write.d.ts +41 -0
  175. package/dist/hooks/shared/atomic-write.js +148 -0
  176. package/dist/hooks/shared/context-budget.d.ts +37 -0
  177. package/dist/hooks/shared/context-budget.js +45 -0
  178. package/dist/hooks/shared/file-lock.d.ts +56 -0
  179. package/dist/hooks/shared/file-lock.js +253 -0
  180. package/dist/hooks/shared/hook-response.d.ts +33 -0
  181. package/dist/hooks/shared/hook-response.js +62 -0
  182. package/dist/hooks/shared/injection-caps.d.ts +39 -0
  183. package/dist/hooks/shared/injection-caps.js +52 -0
  184. package/dist/hooks/shared/plugin-signal.d.ts +23 -0
  185. package/dist/hooks/shared/plugin-signal.js +104 -0
  186. package/dist/hooks/shared/read-stdin.d.ts +8 -0
  187. package/dist/hooks/shared/read-stdin.js +63 -0
  188. package/dist/hooks/shared/sanitize-id.d.ts +7 -0
  189. package/dist/hooks/shared/sanitize-id.js +9 -0
  190. package/dist/hooks/shared/sanitize.d.ts +7 -0
  191. package/dist/hooks/shared/sanitize.js +22 -0
  192. package/dist/hooks/skill-injector.d.ts +38 -0
  193. package/dist/hooks/skill-injector.js +285 -0
  194. package/dist/hooks/slop-detector.d.ts +18 -0
  195. package/dist/hooks/slop-detector.js +93 -0
  196. package/dist/hooks/solution-injector.d.ts +58 -0
  197. package/dist/hooks/solution-injector.js +436 -0
  198. package/dist/hooks/subagent-tracker.d.ts +10 -0
  199. package/dist/hooks/subagent-tracker.js +90 -0
  200. package/dist/i18n/index.d.ts +43 -0
  201. package/dist/i18n/index.js +224 -0
  202. package/dist/lib.d.ts +14 -0
  203. package/dist/lib.js +14 -0
  204. package/dist/mcp/server.d.ts +8 -0
  205. package/dist/mcp/server.js +40 -0
  206. package/dist/mcp/solution-reader.d.ts +90 -0
  207. package/dist/mcp/solution-reader.js +273 -0
  208. package/dist/mcp/tools.d.ts +16 -0
  209. package/dist/mcp/tools.js +302 -0
  210. package/dist/preset/facet-catalog.d.ts +17 -0
  211. package/dist/preset/facet-catalog.js +46 -0
  212. package/dist/preset/preset-manager.d.ts +31 -0
  213. package/dist/preset/preset-manager.js +111 -0
  214. package/dist/renderer/inspect-renderer.d.ts +11 -0
  215. package/dist/renderer/inspect-renderer.js +123 -0
  216. package/dist/renderer/rule-renderer.d.ts +18 -0
  217. package/dist/renderer/rule-renderer.js +159 -0
  218. package/dist/store/evidence-store.d.ts +23 -0
  219. package/dist/store/evidence-store.js +58 -0
  220. package/dist/store/profile-store.d.ts +12 -0
  221. package/dist/store/profile-store.js +53 -0
  222. package/dist/store/recommendation-store.d.ts +22 -0
  223. package/dist/store/recommendation-store.js +64 -0
  224. package/dist/store/rule-store.d.ts +22 -0
  225. package/dist/store/rule-store.js +62 -0
  226. package/dist/store/session-state-store.d.ts +11 -0
  227. package/dist/store/session-state-store.js +44 -0
  228. package/dist/store/types.d.ts +159 -0
  229. package/dist/store/types.js +7 -0
  230. package/hooks/hook-registry.json +21 -0
  231. package/hooks/hooks.json +185 -0
  232. package/package.json +89 -0
  233. package/plugin.json +20 -0
  234. package/scripts/postinstall.js +826 -0
  235. package/skills/api-design/SKILL.md +262 -0
  236. package/skills/architecture-decision/SKILL.md +309 -0
  237. package/skills/ci-cd/SKILL.md +264 -0
  238. package/skills/code-review/SKILL.md +228 -0
  239. package/skills/compound/SKILL.md +101 -0
  240. package/skills/database/SKILL.md +257 -0
  241. package/skills/debug-detective/SKILL.md +95 -0
  242. package/skills/docker/SKILL.md +268 -0
  243. package/skills/documentation/SKILL.md +270 -0
  244. package/skills/ecomode/SKILL.md +46 -0
  245. package/skills/frontend/SKILL.md +265 -0
  246. package/skills/git-master/SKILL.md +86 -0
  247. package/skills/incident-response/SKILL.md +286 -0
  248. package/skills/migrate/SKILL.md +96 -0
  249. package/skills/performance/SKILL.md +282 -0
  250. package/skills/refactor/SKILL.md +100 -0
  251. package/skills/security-review/SKILL.md +282 -0
  252. package/skills/tdd/SKILL.md +178 -0
  253. package/skills/testing-strategy/SKILL.md +260 -0
  254. package/starter-pack/solutions/starter-api-error-responses.md +37 -0
  255. package/starter-pack/solutions/starter-async-patterns.md +40 -0
  256. package/starter-pack/solutions/starter-caching-strategy.md +40 -0
  257. package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
  258. package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
  259. package/starter-pack/solutions/starter-dependency-injection.md +40 -0
  260. package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
  261. package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
  262. package/starter-pack/solutions/starter-input-validation.md +40 -0
  263. package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
  264. package/starter-pack/solutions/starter-refactor-safely.md +38 -0
  265. package/starter-pack/solutions/starter-secret-management.md +37 -0
  266. package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
  267. package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
  268. package/starter-pack/solutions/starter-typescript-strict-types.md +39 -0
@@ -0,0 +1,432 @@
1
+ import yaml from 'js-yaml';
2
+ export const DEFAULT_EVIDENCE = {
3
+ injected: 0, reflected: 0, negative: 0, sessions: 0, reExtracted: 0,
4
+ };
5
+ const VALID_STATUSES = ['experiment', 'candidate', 'verified', 'mature', 'retired'];
6
+ const VALID_TYPES = ['pattern', 'solution', 'decision', 'troubleshoot', 'anti-pattern', 'convention'];
7
+ // ── Helpers ──
8
+ export function slugify(text) {
9
+ const slug = text
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9가-힣\s-]/g, '')
12
+ .replace(/\s+/g, '-')
13
+ .replace(/-+/g, '-')
14
+ .replace(/^-|-$/g, '')
15
+ .slice(0, 60);
16
+ return slug || `untitled-${Date.now()}`;
17
+ }
18
+ // ── Validation ──
19
+ /** Runtime type guard for SolutionFrontmatter */
20
+ export function validateFrontmatter(fm) {
21
+ if (fm == null || typeof fm !== 'object')
22
+ return false;
23
+ const o = fm;
24
+ if (typeof o.name !== 'string')
25
+ return false;
26
+ if (typeof o.version !== 'number' || o.version <= 0)
27
+ return false;
28
+ if (typeof o.status !== 'string' || !VALID_STATUSES.includes(o.status))
29
+ return false;
30
+ if (typeof o.confidence !== 'number' || o.confidence < 0 || o.confidence > 1)
31
+ return false;
32
+ 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;
36
+ if (!Array.isArray(o.tags) || !o.tags.every((t) => typeof t === 'string'))
37
+ return false;
38
+ if (!Array.isArray(o.identifiers) || !o.identifiers.every((t) => typeof t === 'string'))
39
+ return false;
40
+ if (typeof o.created !== 'string')
41
+ return false;
42
+ if (typeof o.updated !== 'string')
43
+ return false;
44
+ if (o.supersedes !== null && typeof o.supersedes !== 'string')
45
+ return false;
46
+ 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;
56
+ }
57
+ return true;
58
+ }
59
+ // ── Parsing ──
60
+ /** Parse YAML frontmatter from solution file content */
61
+ export function parseFrontmatterOnly(content) {
62
+ try {
63
+ const trimmed = content.trimStart();
64
+ if (!trimmed.startsWith('---'))
65
+ return null;
66
+ const endIdx = trimmed.indexOf('---', 3);
67
+ if (endIdx === -1)
68
+ return null;
69
+ const raw = trimmed.slice(3, endIdx);
70
+ // YAML bomb protection: reject oversized frontmatter
71
+ if (raw.length > 5000)
72
+ return null;
73
+ // YAML anchor abuse protection
74
+ const anchorCount = (raw.match(/(?<=\s|^)&\w+/g) ?? []).length;
75
+ if (anchorCount > 3)
76
+ return null;
77
+ const parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
78
+ if (!validateFrontmatter(parsed))
79
+ return null;
80
+ return parsed;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /** Parse a full V3 solution file into its components */
87
+ export function parseSolutionV3(content) {
88
+ try {
89
+ const frontmatter = parseFrontmatterOnly(content);
90
+ if (!frontmatter)
91
+ return null;
92
+ // Extract body after the closing ---
93
+ const trimmed = content.trimStart();
94
+ const endIdx = trimmed.indexOf('---', 3);
95
+ const body = trimmed.slice(endIdx + 3).trim();
96
+ const contextHeader = '## Context';
97
+ const contentHeader = '## Content';
98
+ const ctxIdx = body.indexOf(contextHeader);
99
+ const cntIdx = body.indexOf(contentHeader);
100
+ let context = '';
101
+ let solutionContent = '';
102
+ if (ctxIdx !== -1 && cntIdx !== -1) {
103
+ context = body.slice(ctxIdx + contextHeader.length, cntIdx).trim();
104
+ solutionContent = body.slice(cntIdx + contentHeader.length).trim();
105
+ }
106
+ else if (ctxIdx !== -1) {
107
+ context = body.slice(ctxIdx + contextHeader.length).trim();
108
+ }
109
+ else if (cntIdx !== -1) {
110
+ solutionContent = body.slice(cntIdx + contentHeader.length).trim();
111
+ }
112
+ else {
113
+ // No headers — treat entire body as content
114
+ solutionContent = body;
115
+ }
116
+ return { frontmatter, context, content: solutionContent };
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ // ── Serialization ──
123
+ /** Serialize a SolutionV3 to a markdown string with YAML frontmatter */
124
+ export function serializeSolutionV3(solution) {
125
+ const yamlStr = yaml.dump(solution.frontmatter, { lineWidth: -1, quotingType: '"', schema: yaml.JSON_SCHEMA });
126
+ return `---\n${yamlStr}---\n\n## Context\n${solution.context}\n\n## Content\n${solution.content}\n`;
127
+ }
128
+ // ── Format Detection ──
129
+ /** Check if content is in V3 format (YAML frontmatter) */
130
+ export function isV3Format(content) {
131
+ return content.trimStart().startsWith('---');
132
+ }
133
+ /** Check if content is in V1 format (# Title + > Type: pattern) */
134
+ export function isV1Format(content) {
135
+ const lines = content.split('\n');
136
+ let hasTitle = false;
137
+ let hasType = false;
138
+ for (const line of lines) {
139
+ if (line.startsWith('# '))
140
+ hasTitle = true;
141
+ if (line.startsWith('> Type:'))
142
+ hasType = true;
143
+ if (hasTitle && hasType)
144
+ return true;
145
+ }
146
+ return false;
147
+ }
148
+ // ── Tag Extraction ──
149
+ /** 한국어 불용어 — 태그로 의미 없는 일반 단어 */
150
+ const KO_STOPWORDS = new Set([
151
+ // 일반 불용어
152
+ '적용', '패턴', '모든', '같은', '발견', '다른', '사용', '경우', '위해',
153
+ '통해', '대한', '이후', '때문', '하는', '있는', '없는', '되는', '관련',
154
+ '해야', '하고', '있다', '없다', '한다', '이런', '그런', '저런', '매우',
155
+ '항상', '모두', '각각', '대해', '여러', '시작', '그것', '이것', '저것',
156
+ '아주', '정말', '너무', '많이', '자주', '가장', '먼저', '이미', '아직',
157
+ '그냥', '바로', '다시', '함께', '위한', '따라', '부분', '전체', '방법',
158
+ '내용', '결과', '문제', '시점', '설정', '작업', '확인', '수행', '처리',
159
+ '기본', '추가', '변경', '제거', '포함', '생성', '실행', '완료', '필요',
160
+ // 조사/어미/접속사 — Jaccard 분모 희석 방지
161
+ '에서', '으로', '에게', '에는', '에도', '까지', '부터', '보다', '처럼',
162
+ '만큼', '대로', '밖에', '뿐만', '이나', '이고', '이면', '이라', '인데',
163
+ '했는데', '됐는데', '있으면', '없으면', '하면', '되면', '하지', '되지',
164
+ '하며', '되며', '에서의', '으로의', '라는', '라고', '이라고', '때문에',
165
+ '아니라', '하지만', '그러나', '그래서', '따라서', '그리고', '그러면',
166
+ '만약', '비록', '하여', '않고', '않은', '않는', '해서', '해도', '해야',
167
+ // 일반 동사/형용사 어간 — 의미 없는 고빈도 단어
168
+ '가능', '상태', '이유', '방지', '의존', '의존성', '즉시', '원칙', '근거',
169
+ '수정', '제안', '기능', '구현', '구조', '단계', '목적', '상황', '조건',
170
+ '규칙', '동작', '활성', '비활성', '원래', '현재', '이전', '다음', '최종',
171
+ ]);
172
+ /** 영어 불용어 */
173
+ 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',
180
+ ]);
181
+ /** 한국어 일반 조사/어미 — strip 대상 (긴 것부터 매칭)
182
+ *
183
+ * term-matcher에서 재사용 가능하도록 export — 매칭 시점과 추출 시점의 stripping
184
+ * 규칙을 단일 source of truth로 유지해 한국어 stem 비교 정합성 보장.
185
+ *
186
+ * 주의: 이 리스트는 **추출 시점에도 적용**되므로 1글자 suffix를 추가할 때
187
+ * `집중`→`집`, `시도`→`시` 같은 한자어 명사가 깨지지 않도록 극도로 보수적으로
188
+ * 유지한다. 동사 활용형(`리팩토링중`, `배포시`)처럼 매칭 전용 suffix가 필요하면
189
+ * term-matcher의 `KO_VERBAL_SUFFIXES`에 따로 둔다.
190
+ */
191
+ export const KO_SUFFIXES = [
192
+ '했습니다', '있습니다', '합니다', '입니다', '됩니다',
193
+ '에서', '까지', '으로', '하는', '하고', '했다', '된다', '한다',
194
+ '을', '를', '이', '가', '은', '는', '의', '에', '와', '과', '도', '만', '로',
195
+ ];
196
+ export function stripKoSuffix(word) {
197
+ for (const suffix of KO_SUFFIXES) {
198
+ if (word.endsWith(suffix) && word.length > suffix.length) {
199
+ return word.slice(0, -suffix.length);
200
+ }
201
+ }
202
+ return word;
203
+ }
204
+ /** 최대 태그 수 — Jaccard 분모 희석 방지 */
205
+ const MAX_TAGS = 8;
206
+ /**
207
+ * Extract tags from text.
208
+ * Korean 2-char words preserved (e.g. "에러", "배포"), stopwords filtered.
209
+ * English words require 3+ chars, stopwords filtered.
210
+ * Tags capped at MAX_TAGS, ranked by frequency.
211
+ *
212
+ * NOTE on hyphens: this function strips `-` to a space (`api-key` query token
213
+ * becomes `api` and `key` separately). Solution-side compound tags are
214
+ * recovered downstream by `expandCompoundTags`, and query-side bigram
215
+ * recovery is done by `expandQueryBigrams`. Both ship as part of R4-T1
216
+ * (compound-tag tokenizer fix) — see `docs/plans/2026-04-08-t4-bm25-skip-adr.md`
217
+ * "Round 4 candidates" section for the rationale. Changing this regex
218
+ * directly was considered but rejected: it would silently shift the index
219
+ * representation of every existing solution, requiring an index rebuild and
220
+ * a fresh `ROUND3_BASELINE` measurement on every downstream PR.
221
+ */
222
+ export function extractTags(text) {
223
+ const cleaned = text
224
+ .toLowerCase()
225
+ .replace(/[^가-힣a-z0-9\s]/g, ' ');
226
+ const words = cleaned.split(/\s+/).filter(Boolean);
227
+ const freq = new Map();
228
+ for (const w of words) {
229
+ const isKorean = /[가-힣]/.test(w);
230
+ if (isKorean && w.length >= 2) {
231
+ const stem = stripKoSuffix(w);
232
+ if (stem.length >= 2 && !KO_STOPWORDS.has(stem)) {
233
+ freq.set(stem, (freq.get(stem) ?? 0) + 1);
234
+ }
235
+ }
236
+ else if (!isKorean && w.length > 2 && !EN_STOPWORDS.has(w)) {
237
+ freq.set(w, (freq.get(w) ?? 0) + 1);
238
+ }
239
+ }
240
+ // 빈도 높은 순으로 MAX_TAGS개만 반환
241
+ return [...freq.entries()]
242
+ .sort((a, b) => b[1] - a[1])
243
+ .slice(0, MAX_TAGS)
244
+ .map(([tag]) => tag);
245
+ }
246
+ // ── Compound-tag expansion (R4-T1) ──
247
+ //
248
+ // The matcher's tag intersection step compares solution.tags directly
249
+ // against the (expanded) query tag set. Hyphenated solution tags like
250
+ // `api-key`, `code-review`, `red-green-refactor` only intersect literal
251
+ // query tokens that contain the hyphen — but `extractTags` strips hyphens
252
+ // from query input, so a query "api keys" produces ['api', 'keys'] and
253
+ // never reaches the compound `api-key` tag via direct intersection. The
254
+ // existing `partialMatches` substring rule catches some of these at half
255
+ // weight, but the half-weight discount + a Jaccard denominator that doesn't
256
+ // know about the compound structure means the right solution still loses
257
+ // to a competitor that has a generic single-word match (`api`,
258
+ // `code`, `function`).
259
+ //
260
+ // R4-T1 fixes this from BOTH sides:
261
+ // - solution-side: `expandCompoundTags` returns the raw tags PLUS the
262
+ // hyphen-split parts (≥3 chars each), so `api-key` indexes as
263
+ // {api-key, api, key}. The compound tag stays in the set so existing
264
+ // literal hits keep working.
265
+ // - query-side: `expandQueryBigrams` adds adjacent-token compounds and
266
+ // a singular-stem variant (`api keys` → +{api-key, apikey, api-keys,
267
+ // apikeys}), so the compound tag still wins via direct intersection
268
+ // even after the query lost its hyphen during extraction.
269
+ //
270
+ // Both helpers are intentionally lossless additions on top of the raw
271
+ // tag/token list — callers that pass these into `calculateRelevance` MUST
272
+ // keep the RAW tags for the Jaccard union denominator (otherwise the
273
+ // expanded set inflates the union and silently shifts the score). The
274
+ // `solutionTagsExpanded` option on `calculateRelevance` enforces this
275
+ // separation: matching uses expanded, normalization uses raw.
276
+ /** Minimum length of a hyphen-split part to be kept as an alternative tag. */
277
+ const COMPOUND_PART_MIN_LENGTH = 3;
278
+ /**
279
+ * Expand a solution tag list with hyphen-split alternatives.
280
+ *
281
+ * Each input tag is preserved verbatim, and any tag containing `-` also
282
+ * contributes its parts (length ≥ 3 each) as additional tags. The output
283
+ * is deduplicated.
284
+ *
285
+ * Examples:
286
+ * - `['api-key', 'security']` → `['api-key', 'api', 'key', 'security']`
287
+ * - `['code-review', 'quality']` → `['code-review', 'code', 'review', 'quality']`
288
+ * - `['n+1', 'database']` → `['n+1', 'database']` (no hyphen, n+1 unchanged)
289
+ * - `['red-green-refactor']` → `['red-green-refactor', 'red', 'green', 'refactor']`
290
+ * - `['typescript']` → `['typescript']` (no hyphen, no expansion)
291
+ *
292
+ * Korean compound tags (`API에러`, `테스트주도개발`) are preserved verbatim
293
+ * because they contain no `-`. The expansion is intentionally English-
294
+ * compound-aware only — Korean compound recovery is not in scope for R4-T1
295
+ * (the existing `term-normalizer` family expansion handles Korean ↔ English
296
+ * cross-mapping).
297
+ *
298
+ * The output ordering is insertion order: original tags first, then split
299
+ * parts in left-to-right order. Stable across runs (Set + Array dedup).
300
+ */
301
+ export function expandCompoundTags(tags) {
302
+ const out = new Set();
303
+ for (const t of tags) {
304
+ out.add(t);
305
+ if (t.includes('-')) {
306
+ for (const part of t.split('-')) {
307
+ if (part.length >= COMPOUND_PART_MIN_LENGTH) {
308
+ out.add(part);
309
+ }
310
+ }
311
+ }
312
+ }
313
+ return [...out];
314
+ }
315
+ /**
316
+ * Expand a query tag list with adjacent-token bigram alternatives.
317
+ *
318
+ * For each adjacent (a, b) pair where both tokens are length ≥ 3, the
319
+ * function adds:
320
+ * - `a-b` (hyphen-joined form, e.g. `api-key`)
321
+ * - `ab` (concatenated form, e.g. `apikey`)
322
+ * - `a-b'` (singular stem of b, only if b ends in `s` and length > 3)
323
+ * - `ab'` (concatenated singular stem)
324
+ *
325
+ * Examples:
326
+ * - `['api', 'keys']` → `['api', 'keys', 'api-key', 'apikey', 'api-keys', 'apikeys']`
327
+ * - `['code', 'review']` → `['code', 'review', 'code-review', 'codereview']`
328
+ * - `['red', 'green', 'refactor']` → `[..., 'red-green', 'redgreen', 'green-refactor', 'greenrefactor']`
329
+ *
330
+ * Plural→singular stem is intentionally minimal: only `s`-suffix removal,
331
+ * no `es`/`ies` handling. The cost-benefit is asymmetric — `apis → api`
332
+ * is the highest-value case and is handled correctly; `classes → classe`
333
+ * is wrong but doesn't matter because no solution tag is `classe`.
334
+ *
335
+ * Why both `-` and concatenated forms: solution tag conventions vary
336
+ * across packs (`api-key` vs `apikey`), and this expansion is cheap.
337
+ * The downstream intersection check is O(M) per solution where M = expanded
338
+ * query tag count, so even doubling the query tag count is well within
339
+ * the matcher's hot-path budget for the corpus sizes Forgen targets
340
+ * (N ≤ 200 solutions).
341
+ *
342
+ * Korean tokens (`/[가-힣]/`) are passed through verbatim: bigram
343
+ * concatenation of Korean compound words is meaningless because the
344
+ * boundary is lexical, not whitespace-driven (`디버깅` is one word, not
345
+ * two adjacent tokens). Only ASCII-letter pairs participate.
346
+ */
347
+ export function expandQueryBigrams(tags) {
348
+ const out = new Set(tags);
349
+ for (let i = 0; i < tags.length - 1; i++) {
350
+ const a = tags[i];
351
+ const b = tags[i + 1];
352
+ if (a.length < 3 || b.length < 3)
353
+ continue;
354
+ // ASCII-only filter — Korean bigrams are not meaningful (see header).
355
+ if (!/^[a-z0-9]+$/.test(a) || !/^[a-z0-9]+$/.test(b))
356
+ continue;
357
+ out.add(`${a}-${b}`);
358
+ out.add(`${a}${b}`);
359
+ if (b.endsWith('s') && b.length > 3) {
360
+ const bSing = b.slice(0, -1);
361
+ out.add(`${a}-${bSing}`);
362
+ out.add(`${a}${bSing}`);
363
+ }
364
+ }
365
+ return [...out];
366
+ }
367
+ // ── Migration ──
368
+ const V1_TYPE_MAP = {
369
+ solution: 'pattern',
370
+ rule: 'decision',
371
+ convention: 'decision',
372
+ pattern: 'pattern',
373
+ };
374
+ /** Migrate a V1-format solution file to V3 format */
375
+ export function migrateV1toV3(content, filePath) {
376
+ const lines = content.split('\n');
377
+ let title = '';
378
+ let v1Type = '';
379
+ let scope = 'me';
380
+ let bodyStartIdx = 0;
381
+ for (let i = 0; i < lines.length; i++) {
382
+ const line = lines[i];
383
+ if (!title && line.startsWith('# ')) {
384
+ title = line.replace(/^#\s+/, '').trim();
385
+ bodyStartIdx = i + 1;
386
+ }
387
+ if (line.startsWith('> Type:')) {
388
+ v1Type = line.replace('> Type:', '').trim().toLowerCase();
389
+ bodyStartIdx = Math.max(bodyStartIdx, i + 1);
390
+ }
391
+ if (line.startsWith('> Scope:')) {
392
+ const rawScope = line.replace('> Scope:', '').trim().toLowerCase();
393
+ scope = rawScope === 'project' ? 'project' : 'me';
394
+ bodyStartIdx = Math.max(bodyStartIdx, i + 1);
395
+ }
396
+ }
397
+ // Skip remaining metadata lines (> Classification:, > Created:, blank lines right after)
398
+ while (bodyStartIdx < lines.length) {
399
+ const l = lines[bodyStartIdx].trim();
400
+ if (l.startsWith('>') || l === '') {
401
+ bodyStartIdx++;
402
+ }
403
+ else {
404
+ break;
405
+ }
406
+ }
407
+ const body = lines.slice(bodyStartIdx).join('\n').trim();
408
+ const today = new Date().toISOString().split('T')[0];
409
+ const name = slugify(title || filePath);
410
+ const type = V1_TYPE_MAP[v1Type] ?? 'pattern';
411
+ const tags = extractTags(`${title} ${body}`);
412
+ const solution = {
413
+ frontmatter: {
414
+ name,
415
+ version: 1,
416
+ status: 'candidate',
417
+ confidence: 0.5,
418
+ type,
419
+ scope,
420
+ tags,
421
+ identifiers: [],
422
+ evidence: { ...DEFAULT_EVIDENCE },
423
+ created: today,
424
+ updated: today,
425
+ supersedes: null,
426
+ extractedBy: 'auto',
427
+ },
428
+ context: '',
429
+ content: body,
430
+ };
431
+ return serializeSolutionV3(solution);
432
+ }
@@ -0,0 +1,13 @@
1
+ import type { SolutionIndexEntry } from './solution-format.js';
2
+ export interface SolutionDirConfig {
3
+ dir: string;
4
+ scope: 'me' | 'team' | 'project';
5
+ }
6
+ export interface SolutionIndex {
7
+ entries: SolutionIndexEntry[];
8
+ directoryMtimes: Record<string, number>;
9
+ builtAt: number;
10
+ }
11
+ export declare function isIndexStale(index: SolutionIndex): boolean;
12
+ export declare function getOrBuildIndex(dirs: SolutionDirConfig[]): SolutionIndex;
13
+ export declare function resetIndexCache(): void;