autosnippet 3.3.3 → 3.3.5
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/README.md +176 -81
- package/config/constitution.yaml +2 -0
- package/dist/lib/cli/KnowledgeSyncService.d.ts +5 -1
- package/dist/lib/cli/KnowledgeSyncService.js +5 -2
- package/dist/lib/domain/knowledge/values/Stats.d.ts +1 -1
- package/dist/lib/domain/knowledge/values/Stats.js +2 -2
- package/dist/lib/external/mcp/handlers/consolidated.js +178 -0
- package/dist/lib/external/mcp/handlers/task.js +36 -14
- package/dist/lib/external/mcp/tools.js +2 -1
- package/dist/lib/injection/modules/InfraModule.js +4 -1
- package/dist/lib/injection/modules/KnowledgeModule.js +23 -0
- package/dist/lib/repository/evolution/ProposalRepository.d.ts +99 -0
- package/dist/lib/repository/evolution/ProposalRepository.js +255 -0
- package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +17 -4
- package/dist/lib/service/bootstrap/UiStartupTasks.js +53 -5
- package/dist/lib/service/evolution/DecayDetector.d.ts +4 -3
- package/dist/lib/service/evolution/DecayDetector.js +97 -22
- package/dist/lib/service/evolution/KnowledgeMetabolism.d.ts +4 -2
- package/dist/lib/service/evolution/KnowledgeMetabolism.js +29 -2
- package/dist/lib/service/evolution/ProposalExecutor.d.ts +62 -0
- package/dist/lib/service/evolution/ProposalExecutor.js +360 -0
- package/dist/lib/service/evolution/StagingManager.js +5 -3
- package/dist/lib/service/guard/GuardCrossFileChecks.js +2 -0
- package/dist/lib/service/guard/ReverseGuard.d.ts +1 -1
- package/dist/lib/service/guard/ReverseGuard.js +32 -2
- package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +2 -0
- package/dist/lib/service/knowledge/SourceRefReconciler.js +48 -0
- package/dist/lib/service/task/IntentExtractor.d.ts +3 -1
- package/dist/lib/service/task/IntentExtractor.js +30 -10
- package/dist/lib/service/task/PrimeSearchPipeline.js +67 -12
- package/dist/lib/shared/schemas/mcp-tools.d.ts +2 -0
- package/dist/lib/shared/schemas/mcp-tools.js +9 -1
- package/package.json +1 -1
- package/templates/instructions/conventions.md +4 -2
|
@@ -38,8 +38,10 @@ export interface TechTermOptions {
|
|
|
38
38
|
export declare function extract(userQuery: string, activeFile?: string, language?: string, termOpts?: TechTermOptions): ExtractedIntent;
|
|
39
39
|
/**
|
|
40
40
|
* Build multi-query set from user query + active file.
|
|
41
|
-
* Q1: raw query, Q2: extracted tech terms, Q3: file context.
|
|
41
|
+
* Q1: raw query, Q2: extracted tech terms, Q3: file context, Q4: synonym focus.
|
|
42
42
|
* Q1 is enriched with cross-language synonyms to bridge EN↔CJK matching.
|
|
43
|
+
* Q4 (long queries only): synonym expansion as a separate focused query
|
|
44
|
+
* to prevent BM25 dilution in verbose natural language inputs.
|
|
43
45
|
*/
|
|
44
46
|
export declare function buildQueries(userQuery: string, activeFile?: string, termOpts?: TechTermOptions): string[];
|
|
45
47
|
/**
|
|
@@ -70,6 +70,13 @@ const SYNONYM_GROUPS = [
|
|
|
70
70
|
['sync', 'synchronous', '同步'],
|
|
71
71
|
['thread', 'threading', '线程'],
|
|
72
72
|
['concur', 'concurrency', '并发'],
|
|
73
|
+
// Memory management
|
|
74
|
+
['memory', '内存'],
|
|
75
|
+
['leak', 'leakage', '泄漏'],
|
|
76
|
+
['weak', '弱引用'],
|
|
77
|
+
['retain', '持有', '保留'],
|
|
78
|
+
['release', '释放'],
|
|
79
|
+
['reference', '引用'],
|
|
73
80
|
// Common concepts
|
|
74
81
|
['network', '网络'],
|
|
75
82
|
['cache', 'caching', '缓存'],
|
|
@@ -120,8 +127,10 @@ export function extract(userQuery, activeFile, language, termOpts) {
|
|
|
120
127
|
}
|
|
121
128
|
/**
|
|
122
129
|
* Build multi-query set from user query + active file.
|
|
123
|
-
* Q1: raw query, Q2: extracted tech terms, Q3: file context.
|
|
130
|
+
* Q1: raw query, Q2: extracted tech terms, Q3: file context, Q4: synonym focus.
|
|
124
131
|
* Q1 is enriched with cross-language synonyms to bridge EN↔CJK matching.
|
|
132
|
+
* Q4 (long queries only): synonym expansion as a separate focused query
|
|
133
|
+
* to prevent BM25 dilution in verbose natural language inputs.
|
|
125
134
|
*/
|
|
126
135
|
export function buildQueries(userQuery, activeFile, termOpts) {
|
|
127
136
|
// Enrich raw query with cross-language synonyms
|
|
@@ -132,6 +141,14 @@ export function buildQueries(userQuery, activeFile, termOpts) {
|
|
|
132
141
|
if (terms.length > 0) {
|
|
133
142
|
queries.push(terms.join(' '));
|
|
134
143
|
}
|
|
144
|
+
// Q4: For long queries (> 50 chars), add cross-language synonyms as a
|
|
145
|
+
// separate focused query. In long sentences, synonym terms appended to Q1
|
|
146
|
+
// get diluted by common words ("ViewController", "ViewModel"), causing
|
|
147
|
+
// BM25 to miss the user's actual intent. A short focused query matches
|
|
148
|
+
// domain-specific terms (e.g. "singleton 单例 inject 注入") directly.
|
|
149
|
+
if (synonyms && userQuery.length > 50) {
|
|
150
|
+
queries.push(synonyms);
|
|
151
|
+
}
|
|
135
152
|
if (activeFile) {
|
|
136
153
|
const ctx = inferFileContext(activeFile);
|
|
137
154
|
if (ctx) {
|
|
@@ -203,7 +220,7 @@ export function inferLanguage(filePath) {
|
|
|
203
220
|
*/
|
|
204
221
|
export function classifyScenario(userQuery) {
|
|
205
222
|
const q = userQuery.toLowerCase();
|
|
206
|
-
if (/帮我[加写做实现创建]|implement|add|create|新[增加建]
|
|
223
|
+
if (/帮我[加写做实现创建]|implement|add|create|新[增加建]|添加|修改|删除|实现|开发|编写|创建|初始化/.test(q)) {
|
|
207
224
|
return 'generate';
|
|
208
225
|
}
|
|
209
226
|
if (/检查|review|lint|合规|违规|guard|规[则范]/.test(q)) {
|
|
@@ -220,23 +237,26 @@ export function classifyScenario(userQuery) {
|
|
|
220
237
|
* Tokenizes query, looks up each token in the synonym table,
|
|
221
238
|
* returns a query string of synonym expansions for cross-language matching.
|
|
222
239
|
*
|
|
223
|
-
* Strategy:
|
|
224
|
-
*
|
|
240
|
+
* Strategy: per-token cross-script expansion. Each token's script is checked
|
|
241
|
+
* individually, and only synonyms in the OPPOSITE script are added.
|
|
242
|
+
* This correctly handles mixed EN/CJK queries (e.g. "在 module 里用 singleton")
|
|
243
|
+
* where both EN→CJK and CJK→EN expansions are needed.
|
|
225
244
|
*/
|
|
226
245
|
function expandWithSynonyms(query) {
|
|
227
246
|
const tokens = tokenize(query);
|
|
228
247
|
const crossScriptTerms = new Set();
|
|
229
|
-
|
|
230
|
-
const hasCJK = /[\u4e00-\u9fff\u3400-\u4dbf]/.test(query);
|
|
248
|
+
const CJK_RE = /[\u4e00-\u9fff\u3400-\u4dbf]/;
|
|
231
249
|
for (const token of tokens) {
|
|
232
250
|
const synonyms = SYNONYM_LOOKUP.get(token.toLowerCase());
|
|
233
251
|
if (!synonyms) {
|
|
234
252
|
continue;
|
|
235
253
|
}
|
|
254
|
+
// Determine THIS token's script, not the whole query's
|
|
255
|
+
const tokenIsCJK = CJK_RE.test(token);
|
|
236
256
|
for (const syn of synonyms) {
|
|
237
|
-
const synIsCJK =
|
|
238
|
-
// Cross-script: EN
|
|
239
|
-
if (
|
|
257
|
+
const synIsCJK = CJK_RE.test(syn);
|
|
258
|
+
// Cross-script: EN token → add CJK synonyms; CJK token → add EN synonyms
|
|
259
|
+
if (tokenIsCJK !== synIsCJK) {
|
|
240
260
|
crossScriptTerms.add(syn);
|
|
241
261
|
}
|
|
242
262
|
}
|
|
@@ -244,7 +264,7 @@ function expandWithSynonyms(query) {
|
|
|
244
264
|
if (crossScriptTerms.size === 0) {
|
|
245
265
|
return null;
|
|
246
266
|
}
|
|
247
|
-
return [...crossScriptTerms].slice(0,
|
|
267
|
+
return [...crossScriptTerms].slice(0, 16).join(' ');
|
|
248
268
|
}
|
|
249
269
|
function buildPrefixPattern(prefixes) {
|
|
250
270
|
if (prefixes.length === 0) {
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { slimSearchResult } from '#service/search/SearchTypes.js';
|
|
10
10
|
// ── Constants ───────────────────────────────────────
|
|
11
|
-
|
|
11
|
+
/** Absolute minimum score — items below this are definitely noise */
|
|
12
|
+
const MIN_SCORE_THRESHOLD = 0.3;
|
|
13
|
+
/** Relative threshold — items scoring below this fraction of the best result are dropped */
|
|
14
|
+
const RELATIVE_SCORE_RATIO = 0.15;
|
|
15
|
+
/** Gap ratio — if score drops by more than this factor from the previous item, truncate */
|
|
16
|
+
const GAP_DROP_RATIO = 0.25;
|
|
12
17
|
// ── PrimeSearchPipeline ─────────────────────────────
|
|
13
18
|
export class PrimeSearchPipeline {
|
|
14
19
|
#search;
|
|
@@ -31,8 +36,8 @@ export class PrimeSearchPipeline {
|
|
|
31
36
|
};
|
|
32
37
|
// Multi-query parallel search (auto mode + keyword mode for cross-language)
|
|
33
38
|
const allResults = await this.#multiQuerySearch(intent.queries, intent.keywordQueries ?? [], context);
|
|
34
|
-
//
|
|
35
|
-
const filtered = allResults
|
|
39
|
+
// Quality filter: absolute threshold + relative-to-best + score gap detection
|
|
40
|
+
const filtered = this.#qualityFilter(allResults);
|
|
36
41
|
if (filtered.length === 0) {
|
|
37
42
|
return null;
|
|
38
43
|
}
|
|
@@ -62,14 +67,46 @@ export class PrimeSearchPipeline {
|
|
|
62
67
|
}
|
|
63
68
|
// ── Private ───────────────────────────────────────
|
|
64
69
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
|
|
70
|
+
* Quality filter: absolute threshold + relative-to-best + score gap detection.
|
|
71
|
+
* Expects items sorted by score descending.
|
|
72
|
+
*/
|
|
73
|
+
#qualityFilter(items) {
|
|
74
|
+
if (items.length === 0) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const maxScore = items[0]?.score ?? 0;
|
|
78
|
+
const effectiveThreshold = Math.max(MIN_SCORE_THRESHOLD, maxScore * RELATIVE_SCORE_RATIO);
|
|
79
|
+
const result = [];
|
|
80
|
+
let prevScore = maxScore;
|
|
81
|
+
for (const item of items) {
|
|
82
|
+
const score = item.score;
|
|
83
|
+
if (score < effectiveThreshold) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
// Gap detection: if score drops sharply from previous item, stop
|
|
87
|
+
if (result.length > 0 && score < prevScore * GAP_DROP_RATIO) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
result.push(item);
|
|
91
|
+
prevScore = score;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Multi-query parallel search with optional Reciprocal Rank Fusion (RRF).
|
|
97
|
+
*
|
|
98
|
+
* Single-query: preserves original search engine scores (BM25/CoarseRanker).
|
|
99
|
+
* Multi-query: uses RRF to fuse results, but weights by original score to
|
|
100
|
+
* retain magnitude information.
|
|
68
101
|
*/
|
|
69
102
|
async #multiQuerySearch(autoQueries, keywordQueries, context) {
|
|
70
|
-
// Auto-mode searches (
|
|
103
|
+
// Auto-mode searches (BM25 without CoarseRanker ranking)
|
|
104
|
+
// Using rank: false preserves raw BM25/FWS score magnitude,
|
|
105
|
+
// which the quality filter needs for effective discrimination.
|
|
106
|
+
// CoarseRanker's max-normalization + freshness/popularity signals
|
|
107
|
+
// would cluster scores around 0.35–0.41, defeating the filter.
|
|
71
108
|
const autoPromises = autoQueries.map((q) => this.#search
|
|
72
|
-
.search(q, { mode: 'auto', limit: 8, rank:
|
|
109
|
+
.search(q, { mode: 'auto', limit: 8, rank: false, context })
|
|
73
110
|
.catch(() => ({ items: [] })));
|
|
74
111
|
// Keyword-mode searches (raw FWS scores — for cross-language synonym matching)
|
|
75
112
|
const kwPromises = keywordQueries.map((q) => this.#search
|
|
@@ -80,15 +117,25 @@ export class PrimeSearchPipeline {
|
|
|
80
117
|
Promise.all(kwPromises),
|
|
81
118
|
]);
|
|
82
119
|
const allResponses = [...autoResponses, ...kwResponses];
|
|
83
|
-
//
|
|
120
|
+
// Single-query shortcut: preserve original scores from search engine.
|
|
121
|
+
// RRF is pointless with one response — it just converts rank to score,
|
|
122
|
+
// discarding the magnitude information from BM25/CoarseRanker.
|
|
123
|
+
if (allResponses.length === 1) {
|
|
124
|
+
const items = (allResponses[0]?.items || []);
|
|
125
|
+
return items.map(slimSearchResult).sort((a, b) => b.score - a.score);
|
|
126
|
+
}
|
|
127
|
+
// Multi-query: Weighted RRF — RRF(d) = Σ origScore / (k + rank)
|
|
128
|
+
// Retains original score magnitude while still boosting cross-query overlap.
|
|
84
129
|
const RRF_K = 60;
|
|
85
130
|
const rrfScores = new Map();
|
|
86
131
|
const itemById = new Map();
|
|
87
132
|
for (const resp of allResponses) {
|
|
88
133
|
const items = (resp.items || []);
|
|
89
134
|
for (let rank = 0; rank < items.length; rank++) {
|
|
90
|
-
const
|
|
91
|
-
|
|
135
|
+
const raw = items[rank];
|
|
136
|
+
const origScore = Math.max(raw.score || 0, 0.01);
|
|
137
|
+
const item = slimSearchResult(raw);
|
|
138
|
+
rrfScores.set(item.id, (rrfScores.get(item.id) ?? 0) + origScore / (RRF_K + rank));
|
|
92
139
|
// Keep the richest metadata version
|
|
93
140
|
if (!itemById.has(item.id)) {
|
|
94
141
|
itemById.set(item.id, item);
|
|
@@ -96,10 +143,18 @@ export class PrimeSearchPipeline {
|
|
|
96
143
|
}
|
|
97
144
|
}
|
|
98
145
|
// Assign fused scores and sort
|
|
146
|
+
// Rescale: RRF_K division crushes scores to ~0.003–0.02 range,
|
|
147
|
+
// which falls below qualityFilter's MIN_SCORE_THRESHOLD (0.1).
|
|
148
|
+
// Multiply by RRF_K to restore original score magnitude.
|
|
149
|
+
// Effective formula: Σ origScore / (1 + rank/K), preserving magnitude
|
|
150
|
+
// while still giving a gentle rank-based discount.
|
|
99
151
|
const results = [];
|
|
100
152
|
for (const [id, rrfScore] of rrfScores) {
|
|
101
153
|
const item = itemById.get(id);
|
|
102
|
-
item
|
|
154
|
+
if (!item) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
item.score = Math.round(rrfScore * RRF_K * 1000) / 1000;
|
|
103
158
|
results.push(item);
|
|
104
159
|
}
|
|
105
160
|
return results.sort((a, b) => b.score - a.score);
|
|
@@ -183,6 +183,7 @@ export declare const SubmitKnowledgeInput: z.ZodObject<{
|
|
|
183
183
|
skipDuplicateCheck: z.ZodDefault<z.ZodBoolean>;
|
|
184
184
|
client_id: z.ZodOptional<z.ZodString>;
|
|
185
185
|
dimensionId: z.ZodOptional<z.ZodString>;
|
|
186
|
+
supersedes: z.ZodOptional<z.ZodString>;
|
|
186
187
|
}, z.core.$strip>;
|
|
187
188
|
export type SubmitKnowledgeInput = z.infer<typeof SubmitKnowledgeInput>;
|
|
188
189
|
export declare const SkillInput: z.ZodObject<{
|
|
@@ -247,6 +248,7 @@ export declare const TaskInput: z.ZodObject<{
|
|
|
247
248
|
title: z.ZodOptional<z.ZodString>;
|
|
248
249
|
description: z.ZodOptional<z.ZodString>;
|
|
249
250
|
id: z.ZodOptional<z.ZodString>;
|
|
251
|
+
taskId: z.ZodOptional<z.ZodString>;
|
|
250
252
|
reason: z.ZodOptional<z.ZodString>;
|
|
251
253
|
rationale: z.ZodOptional<z.ZodString>;
|
|
252
254
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -173,6 +173,10 @@ export const SubmitKnowledgeInput = z.object({
|
|
|
173
173
|
skipDuplicateCheck: z.boolean().default(false),
|
|
174
174
|
client_id: z.string().optional(),
|
|
175
175
|
dimensionId: z.string().optional().describe('冷启动关联维度 ID'),
|
|
176
|
+
supersedes: z
|
|
177
|
+
.string()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe('声明新 Recipe 替代旧 Recipe 的 ID。提交后系统将创建 supersede 提案,观察窗口内对比新旧表现后自动执行。'),
|
|
176
180
|
});
|
|
177
181
|
// ══════════════════════════════════════════════════════
|
|
178
182
|
// 10. autosnippet_skill
|
|
@@ -235,7 +239,11 @@ export const TaskInput = z.object({
|
|
|
235
239
|
.describe('prime=加载知识上下文 | create=创建任务锚点 | close=完成+Guard | fail=放弃 | record_decision=记录用户偏好'),
|
|
236
240
|
title: z.string().optional().describe('Task or decision title (create / record_decision)'),
|
|
237
241
|
description: z.string().optional().describe('Decision description (record_decision)'),
|
|
238
|
-
id: z
|
|
242
|
+
id: z
|
|
243
|
+
.string()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe('Task ID (close / fail). Optional if a task was created in the current session.'),
|
|
246
|
+
taskId: z.string().optional().describe('Alias for id (accepted for convenience)'),
|
|
239
247
|
reason: z.string().optional().describe('Close reason or fail reason'),
|
|
240
248
|
rationale: z.string().optional().describe('Decision rationale (record_decision)'),
|
|
241
249
|
tags: z.array(z.string()).optional().describe('Decision tags (record_decision)'),
|
package/package.json
CHANGED
|
@@ -13,11 +13,13 @@ Users speak naturally; you translate to task operations. Never tell users to cal
|
|
|
13
13
|
|
|
14
14
|
| User Says | You Run |
|
|
15
15
|
|---|---|
|
|
16
|
-
| "fix bug" / "implement" | `create` → code → `close` |
|
|
17
|
-
| "continue" | resume in-progress → `close` |
|
|
16
|
+
| "fix bug" / "implement" | `create` → code → `close` → `autosnippet_guard()` |
|
|
17
|
+
| "continue" | resume in-progress → `close` → `autosnippet_guard()` |
|
|
18
18
|
| "pause" / "abandon" | `fail(id, reason)` |
|
|
19
19
|
| "agreed" | `record_decision(...)` |
|
|
20
20
|
|
|
21
|
+
7. **After close** — MUST call `autosnippet_guard()` (no args) for compliance review before moving on. Never skip.
|
|
22
|
+
|
|
21
23
|
## Knowledge Rules
|
|
22
24
|
|
|
23
25
|
- **Do NOT modify** `AutoSnippet/recipes/` or `.autosnippet/` directly.
|