@wooojin/forgen 0.2.1 → 0.3.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/CHANGELOG.md +44 -0
- package/README.ko.md +25 -14
- package/README.md +61 -17
- package/agents/analyst.md +48 -4
- package/agents/architect.md +39 -4
- package/agents/code-reviewer.md +107 -77
- package/agents/critic.md +47 -4
- package/agents/debugger.md +46 -4
- package/agents/designer.md +40 -4
- package/agents/executor.md +112 -30
- package/agents/explore.md +45 -5
- package/agents/git-master.md +48 -4
- package/agents/planner.md +121 -18
- package/agents/test-engineer.md +58 -4
- package/agents/verifier.md +92 -77
- package/commands/architecture-decision.md +127 -258
- package/commands/calibrate.md +225 -0
- package/commands/code-review.md +163 -178
- package/commands/compound.md +127 -68
- package/commands/deep-interview.md +212 -110
- package/commands/docker.md +68 -178
- package/commands/forge-loop.md +215 -0
- package/commands/learn.md +231 -0
- package/commands/retro.md +215 -0
- package/commands/ship.md +277 -0
- package/dist/cli.js +17 -9
- package/dist/core/auto-compound-runner.js +14 -0
- package/dist/core/config-injector.d.ts +2 -1
- package/dist/core/config-injector.js +2 -1
- package/dist/core/dashboard.d.ts +17 -0
- package/dist/core/dashboard.js +112 -2
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/paths.d.ts +6 -1
- package/dist/core/paths.js +18 -2
- package/dist/core/spawn.d.ts +3 -2
- package/dist/core/spawn.js +27 -8
- package/dist/core/types.d.ts +34 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
- package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
- package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
- package/dist/engine/meta-learning/extraction-tuner.js +99 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
- package/dist/engine/meta-learning/runner.d.ts +14 -0
- package/dist/engine/meta-learning/runner.js +90 -0
- package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
- package/dist/engine/meta-learning/scope-promoter.js +84 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
- package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
- package/dist/engine/meta-learning/types.d.ts +114 -0
- package/dist/engine/meta-learning/types.js +43 -0
- package/dist/engine/solution-format.d.ts +2 -2
- package/dist/engine/solution-format.js +249 -34
- package/dist/engine/solution-index.d.ts +1 -1
- package/dist/engine/solution-matcher.d.ts +7 -1
- package/dist/engine/solution-matcher.js +114 -37
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +5 -0
- package/dist/hooks/context-guard.js +118 -2
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/keyword-detector.js +16 -100
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +6 -4
- package/dist/host/codex-adapter.d.ts +10 -0
- package/dist/host/codex-adapter.js +154 -0
- package/dist/mcp/solution-reader.d.ts +5 -5
- package/dist/mcp/solution-reader.js +34 -24
- package/dist/services/session.d.ts +19 -0
- package/dist/services/session.js +62 -0
- package/hooks/hooks.json +2 -2
- package/package.json +2 -1
- package/skills/architecture-decision/SKILL.md +113 -257
- package/skills/calibrate/SKILL.md +207 -0
- package/skills/code-review/SKILL.md +151 -178
- package/skills/compound/SKILL.md +126 -68
- package/skills/deep-interview/SKILL.md +210 -110
- package/skills/docker/SKILL.md +57 -179
- package/skills/forge-loop/SKILL.md +198 -0
- package/skills/learn/SKILL.md +216 -0
- package/skills/retro/SKILL.md +199 -0
- package/skills/ship/SKILL.md +259 -0
- package/agents/code-simplifier.md +0 -197
- package/agents/performance-reviewer.md +0 -172
- package/agents/qa-tester.md +0 -158
- package/agents/refactoring-expert.md +0 -168
- package/agents/scientist.md +0 -144
- package/agents/security-reviewer.md +0 -137
- package/agents/writer.md +0 -184
- package/commands/api-design.md +0 -268
- package/commands/ci-cd.md +0 -270
- package/commands/database.md +0 -263
- package/commands/debug-detective.md +0 -99
- package/commands/documentation.md +0 -276
- package/commands/ecomode.md +0 -51
- package/commands/frontend.md +0 -271
- package/commands/git-master.md +0 -90
- package/commands/incident-response.md +0 -292
- package/commands/migrate.md +0 -101
- package/commands/performance.md +0 -288
- package/commands/refactor.md +0 -105
- package/commands/security-review.md +0 -288
- package/commands/specify.md +0 -128
- package/commands/tdd.md +0 -183
- package/commands/testing-strategy.md +0 -265
- package/skills/api-design/SKILL.md +0 -262
- package/skills/ci-cd/SKILL.md +0 -264
- package/skills/database/SKILL.md +0 -257
- package/skills/debug-detective/SKILL.md +0 -95
- package/skills/documentation/SKILL.md +0 -270
- package/skills/ecomode/SKILL.md +0 -46
- package/skills/frontend/SKILL.md +0 -265
- package/skills/git-master/SKILL.md +0 -86
- package/skills/incident-response/SKILL.md +0 -286
- package/skills/migrate/SKILL.md +0 -96
- package/skills/performance/SKILL.md +0 -282
- package/skills/refactor/SKILL.md +0 -100
- package/skills/security-review/SKILL.md +0 -282
- package/skills/specify/SKILL.md +0 -122
- package/skills/tdd/SKILL.md +0 -178
- package/skills/testing-strategy/SKILL.md +0 -260
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import yaml from 'js-yaml';
|
|
2
2
|
export const DEFAULT_EVIDENCE = {
|
|
3
|
-
injected: 0,
|
|
3
|
+
injected: 0,
|
|
4
|
+
reflected: 0,
|
|
5
|
+
negative: 0,
|
|
6
|
+
sessions: 0,
|
|
7
|
+
reExtracted: 0,
|
|
4
8
|
};
|
|
5
|
-
const VALID_STATUSES = [
|
|
6
|
-
|
|
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
|
|
@@ -31,7 +48,7 @@ export function validateFrontmatter(fm) {
|
|
|
31
48
|
return false;
|
|
32
49
|
if (typeof o.type !== 'string' || !VALID_TYPES.includes(o.type))
|
|
33
50
|
return false;
|
|
34
|
-
if (o.scope !== 'me' && o.scope !== 'team' && o.scope !== 'project')
|
|
51
|
+
if (o.scope !== 'me' && o.scope !== 'team' && o.scope !== 'project' && o.scope !== 'universal')
|
|
35
52
|
return false;
|
|
36
53
|
if (!Array.isArray(o.tags) || !o.tags.every((t) => typeof t === 'string'))
|
|
37
54
|
return false;
|
|
@@ -122,7 +139,11 @@ export function parseSolutionV3(content) {
|
|
|
122
139
|
// ── Serialization ──
|
|
123
140
|
/** Serialize a SolutionV3 to a markdown string with YAML frontmatter */
|
|
124
141
|
export function serializeSolutionV3(solution) {
|
|
125
|
-
const yamlStr = yaml.dump(solution.frontmatter, {
|
|
142
|
+
const yamlStr = yaml.dump(solution.frontmatter, {
|
|
143
|
+
lineWidth: -1,
|
|
144
|
+
quotingType: '"',
|
|
145
|
+
schema: yaml.JSON_SCHEMA,
|
|
146
|
+
});
|
|
126
147
|
return `---\n${yamlStr}---\n\n## Context\n${solution.context}\n\n## Content\n${solution.content}\n`;
|
|
127
148
|
}
|
|
128
149
|
// ── Format Detection ──
|
|
@@ -149,34 +170,207 @@ export function isV1Format(content) {
|
|
|
149
170
|
/** 한국어 불용어 — 태그로 의미 없는 일반 단어 */
|
|
150
171
|
const KO_STOPWORDS = new Set([
|
|
151
172
|
// 일반 불용어
|
|
152
|
-
'적용',
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
173
|
+
'적용',
|
|
174
|
+
'패턴',
|
|
175
|
+
'모든',
|
|
176
|
+
'같은',
|
|
177
|
+
'발견',
|
|
178
|
+
'다른',
|
|
179
|
+
'사용',
|
|
180
|
+
'경우',
|
|
181
|
+
'위해',
|
|
182
|
+
'통해',
|
|
183
|
+
'대한',
|
|
184
|
+
'이후',
|
|
185
|
+
'때문',
|
|
186
|
+
'하는',
|
|
187
|
+
'있는',
|
|
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
|
+
'필요',
|
|
160
245
|
// 조사/어미/접속사 — Jaccard 분모 희석 방지
|
|
161
|
-
'에서',
|
|
162
|
-
'
|
|
163
|
-
'
|
|
164
|
-
'
|
|
165
|
-
'
|
|
166
|
-
'
|
|
246
|
+
'에서',
|
|
247
|
+
'으로',
|
|
248
|
+
'에게',
|
|
249
|
+
'에는',
|
|
250
|
+
'에도',
|
|
251
|
+
'까지',
|
|
252
|
+
'부터',
|
|
253
|
+
'보다',
|
|
254
|
+
'처럼',
|
|
255
|
+
'만큼',
|
|
256
|
+
'대로',
|
|
257
|
+
'밖에',
|
|
258
|
+
'뿐만',
|
|
259
|
+
'이나',
|
|
260
|
+
'이고',
|
|
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
|
+
'해야',
|
|
167
296
|
// 일반 동사/형용사 어간 — 의미 없는 고빈도 단어
|
|
168
|
-
'가능',
|
|
169
|
-
'
|
|
170
|
-
'
|
|
297
|
+
'가능',
|
|
298
|
+
'상태',
|
|
299
|
+
'이유',
|
|
300
|
+
'방지',
|
|
301
|
+
'의존',
|
|
302
|
+
'의존성',
|
|
303
|
+
'즉시',
|
|
304
|
+
'원칙',
|
|
305
|
+
'근거',
|
|
306
|
+
'수정',
|
|
307
|
+
'제안',
|
|
308
|
+
'기능',
|
|
309
|
+
'구현',
|
|
310
|
+
'구조',
|
|
311
|
+
'단계',
|
|
312
|
+
'목적',
|
|
313
|
+
'상황',
|
|
314
|
+
'조건',
|
|
315
|
+
'규칙',
|
|
316
|
+
'동작',
|
|
317
|
+
'활성',
|
|
318
|
+
'비활성',
|
|
319
|
+
'원래',
|
|
320
|
+
'현재',
|
|
321
|
+
'이전',
|
|
322
|
+
'다음',
|
|
323
|
+
'최종',
|
|
171
324
|
]);
|
|
172
325
|
/** 영어 불용어 */
|
|
173
326
|
const EN_STOPWORDS = new Set([
|
|
174
|
-
'the',
|
|
175
|
-
'
|
|
176
|
-
'
|
|
177
|
-
'
|
|
178
|
-
'
|
|
179
|
-
'
|
|
327
|
+
'the',
|
|
328
|
+
'and',
|
|
329
|
+
'for',
|
|
330
|
+
'that',
|
|
331
|
+
'this',
|
|
332
|
+
'with',
|
|
333
|
+
'from',
|
|
334
|
+
'are',
|
|
335
|
+
'was',
|
|
336
|
+
'were',
|
|
337
|
+
'been',
|
|
338
|
+
'have',
|
|
339
|
+
'has',
|
|
340
|
+
'had',
|
|
341
|
+
'not',
|
|
342
|
+
'but',
|
|
343
|
+
'all',
|
|
344
|
+
'can',
|
|
345
|
+
'will',
|
|
346
|
+
'use',
|
|
347
|
+
'used',
|
|
348
|
+
'using',
|
|
349
|
+
'when',
|
|
350
|
+
'each',
|
|
351
|
+
'which',
|
|
352
|
+
'their',
|
|
353
|
+
'also',
|
|
354
|
+
'into',
|
|
355
|
+
'more',
|
|
356
|
+
'some',
|
|
357
|
+
'than',
|
|
358
|
+
'other',
|
|
359
|
+
'should',
|
|
360
|
+
'would',
|
|
361
|
+
'could',
|
|
362
|
+
'about',
|
|
363
|
+
'after',
|
|
364
|
+
'before',
|
|
365
|
+
'between',
|
|
366
|
+
'does',
|
|
367
|
+
'only',
|
|
368
|
+
'across',
|
|
369
|
+
'just',
|
|
370
|
+
'detected',
|
|
371
|
+
'based',
|
|
372
|
+
'sessions',
|
|
373
|
+
'prompts',
|
|
180
374
|
]);
|
|
181
375
|
/** 한국어 일반 조사/어미 — strip 대상 (긴 것부터 매칭)
|
|
182
376
|
*
|
|
@@ -189,9 +383,32 @@ const EN_STOPWORDS = new Set([
|
|
|
189
383
|
* term-matcher의 `KO_VERBAL_SUFFIXES`에 따로 둔다.
|
|
190
384
|
*/
|
|
191
385
|
export const KO_SUFFIXES = [
|
|
192
|
-
'했습니다',
|
|
193
|
-
'
|
|
194
|
-
'
|
|
386
|
+
'했습니다',
|
|
387
|
+
'있습니다',
|
|
388
|
+
'합니다',
|
|
389
|
+
'입니다',
|
|
390
|
+
'됩니다',
|
|
391
|
+
'에서',
|
|
392
|
+
'까지',
|
|
393
|
+
'으로',
|
|
394
|
+
'하는',
|
|
395
|
+
'하고',
|
|
396
|
+
'했다',
|
|
397
|
+
'된다',
|
|
398
|
+
'한다',
|
|
399
|
+
'을',
|
|
400
|
+
'를',
|
|
401
|
+
'이',
|
|
402
|
+
'가',
|
|
403
|
+
'은',
|
|
404
|
+
'는',
|
|
405
|
+
'의',
|
|
406
|
+
'에',
|
|
407
|
+
'와',
|
|
408
|
+
'과',
|
|
409
|
+
'도',
|
|
410
|
+
'만',
|
|
411
|
+
'로',
|
|
195
412
|
];
|
|
196
413
|
export function stripKoSuffix(word) {
|
|
197
414
|
for (const suffix of KO_SUFFIXES) {
|
|
@@ -220,9 +437,7 @@ const MAX_TAGS = 8;
|
|
|
220
437
|
* a fresh `ROUND3_BASELINE` measurement on every downstream PR.
|
|
221
438
|
*/
|
|
222
439
|
export function extractTags(text) {
|
|
223
|
-
const cleaned = text
|
|
224
|
-
.toLowerCase()
|
|
225
|
-
.replace(/[^가-힣a-z0-9\s]/g, ' ');
|
|
440
|
+
const cleaned = text.toLowerCase().replace(/[^가-힣a-z0-9\s]/g, ' ');
|
|
226
441
|
const words = cleaned.split(/\s+/).filter(Boolean);
|
|
227
442
|
const freq = new Map();
|
|
228
443
|
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[];
|
|
@@ -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;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
1
2
|
import * as path from 'node:path';
|
|
2
|
-
import { ME_SOLUTIONS, PACKS_DIR } from '../core/paths.js';
|
|
3
|
-
import {
|
|
3
|
+
import { ME_SOLUTIONS, META_LEARNING_DIR, PACKS_DIR } from '../core/paths.js';
|
|
4
|
+
import { maskBlockedTokens } from './phrase-blocklist.js';
|
|
5
|
+
import { expandCompoundTags, expandQueryBigrams, extractTags } from './solution-format.js';
|
|
4
6
|
import { getOrBuildIndex } from './solution-index.js';
|
|
5
7
|
import { defaultNormalizer } from './term-normalizer.js';
|
|
6
|
-
import { maskBlockedTokens } from './phrase-blocklist.js';
|
|
7
8
|
// ── Synonym expansion (delegates to term-normalizer) ──
|
|
8
9
|
//
|
|
9
10
|
// The old `SYNONYM_MAP` + `expandTagsWithSynonyms` pair had two problems:
|
|
@@ -87,7 +88,7 @@ export function bm25Score(queryTags, docTags, avgDocLength) {
|
|
|
87
88
|
let score = 0;
|
|
88
89
|
for (const qt of queryTags) {
|
|
89
90
|
// Term frequency in document
|
|
90
|
-
const tf = docTags.filter(dt => dt === qt || (dt.length > 3 && qt.length > 3 && (dt.includes(qt) || qt.includes(dt)))).length;
|
|
91
|
+
const tf = docTags.filter((dt) => dt === qt || (dt.length > 3 && qt.length > 3 && (dt.includes(qt) || qt.includes(dt)))).length;
|
|
91
92
|
if (tf === 0)
|
|
92
93
|
continue;
|
|
93
94
|
// BM25 TF saturation
|
|
@@ -100,10 +101,37 @@ export function bm25Score(queryTags, docTags, avgDocLength) {
|
|
|
100
101
|
}
|
|
101
102
|
/** High-frequency tags that should be weighted lower */
|
|
102
103
|
const COMMON_TAGS = new Set([
|
|
103
|
-
'typescript',
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
'
|
|
104
|
+
'typescript',
|
|
105
|
+
'ts',
|
|
106
|
+
'javascript',
|
|
107
|
+
'js',
|
|
108
|
+
'fix',
|
|
109
|
+
'update',
|
|
110
|
+
'add',
|
|
111
|
+
'change',
|
|
112
|
+
'file',
|
|
113
|
+
'code',
|
|
114
|
+
'function',
|
|
115
|
+
'import',
|
|
116
|
+
'export',
|
|
117
|
+
'error',
|
|
118
|
+
'type',
|
|
119
|
+
'string',
|
|
120
|
+
'number',
|
|
121
|
+
'object',
|
|
122
|
+
'array',
|
|
123
|
+
'return',
|
|
124
|
+
'const',
|
|
125
|
+
'class',
|
|
126
|
+
'module',
|
|
127
|
+
'코드',
|
|
128
|
+
'파일',
|
|
129
|
+
'함수',
|
|
130
|
+
'수정',
|
|
131
|
+
'추가',
|
|
132
|
+
'변경',
|
|
133
|
+
'에러',
|
|
134
|
+
'타입',
|
|
107
135
|
]);
|
|
108
136
|
/** Apply IDF-like weight: common tags get reduced weight */
|
|
109
137
|
export function tagWeight(tag) {
|
|
@@ -114,7 +142,7 @@ export function calculateRelevance(promptOrTags, keywordsOrTags, confidence, opt
|
|
|
114
142
|
// Legacy mode: substring matching for backwards compatibility.
|
|
115
143
|
// Not a hot path — only hit by the (old) solution-matcher.test.ts cases.
|
|
116
144
|
const promptTags = extractTags(promptOrTags);
|
|
117
|
-
const intersection = keywordsOrTags.filter(kw => promptTags.some(pt => pt === kw || (pt.length > 3 && kw.length > 3 && (pt.startsWith(kw) || kw.startsWith(pt)))));
|
|
145
|
+
const intersection = keywordsOrTags.filter((kw) => promptTags.some((pt) => pt === kw || (pt.length > 3 && kw.length > 3 && (pt.startsWith(kw) || kw.startsWith(pt)))));
|
|
118
146
|
return Math.min(1, intersection.length / Math.max(promptTags.length * 0.5, 1));
|
|
119
147
|
}
|
|
120
148
|
// v3 mode: tag matching with synonym expansion + TF-IDF weighting.
|
|
@@ -124,21 +152,21 @@ export function calculateRelevance(promptOrTags, keywordsOrTags, confidence, opt
|
|
|
124
152
|
// the hot path pre-compute the expansion once per query and pass it via
|
|
125
153
|
// `options.normalizedPromptTags`, so this function no longer repeats the
|
|
126
154
|
// work per solution.
|
|
127
|
-
const expandedPromptTags = options?.normalizedPromptTags
|
|
128
|
-
?? defaultNormalizer.normalizeTerms(promptOrTags);
|
|
155
|
+
const expandedPromptTags = options?.normalizedPromptTags ?? defaultNormalizer.normalizeTerms(promptOrTags);
|
|
129
156
|
// R4-T1: when the caller supplies a compound-expanded solution tag set,
|
|
130
157
|
// intersection and partial matching run against the expanded set (so
|
|
131
158
|
// `api-key` matches `api`/`key` queries via the split parts), but the
|
|
132
159
|
// Jaccard union denominator below still uses the RAW `keywordsOrTags`
|
|
133
160
|
// for normalization stability.
|
|
134
161
|
const matchTags = options?.solutionTagsExpanded ?? keywordsOrTags;
|
|
135
|
-
const intersection = matchTags.filter(t => expandedPromptTags.includes(t));
|
|
162
|
+
const intersection = matchTags.filter((t) => expandedPromptTags.includes(t));
|
|
136
163
|
// partial/substring matches for longer tags (>3 chars)
|
|
137
|
-
const partialMatches = matchTags.filter(t => t.length > 3 &&
|
|
138
|
-
|
|
164
|
+
const partialMatches = matchTags.filter((t) => t.length > 3 &&
|
|
165
|
+
!intersection.includes(t) &&
|
|
166
|
+
expandedPromptTags.some((pt) => pt.length > 3 && (pt.includes(t) || t.includes(pt))));
|
|
139
167
|
// Apply TF-IDF weighting: common tags count less
|
|
140
|
-
const weightedMatched = intersection.reduce((sum, t) => sum + tagWeight(t), 0)
|
|
141
|
-
|
|
168
|
+
const weightedMatched = intersection.reduce((sum, t) => sum + tagWeight(t), 0) +
|
|
169
|
+
partialMatches.reduce((sum, t) => sum + tagWeight(t) * 0.5, 0);
|
|
142
170
|
// ── Bigram similarity boost for borderline cases ──
|
|
143
171
|
//
|
|
144
172
|
// When the TF-IDF intersection score is below the match threshold (0.5),
|
|
@@ -176,7 +204,11 @@ export function calculateRelevance(promptOrTags, keywordsOrTags, confidence, opt
|
|
|
176
204
|
const blendedScore = tfidfScore * 0.8 + bestBigramScore * 0.2;
|
|
177
205
|
return {
|
|
178
206
|
relevance: blendedScore * (confidence ?? 1),
|
|
179
|
-
matchedTags: [
|
|
207
|
+
matchedTags: [
|
|
208
|
+
...intersection,
|
|
209
|
+
...partialMatches,
|
|
210
|
+
...bigramMatchedTags.filter((t) => !intersection.includes(t) && !partialMatches.includes(t)),
|
|
211
|
+
],
|
|
180
212
|
};
|
|
181
213
|
}
|
|
182
214
|
return { relevance: 0, matchedTags: [] };
|
|
@@ -196,7 +228,8 @@ export function calculateRelevance(promptOrTags, keywordsOrTags, confidence, opt
|
|
|
196
228
|
bigramBoost = sim;
|
|
197
229
|
}
|
|
198
230
|
}
|
|
199
|
-
const
|
|
231
|
+
const w = options?.ensembleWeights ?? { tfidf: 0.5, bm25: 0.3, bigram: 0.2 };
|
|
232
|
+
const ensembleScore = tfidfScore * w.tfidf + bm25 * w.bm25 + bigramBoost * w.bigram;
|
|
200
233
|
return {
|
|
201
234
|
relevance: ensembleScore * (confidence ?? 1),
|
|
202
235
|
matchedTags: [...intersection, ...partialMatches],
|
|
@@ -274,8 +307,8 @@ export function shouldRejectByR4T3Rules(promptTags, matchedTags) {
|
|
|
274
307
|
// Rule B
|
|
275
308
|
if (matchedTags.length === 1) {
|
|
276
309
|
const tag = matchedTags[0];
|
|
277
|
-
const literalHit = promptTags.includes(tag)
|
|
278
|
-
|
|
310
|
+
const literalHit = promptTags.includes(tag) ||
|
|
311
|
+
promptTags.some((pt) => {
|
|
279
312
|
if (pt.length <= 3 || tag.length <= 3)
|
|
280
313
|
return false;
|
|
281
314
|
if (pt.includes(tag) || tag.includes(pt))
|
|
@@ -310,7 +343,7 @@ export function shouldRejectByR4T3Rules(promptTags, matchedTags) {
|
|
|
310
343
|
* `matchSolutions` behaviour (both scopes could rank). Callers that want
|
|
311
344
|
* first-wins scope precedence must dedupe on their side.
|
|
312
345
|
*/
|
|
313
|
-
function rankCandidates(promptTags, promptLower, solutions) {
|
|
346
|
+
function rankCandidates(promptTags, promptLower, solutions, ensembleWeights) {
|
|
314
347
|
// T2: normalize prompt tags ONCE per query (not once per solution).
|
|
315
348
|
// Pre-T2 this expansion happened inside calculateRelevance and was
|
|
316
349
|
// repeated N times for N solutions — the plan's primary hot-path win.
|
|
@@ -345,7 +378,7 @@ function rankCandidates(promptTags, promptLower, solutions) {
|
|
|
345
378
|
const promptTagsWithBigrams = expandQueryBigrams(maskedPromptTags);
|
|
346
379
|
const normalizedPromptTags = defaultNormalizer.normalizeTerms(promptTagsWithBigrams);
|
|
347
380
|
return solutions
|
|
348
|
-
.map(sol => {
|
|
381
|
+
.map((sol) => {
|
|
349
382
|
// R4-T1: solution-side compound-tag expansion. `api-key` becomes
|
|
350
383
|
// {api-key, api, key} so a query token `api` (from "api keys") hits
|
|
351
384
|
// it directly. Computed per solution because each sol.tags is
|
|
@@ -358,7 +391,11 @@ function rankCandidates(promptTags, promptLower, solutions) {
|
|
|
358
391
|
// step (intersection/partialMatches) already uses the masked set
|
|
359
392
|
// via `normalizedPromptTags` — the union must match for score
|
|
360
393
|
// semantics to stay consistent.
|
|
361
|
-
const result = calculateRelevance(maskedPromptTags, sol.tags, sol.confidence, {
|
|
394
|
+
const result = calculateRelevance(maskedPromptTags, sol.tags, sol.confidence, {
|
|
395
|
+
normalizedPromptTags,
|
|
396
|
+
solutionTagsExpanded: solTagsExpanded,
|
|
397
|
+
ensembleWeights,
|
|
398
|
+
});
|
|
362
399
|
// Compute identifier boost FIRST — independent of tag scoring so
|
|
363
400
|
// R4-T3's tag-evidence precision rules below cannot silently drop
|
|
364
401
|
// a candidate that has strong identifier-level evidence.
|
|
@@ -385,9 +422,9 @@ function rankCandidates(promptTags, promptLower, solutions) {
|
|
|
385
422
|
// the `matchedTags.length + matchedIdentifiers.length >= 1` filter.
|
|
386
423
|
let tagRelevance = result.relevance;
|
|
387
424
|
let tagMatches = result.matchedTags;
|
|
388
|
-
if (matchedIdentifiers.length === 0
|
|
389
|
-
|
|
390
|
-
|
|
425
|
+
if (matchedIdentifiers.length === 0 &&
|
|
426
|
+
tagMatches.length > 0 &&
|
|
427
|
+
shouldRejectByR4T3Rules(maskedPromptTags, tagMatches)) {
|
|
391
428
|
tagRelevance = 0;
|
|
392
429
|
tagMatches = [];
|
|
393
430
|
}
|
|
@@ -398,7 +435,7 @@ function rankCandidates(promptTags, promptLower, solutions) {
|
|
|
398
435
|
matchedIdentifiers,
|
|
399
436
|
};
|
|
400
437
|
})
|
|
401
|
-
.filter(c => c.matchedTags.length + c.matchedIdentifiers.length >= 1)
|
|
438
|
+
.filter((c) => c.matchedTags.length + c.matchedIdentifiers.length >= 1)
|
|
402
439
|
.sort((a, b) => b.relevance - a.relevance)
|
|
403
440
|
.slice(0, 5);
|
|
404
441
|
}
|
|
@@ -675,7 +712,7 @@ function computeBucketMetrics(queries, solutions) {
|
|
|
675
712
|
*/
|
|
676
713
|
export function evaluateQuery(query, solutions) {
|
|
677
714
|
const promptTags = extractTags(query);
|
|
678
|
-
return rankCandidates(promptTags, query.toLowerCase(), solutions).map(c => ({
|
|
715
|
+
return rankCandidates(promptTags, query.toLowerCase(), solutions).map((c) => ({
|
|
679
716
|
name: c.solution.name,
|
|
680
717
|
relevance: c.relevance,
|
|
681
718
|
matchedTags: c.matchedTags,
|
|
@@ -701,13 +738,16 @@ export function evaluateSolutionMatcher(fixture) {
|
|
|
701
738
|
// doesn't drown a small paraphrase bucket but also a single-query bucket
|
|
702
739
|
// doesn't dominate.
|
|
703
740
|
const recallAt5 = combinedTotal > 0
|
|
704
|
-
? (positiveM.recallAt5 * positiveM.total + paraphraseM.recallAt5 * paraphraseM.total) /
|
|
741
|
+
? (positiveM.recallAt5 * positiveM.total + paraphraseM.recallAt5 * paraphraseM.total) /
|
|
742
|
+
combinedTotal
|
|
705
743
|
: 0;
|
|
706
744
|
const mrrAt5 = combinedTotal > 0
|
|
707
|
-
? (positiveM.mrrAt5 * positiveM.total + paraphraseM.mrrAt5 * paraphraseM.total) /
|
|
745
|
+
? (positiveM.mrrAt5 * positiveM.total + paraphraseM.mrrAt5 * paraphraseM.total) /
|
|
746
|
+
combinedTotal
|
|
708
747
|
: 0;
|
|
709
748
|
const noResultRate = combinedTotal > 0
|
|
710
|
-
? (positiveM.noResultRate * positiveM.total + paraphraseM.noResultRate * paraphraseM.total) /
|
|
749
|
+
? (positiveM.noResultRate * positiveM.total + paraphraseM.noResultRate * paraphraseM.total) /
|
|
750
|
+
combinedTotal
|
|
711
751
|
: 0;
|
|
712
752
|
let negAnyResult = 0;
|
|
713
753
|
for (const q of fixture.negative) {
|
|
@@ -733,27 +773,64 @@ export function evaluateSolutionMatcher(fixture) {
|
|
|
733
773
|
},
|
|
734
774
|
};
|
|
735
775
|
}
|
|
776
|
+
// ── Meta-learning: dynamic ensemble weights ──
|
|
777
|
+
let _cachedWeights;
|
|
778
|
+
let _weightsCacheTime = 0;
|
|
779
|
+
const WEIGHTS_CACHE_TTL = 60_000; // 1 minute cache
|
|
780
|
+
/**
|
|
781
|
+
* Load tuned matcher weights from meta-learning state.
|
|
782
|
+
* Returns undefined (use defaults) if no tuned weights exist.
|
|
783
|
+
* Cached for 1 minute to avoid re-reading per matchSolutions call.
|
|
784
|
+
*/
|
|
785
|
+
function loadTunedMatcherWeights() {
|
|
786
|
+
const now = Date.now();
|
|
787
|
+
if (_cachedWeights !== undefined && now - _weightsCacheTime < WEIGHTS_CACHE_TTL) {
|
|
788
|
+
return _cachedWeights ?? undefined;
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
const weightsPath = path.join(META_LEARNING_DIR, 'matcher-weights.json');
|
|
792
|
+
if (!fs.existsSync(weightsPath)) {
|
|
793
|
+
_cachedWeights = null;
|
|
794
|
+
_weightsCacheTime = now;
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
const data = JSON.parse(fs.readFileSync(weightsPath, 'utf-8'));
|
|
798
|
+
if (typeof data.tfidf === 'number' &&
|
|
799
|
+
typeof data.bm25 === 'number' &&
|
|
800
|
+
typeof data.bigram === 'number') {
|
|
801
|
+
_cachedWeights = { tfidf: data.tfidf, bm25: data.bm25, bigram: data.bigram };
|
|
802
|
+
_weightsCacheTime = now;
|
|
803
|
+
return _cachedWeights;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
/* fail-open: use defaults */
|
|
808
|
+
}
|
|
809
|
+
_cachedWeights = null;
|
|
810
|
+
_weightsCacheTime = now;
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
736
813
|
export function matchSolutions(prompt, scope, cwd) {
|
|
737
814
|
// Build solution dirs for index cache
|
|
738
|
-
const dirs = [
|
|
739
|
-
{ dir: ME_SOLUTIONS, scope: 'me' },
|
|
740
|
-
];
|
|
815
|
+
const dirs = [{ dir: ME_SOLUTIONS, scope: 'me' }];
|
|
741
816
|
if (scope.team) {
|
|
742
817
|
dirs.push({ dir: path.join(PACKS_DIR, scope.team.name, 'solutions'), scope: 'team' });
|
|
743
818
|
}
|
|
744
819
|
dirs.push({ dir: path.join(cwd, '.compound', 'solutions'), scope: 'project' });
|
|
745
820
|
// Use cached index (rebuilt only when dirs change)
|
|
746
821
|
const index = getOrBuildIndex(dirs);
|
|
747
|
-
const allSolutions = index.entries.map(e => ({ ...e }));
|
|
822
|
+
const allSolutions = index.entries.map((e) => ({ ...e }));
|
|
748
823
|
const promptTags = extractTags(prompt);
|
|
749
824
|
const promptLower = prompt.toLowerCase();
|
|
825
|
+
// Meta-learning: load tuned weights if available
|
|
826
|
+
const tunedWeights = loadTunedMatcherWeights();
|
|
750
827
|
// Delegate to shared ranking core. `rankCandidates` is generic so each
|
|
751
828
|
// ranked candidate carries the original `LoadedSolution` reference — no
|
|
752
829
|
// name-based re-lookup, so two scopes sharing a name (e.g. me/foo and
|
|
753
830
|
// project/foo) can both appear in the result without a Map last-wins
|
|
754
831
|
// scope-precedence bug.
|
|
755
|
-
const ranked = rankCandidates(promptTags, promptLower, allSolutions);
|
|
756
|
-
return ranked.map(c => ({
|
|
832
|
+
const ranked = rankCandidates(promptTags, promptLower, allSolutions, tunedWeights);
|
|
833
|
+
return ranked.map((c) => ({
|
|
757
834
|
name: c.solution.name,
|
|
758
835
|
path: c.solution.filePath,
|
|
759
836
|
scope: c.solution.scope,
|