@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,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;
|