@wooojin/forgen 0.2.0 → 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 +72 -0
- package/README.ja.md +79 -14
- package/README.ko.md +100 -14
- package/README.md +124 -17
- package/README.zh.md +79 -14
- 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 +273 -0
- 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 +26 -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 +108 -0
- package/dist/core/dashboard.js +495 -0
- package/dist/core/doctor.js +151 -21
- package/dist/core/drift-score.d.ts +49 -0
- package/dist/core/drift-score.js +87 -0
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/mcp-config.d.ts +2 -0
- package/dist/core/mcp-config.js +6 -1
- 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-export.d.ts +41 -0
- package/dist/engine/compound-export.js +169 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/compound-loop.js +18 -0
- 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 +30 -1
- package/dist/engine/solution-matcher.js +235 -45
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +15 -0
- package/dist/hooks/context-guard.js +218 -56
- package/dist/hooks/db-guard.js +2 -2
- package/dist/hooks/hook-config.d.ts +27 -1
- package/dist/hooks/hook-config.js +72 -12
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/intent-classifier.d.ts +0 -2
- package/dist/hooks/intent-classifier.js +32 -18
- package/dist/hooks/keyword-detector.js +126 -204
- package/dist/hooks/notepad-injector.js +2 -2
- package/dist/hooks/permission-handler.js +2 -2
- package/dist/hooks/post-tool-failure.js +12 -6
- package/dist/hooks/post-tool-handlers.d.ts +1 -1
- package/dist/hooks/post-tool-handlers.js +14 -11
- package/dist/hooks/post-tool-use.d.ts +11 -0
- package/dist/hooks/post-tool-use.js +184 -71
- package/dist/hooks/pre-compact.d.ts +11 -1
- package/dist/hooks/pre-compact.js +112 -37
- package/dist/hooks/pre-tool-use.js +86 -56
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/session-recovery.js +256 -236
- package/dist/hooks/shared/hook-response.d.ts +4 -4
- package/dist/hooks/shared/hook-response.js +13 -24
- package/dist/hooks/shared/hook-timing.d.ts +15 -0
- package/dist/hooks/shared/hook-timing.js +64 -0
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +47 -16
- package/dist/hooks/slop-detector.js +3 -3
- package/dist/hooks/solution-injector.js +224 -197
- package/dist/hooks/subagent-tracker.js +2 -2
- 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/renderer/rule-renderer.js +9 -11
- 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 +266 -0
- 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/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/tdd/SKILL.md +0 -178
- package/skills/testing-strategy/SKILL.md +0 -260
|
@@ -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:
|
|
@@ -31,12 +32,106 @@ export function expandTagsWithSynonyms(tags) {
|
|
|
31
32
|
return defaultNormalizer.normalizeTerms(tags);
|
|
32
33
|
}
|
|
33
34
|
// ── TF-IDF weighting for common tags ──
|
|
35
|
+
// ── Character bigram similarity (Dice coefficient) ──
|
|
36
|
+
/**
|
|
37
|
+
* Compute the Dice coefficient between two strings using character bigrams.
|
|
38
|
+
*
|
|
39
|
+
* Dice = 2 * |intersection| / (|A| + |B|)
|
|
40
|
+
*
|
|
41
|
+
* Both strings are lowercased and whitespace-stripped before bigram generation.
|
|
42
|
+
* Returns 0 for empty strings or single-character strings (no bigrams possible).
|
|
43
|
+
* Returns 1.0 for identical non-trivial strings.
|
|
44
|
+
*
|
|
45
|
+
* This is used as a lightweight fuzzy matching signal for borderline cases
|
|
46
|
+
* where the TF-IDF tag intersection produces a low score but the query and
|
|
47
|
+
* solution tags are character-similar (e.g., "database" vs "데이터베이스"
|
|
48
|
+
* won't match, but "database" vs "databse" will get a high score).
|
|
49
|
+
*/
|
|
50
|
+
export function bigramSimilarity(a, b) {
|
|
51
|
+
const na = a.toLowerCase().replace(/\s+/g, '');
|
|
52
|
+
const nb = b.toLowerCase().replace(/\s+/g, '');
|
|
53
|
+
if (na.length < 2 || nb.length < 2)
|
|
54
|
+
return 0;
|
|
55
|
+
if (na === nb)
|
|
56
|
+
return 1.0;
|
|
57
|
+
const bigramsA = new Map();
|
|
58
|
+
for (let i = 0; i < na.length - 1; i++) {
|
|
59
|
+
const bg = na.slice(i, i + 2);
|
|
60
|
+
bigramsA.set(bg, (bigramsA.get(bg) ?? 0) + 1);
|
|
61
|
+
}
|
|
62
|
+
const bigramsB = new Map();
|
|
63
|
+
for (let i = 0; i < nb.length - 1; i++) {
|
|
64
|
+
const bg = nb.slice(i, i + 2);
|
|
65
|
+
bigramsB.set(bg, (bigramsB.get(bg) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
let intersectionSize = 0;
|
|
68
|
+
for (const [bg, countA] of bigramsA) {
|
|
69
|
+
const countB = bigramsB.get(bg) ?? 0;
|
|
70
|
+
intersectionSize += Math.min(countA, countB);
|
|
71
|
+
}
|
|
72
|
+
const totalA = na.length - 1;
|
|
73
|
+
const totalB = nb.length - 1;
|
|
74
|
+
return (2 * intersectionSize) / (totalA + totalB);
|
|
75
|
+
}
|
|
76
|
+
// ── BM25-like scoring ──
|
|
77
|
+
/**
|
|
78
|
+
* Simplified BM25 score for a single query-document pair.
|
|
79
|
+
* Uses tag overlap with term frequency normalization.
|
|
80
|
+
* k1=1.2, b=0.75 (standard BM25 parameters).
|
|
81
|
+
*/
|
|
82
|
+
export function bm25Score(queryTags, docTags, avgDocLength) {
|
|
83
|
+
const k1 = 1.2;
|
|
84
|
+
const b = 0.75;
|
|
85
|
+
const docLen = docTags.length;
|
|
86
|
+
if (docLen === 0 || queryTags.length === 0 || avgDocLength === 0)
|
|
87
|
+
return 0;
|
|
88
|
+
let score = 0;
|
|
89
|
+
for (const qt of queryTags) {
|
|
90
|
+
// Term frequency in document
|
|
91
|
+
const tf = docTags.filter((dt) => dt === qt || (dt.length > 3 && qt.length > 3 && (dt.includes(qt) || qt.includes(dt)))).length;
|
|
92
|
+
if (tf === 0)
|
|
93
|
+
continue;
|
|
94
|
+
// BM25 TF saturation
|
|
95
|
+
const numerator = tf * (k1 + 1);
|
|
96
|
+
const denominator = tf + k1 * (1 - b + b * (docLen / avgDocLength));
|
|
97
|
+
score += numerator / denominator;
|
|
98
|
+
}
|
|
99
|
+
// Normalize by query length
|
|
100
|
+
return score / queryTags.length;
|
|
101
|
+
}
|
|
34
102
|
/** High-frequency tags that should be weighted lower */
|
|
35
103
|
const COMMON_TAGS = new Set([
|
|
36
|
-
'typescript',
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
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
|
+
'타입',
|
|
40
135
|
]);
|
|
41
136
|
/** Apply IDF-like weight: common tags get reduced weight */
|
|
42
137
|
export function tagWeight(tag) {
|
|
@@ -47,7 +142,7 @@ export function calculateRelevance(promptOrTags, keywordsOrTags, confidence, opt
|
|
|
47
142
|
// Legacy mode: substring matching for backwards compatibility.
|
|
48
143
|
// Not a hot path — only hit by the (old) solution-matcher.test.ts cases.
|
|
49
144
|
const promptTags = extractTags(promptOrTags);
|
|
50
|
-
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)))));
|
|
51
146
|
return Math.min(1, intersection.length / Math.max(promptTags.length * 0.5, 1));
|
|
52
147
|
}
|
|
53
148
|
// v3 mode: tag matching with synonym expansion + TF-IDF weighting.
|
|
@@ -57,35 +152,86 @@ export function calculateRelevance(promptOrTags, keywordsOrTags, confidence, opt
|
|
|
57
152
|
// the hot path pre-compute the expansion once per query and pass it via
|
|
58
153
|
// `options.normalizedPromptTags`, so this function no longer repeats the
|
|
59
154
|
// work per solution.
|
|
60
|
-
const expandedPromptTags = options?.normalizedPromptTags
|
|
61
|
-
?? defaultNormalizer.normalizeTerms(promptOrTags);
|
|
155
|
+
const expandedPromptTags = options?.normalizedPromptTags ?? defaultNormalizer.normalizeTerms(promptOrTags);
|
|
62
156
|
// R4-T1: when the caller supplies a compound-expanded solution tag set,
|
|
63
157
|
// intersection and partial matching run against the expanded set (so
|
|
64
158
|
// `api-key` matches `api`/`key` queries via the split parts), but the
|
|
65
159
|
// Jaccard union denominator below still uses the RAW `keywordsOrTags`
|
|
66
160
|
// for normalization stability.
|
|
67
161
|
const matchTags = options?.solutionTagsExpanded ?? keywordsOrTags;
|
|
68
|
-
const intersection = matchTags.filter(t => expandedPromptTags.includes(t));
|
|
162
|
+
const intersection = matchTags.filter((t) => expandedPromptTags.includes(t));
|
|
69
163
|
// partial/substring matches for longer tags (>3 chars)
|
|
70
|
-
const partialMatches = matchTags.filter(t => t.length > 3 &&
|
|
71
|
-
|
|
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))));
|
|
72
167
|
// Apply TF-IDF weighting: common tags count less
|
|
73
|
-
const weightedMatched = intersection.reduce((sum, t) => sum + tagWeight(t), 0)
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
168
|
+
const weightedMatched = intersection.reduce((sum, t) => sum + tagWeight(t), 0) +
|
|
169
|
+
partialMatches.reduce((sum, t) => sum + tagWeight(t) * 0.5, 0);
|
|
170
|
+
// ── Bigram similarity boost for borderline cases ──
|
|
171
|
+
//
|
|
172
|
+
// When the TF-IDF intersection score is below the match threshold (0.5),
|
|
173
|
+
// compute a character-bigram Dice coefficient between the query tags and
|
|
174
|
+
// the solution tags. If the best bigram similarity is high enough, blend
|
|
175
|
+
// it in at 20% weight (TF-IDF 80%, bigram 20%) to rescue fuzzy matches
|
|
176
|
+
// that the exact/substring intersection missed (e.g., typos, slight
|
|
177
|
+
// morphological variants).
|
|
178
|
+
//
|
|
179
|
+
// When TF-IDF score is already above threshold, the bigram boost is NOT
|
|
180
|
+
// applied — this preserves existing match quality and avoids disturbing
|
|
181
|
+
// already-good rankings. The bigram path is purely a rescue mechanism
|
|
182
|
+
// for borderline cases.
|
|
183
|
+
if (weightedMatched < 0.5) {
|
|
184
|
+
// Compute best bigram similarity across all (promptTag, solutionTag) pairs
|
|
185
|
+
let bestBigramScore = 0;
|
|
186
|
+
const bigramMatchedTags = [];
|
|
187
|
+
for (const st of matchTags) {
|
|
188
|
+
for (const pt of expandedPromptTags) {
|
|
189
|
+
const sim = bigramSimilarity(pt, st);
|
|
190
|
+
if (sim > bestBigramScore) {
|
|
191
|
+
bestBigramScore = sim;
|
|
192
|
+
}
|
|
193
|
+
// Track solution tags with meaningful bigram similarity (> 0.4)
|
|
194
|
+
if (sim > 0.4 && !bigramMatchedTags.includes(st)) {
|
|
195
|
+
bigramMatchedTags.push(st);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Only rescue if the bigram signal is strong enough (> 0.4 threshold)
|
|
200
|
+
// to avoid noise from weakly similar strings
|
|
201
|
+
if (bestBigramScore > 0.4) {
|
|
202
|
+
const union = new Set([...promptOrTags, ...keywordsOrTags]).size;
|
|
203
|
+
const tfidfScore = weightedMatched / Math.max(union, 1);
|
|
204
|
+
const blendedScore = tfidfScore * 0.8 + bestBigramScore * 0.2;
|
|
205
|
+
return {
|
|
206
|
+
relevance: blendedScore * (confidence ?? 1),
|
|
207
|
+
matchedTags: [
|
|
208
|
+
...intersection,
|
|
209
|
+
...partialMatches,
|
|
210
|
+
...bigramMatchedTags.filter((t) => !intersection.includes(t) && !partialMatches.includes(t)),
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
77
214
|
return { relevance: 0, matchedTags: [] };
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
// so that the denominator semantics are unchanged from pre-T2 behaviour.
|
|
81
|
-
// This is intentional: expanding both sides of the Jaccard would
|
|
82
|
-
// asymmetrically inflate recall and silently shift all baseline metrics.
|
|
83
|
-
// R4-T1 explicitly preserves this: `keywordsOrTags` is the raw solution
|
|
84
|
-
// tag list, not the compound-expanded `matchTags` used above.
|
|
215
|
+
}
|
|
216
|
+
// Ensemble: TF-IDF (Jaccard) 0.5 + BM25 0.3 + bigram 0.2
|
|
85
217
|
const union = new Set([...promptOrTags, ...keywordsOrTags]).size;
|
|
86
|
-
const
|
|
218
|
+
const tfidfScore = weightedMatched / Math.max(union, 1);
|
|
219
|
+
// BM25 component: average doc length defaults to 6 tags (typical solution)
|
|
220
|
+
const avgDocLen = options?.avgDocLength ?? 6;
|
|
221
|
+
const bm25 = bm25Score(promptOrTags, keywordsOrTags, avgDocLen);
|
|
222
|
+
// Bigram component (mild boost for partial string matches)
|
|
223
|
+
let bigramBoost = 0;
|
|
224
|
+
for (const st of matchTags) {
|
|
225
|
+
for (const pt of expandedPromptTags) {
|
|
226
|
+
const sim = bigramSimilarity(pt, st);
|
|
227
|
+
if (sim > bigramBoost)
|
|
228
|
+
bigramBoost = sim;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
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;
|
|
87
233
|
return {
|
|
88
|
-
relevance:
|
|
234
|
+
relevance: ensembleScore * (confidence ?? 1),
|
|
89
235
|
matchedTags: [...intersection, ...partialMatches],
|
|
90
236
|
};
|
|
91
237
|
}
|
|
@@ -161,8 +307,8 @@ export function shouldRejectByR4T3Rules(promptTags, matchedTags) {
|
|
|
161
307
|
// Rule B
|
|
162
308
|
if (matchedTags.length === 1) {
|
|
163
309
|
const tag = matchedTags[0];
|
|
164
|
-
const literalHit = promptTags.includes(tag)
|
|
165
|
-
|
|
310
|
+
const literalHit = promptTags.includes(tag) ||
|
|
311
|
+
promptTags.some((pt) => {
|
|
166
312
|
if (pt.length <= 3 || tag.length <= 3)
|
|
167
313
|
return false;
|
|
168
314
|
if (pt.includes(tag) || tag.includes(pt))
|
|
@@ -197,7 +343,7 @@ export function shouldRejectByR4T3Rules(promptTags, matchedTags) {
|
|
|
197
343
|
* `matchSolutions` behaviour (both scopes could rank). Callers that want
|
|
198
344
|
* first-wins scope precedence must dedupe on their side.
|
|
199
345
|
*/
|
|
200
|
-
function rankCandidates(promptTags, promptLower, solutions) {
|
|
346
|
+
function rankCandidates(promptTags, promptLower, solutions, ensembleWeights) {
|
|
201
347
|
// T2: normalize prompt tags ONCE per query (not once per solution).
|
|
202
348
|
// Pre-T2 this expansion happened inside calculateRelevance and was
|
|
203
349
|
// repeated N times for N solutions — the plan's primary hot-path win.
|
|
@@ -232,7 +378,7 @@ function rankCandidates(promptTags, promptLower, solutions) {
|
|
|
232
378
|
const promptTagsWithBigrams = expandQueryBigrams(maskedPromptTags);
|
|
233
379
|
const normalizedPromptTags = defaultNormalizer.normalizeTerms(promptTagsWithBigrams);
|
|
234
380
|
return solutions
|
|
235
|
-
.map(sol => {
|
|
381
|
+
.map((sol) => {
|
|
236
382
|
// R4-T1: solution-side compound-tag expansion. `api-key` becomes
|
|
237
383
|
// {api-key, api, key} so a query token `api` (from "api keys") hits
|
|
238
384
|
// it directly. Computed per solution because each sol.tags is
|
|
@@ -245,7 +391,11 @@ function rankCandidates(promptTags, promptLower, solutions) {
|
|
|
245
391
|
// step (intersection/partialMatches) already uses the masked set
|
|
246
392
|
// via `normalizedPromptTags` — the union must match for score
|
|
247
393
|
// semantics to stay consistent.
|
|
248
|
-
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
|
+
});
|
|
249
399
|
// Compute identifier boost FIRST — independent of tag scoring so
|
|
250
400
|
// R4-T3's tag-evidence precision rules below cannot silently drop
|
|
251
401
|
// a candidate that has strong identifier-level evidence.
|
|
@@ -272,9 +422,9 @@ function rankCandidates(promptTags, promptLower, solutions) {
|
|
|
272
422
|
// the `matchedTags.length + matchedIdentifiers.length >= 1` filter.
|
|
273
423
|
let tagRelevance = result.relevance;
|
|
274
424
|
let tagMatches = result.matchedTags;
|
|
275
|
-
if (matchedIdentifiers.length === 0
|
|
276
|
-
|
|
277
|
-
|
|
425
|
+
if (matchedIdentifiers.length === 0 &&
|
|
426
|
+
tagMatches.length > 0 &&
|
|
427
|
+
shouldRejectByR4T3Rules(maskedPromptTags, tagMatches)) {
|
|
278
428
|
tagRelevance = 0;
|
|
279
429
|
tagMatches = [];
|
|
280
430
|
}
|
|
@@ -285,7 +435,7 @@ function rankCandidates(promptTags, promptLower, solutions) {
|
|
|
285
435
|
matchedIdentifiers,
|
|
286
436
|
};
|
|
287
437
|
})
|
|
288
|
-
.filter(c => c.matchedTags.length + c.matchedIdentifiers.length >= 1)
|
|
438
|
+
.filter((c) => c.matchedTags.length + c.matchedIdentifiers.length >= 1)
|
|
289
439
|
.sort((a, b) => b.relevance - a.relevance)
|
|
290
440
|
.slice(0, 5);
|
|
291
441
|
}
|
|
@@ -562,7 +712,7 @@ function computeBucketMetrics(queries, solutions) {
|
|
|
562
712
|
*/
|
|
563
713
|
export function evaluateQuery(query, solutions) {
|
|
564
714
|
const promptTags = extractTags(query);
|
|
565
|
-
return rankCandidates(promptTags, query.toLowerCase(), solutions).map(c => ({
|
|
715
|
+
return rankCandidates(promptTags, query.toLowerCase(), solutions).map((c) => ({
|
|
566
716
|
name: c.solution.name,
|
|
567
717
|
relevance: c.relevance,
|
|
568
718
|
matchedTags: c.matchedTags,
|
|
@@ -588,13 +738,16 @@ export function evaluateSolutionMatcher(fixture) {
|
|
|
588
738
|
// doesn't drown a small paraphrase bucket but also a single-query bucket
|
|
589
739
|
// doesn't dominate.
|
|
590
740
|
const recallAt5 = combinedTotal > 0
|
|
591
|
-
? (positiveM.recallAt5 * positiveM.total + paraphraseM.recallAt5 * paraphraseM.total) /
|
|
741
|
+
? (positiveM.recallAt5 * positiveM.total + paraphraseM.recallAt5 * paraphraseM.total) /
|
|
742
|
+
combinedTotal
|
|
592
743
|
: 0;
|
|
593
744
|
const mrrAt5 = combinedTotal > 0
|
|
594
|
-
? (positiveM.mrrAt5 * positiveM.total + paraphraseM.mrrAt5 * paraphraseM.total) /
|
|
745
|
+
? (positiveM.mrrAt5 * positiveM.total + paraphraseM.mrrAt5 * paraphraseM.total) /
|
|
746
|
+
combinedTotal
|
|
595
747
|
: 0;
|
|
596
748
|
const noResultRate = combinedTotal > 0
|
|
597
|
-
? (positiveM.noResultRate * positiveM.total + paraphraseM.noResultRate * paraphraseM.total) /
|
|
749
|
+
? (positiveM.noResultRate * positiveM.total + paraphraseM.noResultRate * paraphraseM.total) /
|
|
750
|
+
combinedTotal
|
|
598
751
|
: 0;
|
|
599
752
|
let negAnyResult = 0;
|
|
600
753
|
for (const q of fixture.negative) {
|
|
@@ -620,27 +773,64 @@ export function evaluateSolutionMatcher(fixture) {
|
|
|
620
773
|
},
|
|
621
774
|
};
|
|
622
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
|
+
}
|
|
623
813
|
export function matchSolutions(prompt, scope, cwd) {
|
|
624
814
|
// Build solution dirs for index cache
|
|
625
|
-
const dirs = [
|
|
626
|
-
{ dir: ME_SOLUTIONS, scope: 'me' },
|
|
627
|
-
];
|
|
815
|
+
const dirs = [{ dir: ME_SOLUTIONS, scope: 'me' }];
|
|
628
816
|
if (scope.team) {
|
|
629
817
|
dirs.push({ dir: path.join(PACKS_DIR, scope.team.name, 'solutions'), scope: 'team' });
|
|
630
818
|
}
|
|
631
819
|
dirs.push({ dir: path.join(cwd, '.compound', 'solutions'), scope: 'project' });
|
|
632
820
|
// Use cached index (rebuilt only when dirs change)
|
|
633
821
|
const index = getOrBuildIndex(dirs);
|
|
634
|
-
const allSolutions = index.entries.map(e => ({ ...e }));
|
|
822
|
+
const allSolutions = index.entries.map((e) => ({ ...e }));
|
|
635
823
|
const promptTags = extractTags(prompt);
|
|
636
824
|
const promptLower = prompt.toLowerCase();
|
|
825
|
+
// Meta-learning: load tuned weights if available
|
|
826
|
+
const tunedWeights = loadTunedMatcherWeights();
|
|
637
827
|
// Delegate to shared ranking core. `rankCandidates` is generic so each
|
|
638
828
|
// ranked candidate carries the original `LoadedSolution` reference — no
|
|
639
829
|
// name-based re-lookup, so two scopes sharing a name (e.g. me/foo and
|
|
640
830
|
// project/foo) can both appear in the result without a Map last-wins
|
|
641
831
|
// scope-precedence bug.
|
|
642
|
-
const ranked = rankCandidates(promptTags, promptLower, allSolutions);
|
|
643
|
-
return ranked.map(c => ({
|
|
832
|
+
const ranked = rankCandidates(promptTags, promptLower, allSolutions, tunedWeights);
|
|
833
|
+
return ranked.map((c) => ({
|
|
644
834
|
name: c.solution.name,
|
|
645
835
|
path: c.solution.filePath,
|
|
646
836
|
scope: c.solution.scope,
|
package/dist/fgx.js
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
* fgx — forgen --dangerously-skip-permissions 의 단축 명령
|
|
4
4
|
* 모든 인자를 그대로 전달하되, --dangerously-skip-permissions 를 자동 주입
|
|
5
5
|
*/
|
|
6
|
+
import { resolveLaunchContext } from './services/session.js';
|
|
7
|
+
import { prepareHarness, isFirstRun } from './core/harness.js';
|
|
8
|
+
import { spawnClaude } from './core/spawn.js';
|
|
6
9
|
const args = process.argv.slice(2);
|
|
7
10
|
// 이미 포함되어 있으면 중복 추가하지 않음
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const launchContext = resolveLaunchContext(args);
|
|
12
|
+
const runtime = launchContext.runtime;
|
|
13
|
+
const launchArgs = [...launchContext.args];
|
|
14
|
+
if (!launchArgs.includes('--dangerously-skip-permissions')) {
|
|
15
|
+
launchArgs.unshift('--dangerously-skip-permissions');
|
|
10
16
|
}
|
|
11
|
-
// cli.ts 의 main 로직을 재사용
|
|
12
|
-
import { prepareHarness, isFirstRun } from './core/harness.js';
|
|
13
|
-
import { spawnClaude } from './core/spawn.js';
|
|
14
17
|
async function main() {
|
|
15
18
|
// Security warning — fgx bypasses all Claude Code permission checks
|
|
16
19
|
console.warn('\n ⚠ fgx: ALL permission checks are disabled (--dangerously-skip-permissions)');
|
|
@@ -23,7 +26,7 @@ async function main() {
|
|
|
23
26
|
console.log(' Creating ~/.forgen/ directory and default philosophy.');
|
|
24
27
|
console.log(' Run `forgen onboarding` afterwards to complete personalization.\n');
|
|
25
28
|
}
|
|
26
|
-
const context = await prepareHarness(process.cwd());
|
|
29
|
+
const context = await prepareHarness(process.cwd(), { runtime });
|
|
27
30
|
if (firstRun) {
|
|
28
31
|
console.log(' [Done] Initial setup complete.\n');
|
|
29
32
|
}
|
|
@@ -33,8 +36,9 @@ async function main() {
|
|
|
33
36
|
console.log(`[forgen] Trust: ${v1.session.effective_trust_policy}`);
|
|
34
37
|
}
|
|
35
38
|
console.log('[forgen] Mode: dangerously-skip-permissions');
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const runtimeLabel = runtime === 'codex' ? 'Codex' : 'Claude';
|
|
40
|
+
console.log(`[forgen] Starting ${runtimeLabel}...\n`);
|
|
41
|
+
await spawnClaude(launchArgs, context, runtime);
|
|
38
42
|
}
|
|
39
43
|
main().catch((err) => {
|
|
40
44
|
console.error('[forgen] Error:', err instanceof Error ? err.message : err);
|
|
@@ -19,6 +19,21 @@ export declare function shouldWarn(contextPercent: {
|
|
|
19
19
|
charsThreshold?: number;
|
|
20
20
|
cooldownMs?: number;
|
|
21
21
|
}): boolean;
|
|
22
|
+
/** auto-compact 트리거 여부 판정 (순수 함수) */
|
|
23
|
+
export declare function shouldAutoCompact(state: {
|
|
24
|
+
totalChars: number;
|
|
25
|
+
lastAutoCompactAt: number;
|
|
26
|
+
}, thresholds?: {
|
|
27
|
+
charsThreshold?: number;
|
|
28
|
+
cooldownMs?: number;
|
|
29
|
+
}): boolean;
|
|
30
|
+
/** auto-compact 지시 메시지 생성 (순수 함수) */
|
|
31
|
+
export declare function buildAutoCompactMessage(totalChars: number): string;
|
|
22
32
|
/** 경고 메시지 생성 (순수 함수) */
|
|
23
33
|
export declare function buildContextWarningMessage(promptCount: number, totalChars: number): string;
|
|
24
34
|
export declare function main(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* forge-loop 활성 시 미완료 스토리가 있으면 Stop을 차단하고 지속 메시지 주입.
|
|
37
|
+
* OMC의 persistent-mode.cjs 패턴 참고.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkForgeLoopActive(): string | null;
|