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.
Files changed (34) hide show
  1. package/README.md +176 -81
  2. package/config/constitution.yaml +2 -0
  3. package/dist/lib/cli/KnowledgeSyncService.d.ts +5 -1
  4. package/dist/lib/cli/KnowledgeSyncService.js +5 -2
  5. package/dist/lib/domain/knowledge/values/Stats.d.ts +1 -1
  6. package/dist/lib/domain/knowledge/values/Stats.js +2 -2
  7. package/dist/lib/external/mcp/handlers/consolidated.js +178 -0
  8. package/dist/lib/external/mcp/handlers/task.js +36 -14
  9. package/dist/lib/external/mcp/tools.js +2 -1
  10. package/dist/lib/injection/modules/InfraModule.js +4 -1
  11. package/dist/lib/injection/modules/KnowledgeModule.js +23 -0
  12. package/dist/lib/repository/evolution/ProposalRepository.d.ts +99 -0
  13. package/dist/lib/repository/evolution/ProposalRepository.js +255 -0
  14. package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +17 -4
  15. package/dist/lib/service/bootstrap/UiStartupTasks.js +53 -5
  16. package/dist/lib/service/evolution/DecayDetector.d.ts +4 -3
  17. package/dist/lib/service/evolution/DecayDetector.js +97 -22
  18. package/dist/lib/service/evolution/KnowledgeMetabolism.d.ts +4 -2
  19. package/dist/lib/service/evolution/KnowledgeMetabolism.js +29 -2
  20. package/dist/lib/service/evolution/ProposalExecutor.d.ts +62 -0
  21. package/dist/lib/service/evolution/ProposalExecutor.js +360 -0
  22. package/dist/lib/service/evolution/StagingManager.js +5 -3
  23. package/dist/lib/service/guard/GuardCrossFileChecks.js +2 -0
  24. package/dist/lib/service/guard/ReverseGuard.d.ts +1 -1
  25. package/dist/lib/service/guard/ReverseGuard.js +32 -2
  26. package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +2 -0
  27. package/dist/lib/service/knowledge/SourceRefReconciler.js +48 -0
  28. package/dist/lib/service/task/IntentExtractor.d.ts +3 -1
  29. package/dist/lib/service/task/IntentExtractor.js +30 -10
  30. package/dist/lib/service/task/PrimeSearchPipeline.js +67 -12
  31. package/dist/lib/shared/schemas/mcp-tools.d.ts +2 -0
  32. package/dist/lib/shared/schemas/mcp-tools.js +9 -1
  33. package/package.json +1 -1
  34. 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|新[增加建]/.test(q)) {
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: return only cross-script synonyms (EN→CJK or CJK→EN).
224
- * This keeps the expansion focused the original script tokens are already in Q1.
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
- // Detect query script: does it contain CJK?
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 = /[\u4e00-\u9fff\u3400-\u4dbf]/.test(syn);
238
- // Cross-script: EN query → add CJK synonyms; CJK query → add EN synonyms
239
- if (hasCJK !== synIsCJK) {
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, 12).join(' ');
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
- const RELEVANCE_THRESHOLD = 0.01;
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
- // Threshold filter
35
- const filtered = allResults.filter((r) => (r.score ?? 0) >= RELEVANCE_THRESHOLD);
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
- * Multi-query parallel search with Reciprocal Rank Fusion (RRF).
66
- * Auto-mode queries use CoarseRanker; keyword queries use raw FWS scores.
67
- * Results are fused by rank position, not absolute scores — robust across heterogeneous scorers.
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 (full CoarseRanker pipeline)
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: true, context })
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
- // Reciprocal Rank Fusion: RRF(d) = Σ 1/(k + rank)
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 item = slimSearchResult(items[rank]);
91
- rrfScores.set(item.id, (rrfScores.get(item.id) ?? 0) + 1 / (RRF_K + rank));
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.score = rrfScore;
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.string().optional().describe('Task ID (close / fail)'),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
4
4
  "description": "Extract code patterns into a knowledge base for AI coding assistants",
5
5
  "type": "module",
6
6
  "main": "dist/lib/bootstrap.js",
@@ -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.