autosnippet 3.3.2 → 3.3.4

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 (55) hide show
  1. package/README.md +8 -4
  2. package/dist/bin/cli.js +27 -1
  3. package/dist/lib/cli/KnowledgeSyncService.d.ts +26 -0
  4. package/dist/lib/cli/KnowledgeSyncService.js +33 -1
  5. package/dist/lib/external/mcp/handlers/browse.d.ts +1 -0
  6. package/dist/lib/external/mcp/handlers/browse.js +2 -1
  7. package/dist/lib/external/mcp/handlers/consolidated.d.ts +1 -0
  8. package/dist/lib/external/mcp/handlers/panorama.d.ts +11 -11
  9. package/dist/lib/external/mcp/handlers/panorama.js +20 -20
  10. package/dist/lib/external/mcp/handlers/system.d.ts +1 -1
  11. package/dist/lib/external/mcp/handlers/task.js +38 -15
  12. package/dist/lib/external/mcp/tools.d.ts +12 -12
  13. package/dist/lib/external/mcp/tools.js +120 -118
  14. package/dist/lib/http/middleware/validate.js +7 -3
  15. package/dist/lib/infrastructure/database/drizzle/schema.d.ts +100 -0
  16. package/dist/lib/infrastructure/database/drizzle/schema.js +10 -0
  17. package/dist/lib/infrastructure/database/migrations/005_recipe_source_refs.d.ts +9 -0
  18. package/dist/lib/infrastructure/database/migrations/005_recipe_source_refs.js +24 -0
  19. package/dist/lib/infrastructure/vector/HnswVectorAdapter.js +18 -2
  20. package/dist/lib/injection/ServiceContainer.js +2 -0
  21. package/dist/lib/injection/modules/KnowledgeModule.d.ts +5 -0
  22. package/dist/lib/injection/modules/KnowledgeModule.js +80 -0
  23. package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +45 -0
  24. package/dist/lib/service/bootstrap/UiStartupTasks.js +101 -0
  25. package/dist/lib/service/evolution/ConsolidationAdvisor.js +9 -9
  26. package/dist/lib/service/evolution/ContradictionDetector.js +2 -2
  27. package/dist/lib/service/evolution/RedundancyAnalyzer.js +2 -2
  28. package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +68 -0
  29. package/dist/lib/service/knowledge/SourceRefReconciler.js +309 -0
  30. package/dist/lib/service/panorama/PanoramaService.d.ts +18 -1
  31. package/dist/lib/service/panorama/PanoramaService.js +148 -5
  32. package/dist/lib/service/search/BM25Scorer.d.ts +2 -2
  33. package/dist/lib/service/search/CoarseRanker.d.ts +7 -6
  34. package/dist/lib/service/search/CoarseRanker.js +11 -10
  35. package/dist/lib/service/search/FieldWeightedScorer.d.ts +81 -0
  36. package/dist/lib/service/search/FieldWeightedScorer.js +318 -0
  37. package/dist/lib/service/search/MultiSignalRanker.d.ts +2 -2
  38. package/dist/lib/service/search/MultiSignalRanker.js +1 -1
  39. package/dist/lib/service/search/SearchEngine.d.ts +8 -7
  40. package/dist/lib/service/search/SearchEngine.js +59 -10
  41. package/dist/lib/service/search/SearchTypes.d.ts +23 -3
  42. package/dist/lib/service/search/SearchTypes.js +6 -1
  43. package/dist/lib/service/task/IntentExtractor.d.ts +11 -1
  44. package/dist/lib/service/task/IntentExtractor.js +137 -3
  45. package/dist/lib/service/task/PrimeSearchPipeline.js +95 -25
  46. package/dist/lib/service/vector/VectorService.d.ts +3 -0
  47. package/dist/lib/service/vector/VectorService.js +38 -4
  48. package/dist/lib/shared/schemas/mcp-tools.d.ts +1 -0
  49. package/dist/lib/shared/schemas/mcp-tools.js +5 -1
  50. package/package.json +1 -1
  51. package/skills/autosnippet-create/SKILL.md +98 -89
  52. package/skills/autosnippet-devdocs/SKILL.md +55 -60
  53. package/templates/guard-ci.yml +2 -2
  54. package/templates/instructions/conventions.md +4 -2
  55. package/templates/recipes-setup/_template.md +39 -39
@@ -57,6 +57,23 @@ export interface PanoramaModuleDetail {
57
57
  direction: 'in' | 'out';
58
58
  weight: number;
59
59
  }>;
60
+ /** File groups by subdirectory within the module */
61
+ fileGroups: Array<{
62
+ group: string;
63
+ files: string[];
64
+ count: number;
65
+ }>;
66
+ /** Recipes matched to this module (by category/trigger/file path) */
67
+ recipes: Array<{
68
+ id: string;
69
+ title: string;
70
+ trigger: string;
71
+ kind: string;
72
+ }>;
73
+ /** Files not covered by any matched recipe */
74
+ uncoveredFileCount: number;
75
+ /** Auto-generated structural summary for the agent */
76
+ summary: string;
60
77
  }
61
78
  export interface PanoramaHealth {
62
79
  /** 多维度知识健康雷达 */
@@ -77,7 +94,7 @@ export declare class PanoramaService {
77
94
  */
78
95
  getOverview(): PanoramaOverview;
79
96
  /**
80
- * 获取单模块详情
97
+ * 获取单模块详情 (enriched with file groups, recipes, and summary)
81
98
  */
82
99
  getModule(moduleName: string): PanoramaModuleDetail | null;
83
100
  /**
@@ -96,7 +96,7 @@ export class PanoramaService {
96
96
  return overview;
97
97
  }
98
98
  /**
99
- * 获取单模块详情
99
+ * 获取单模块详情 (enriched with file groups, recipes, and summary)
100
100
  */
101
101
  getModule(moduleName) {
102
102
  const result = this.#getOrCompute();
@@ -104,9 +104,16 @@ export class PanoramaService {
104
104
  if (!mod) {
105
105
  return null;
106
106
  }
107
- // 找到该模块所在层级
108
- const layerName = result.layers.levels.find((l) => l.modules.includes(moduleName))?.name ?? 'Unknown';
109
- // DB 查邻居边
107
+ // Layer name: derive from module's own refinedRole (more accurate than level vote)
108
+ const layerName = PanoramaService.#roleToLayer(mod.refinedRole || mod.inferredRole);
109
+ // File groups: group by immediate subdirectory within the module
110
+ const fileGroups = PanoramaService.#groupFilesBySubdir(mod.files);
111
+ // Matched recipes from DB
112
+ const recipes = this.#findModuleRecipes(moduleName, mod);
113
+ // Uncovered file count estimate
114
+ const coveredFileCount = Math.min(recipes.length * 2, mod.fileCount); // rough heuristic
115
+ const uncoveredFileCount = Math.max(0, mod.fileCount - coveredFileCount);
116
+ // Neighbors from DB edges
110
117
  const neighbors = [];
111
118
  const outNeighbors = this.#db
112
119
  .prepare(`SELECT DISTINCT to_id, weight FROM knowledge_edges
@@ -130,7 +137,143 @@ export class PanoramaService {
130
137
  weight: Number(n.weight ?? 1),
131
138
  });
132
139
  }
133
- return { module: mod, layerName, neighbors };
140
+ // Generate summary
141
+ const summary = PanoramaService.#generateModuleSummary(mod, layerName, fileGroups, recipes, neighbors);
142
+ return { module: mod, layerName, neighbors, fileGroups, recipes, uncoveredFileCount, summary };
143
+ }
144
+ /* ─── Module detail helpers ─────────────────────── */
145
+ /** Role → layer name mapping (consistent with PanoramaAggregator) */
146
+ static #roleToLayer(role) {
147
+ const map = {
148
+ core: 'Foundation',
149
+ foundation: 'Foundation',
150
+ model: 'Model',
151
+ service: 'Service',
152
+ networking: 'Infrastructure',
153
+ storage: 'Infrastructure',
154
+ ui: 'UI',
155
+ feature: 'Feature',
156
+ config: 'Configuration',
157
+ test: 'Test',
158
+ app: 'Application',
159
+ };
160
+ return map[role] ?? 'Feature';
161
+ }
162
+ /** Group file paths by their immediate subdirectory within the module */
163
+ static #groupFilesBySubdir(files) {
164
+ if (files.length === 0) {
165
+ return [];
166
+ }
167
+ // Find common prefix to determine module root
168
+ const prefix = PanoramaService.#commonPathPrefix(files);
169
+ const groups = new Map();
170
+ for (const f of files) {
171
+ const relative = f.slice(prefix.length);
172
+ const firstSlash = relative.indexOf('/');
173
+ const group = firstSlash > 0 ? relative.slice(0, firstSlash) : '(root)';
174
+ if (!groups.has(group)) {
175
+ groups.set(group, []);
176
+ }
177
+ groups.get(group).push(f);
178
+ }
179
+ return [...groups.entries()]
180
+ .sort((a, b) => b[1].length - a[1].length)
181
+ .map(([group, groupFiles]) => ({ group, files: groupFiles, count: groupFiles.length }));
182
+ }
183
+ static #commonPathPrefix(paths) {
184
+ if (paths.length === 0) {
185
+ return '';
186
+ }
187
+ let prefix = paths[0];
188
+ for (const p of paths) {
189
+ while (!p.startsWith(prefix)) {
190
+ const lastSlash = prefix.lastIndexOf('/');
191
+ if (lastSlash < 0) {
192
+ return '';
193
+ }
194
+ prefix = prefix.slice(0, lastSlash + 1);
195
+ }
196
+ }
197
+ return prefix;
198
+ }
199
+ /** Find recipes related to this module by category, trigger, or title match */
200
+ #findModuleRecipes(moduleName, mod) {
201
+ try {
202
+ // Map refined role to typical recipe categories
203
+ const roleCategories = {
204
+ networking: ['Network', 'API', 'Http'],
205
+ storage: ['Storage', 'Database', 'Cache'],
206
+ ui: ['UI', 'View', 'Component'],
207
+ service: ['Service', 'Manager'],
208
+ model: ['Model', 'Entity'],
209
+ core: ['Core', 'Foundation', 'Utility'],
210
+ foundation: ['Core', 'Foundation', 'Utility'],
211
+ feature: ['Feature'],
212
+ };
213
+ const categories = roleCategories[mod.refinedRole] ?? [];
214
+ // Build a LIKE query that matches module name or related categories
215
+ const conditions = [];
216
+ const params = [];
217
+ // Match by module name in title or trigger
218
+ conditions.push('(title LIKE ? OR trigger LIKE ?)');
219
+ params.push(`%${moduleName}%`, `%${moduleName}%`);
220
+ // Match by category
221
+ for (const cat of categories) {
222
+ conditions.push('category = ?');
223
+ params.push(cat);
224
+ }
225
+ const whereClause = conditions.join(' OR ');
226
+ const rows = this.#db
227
+ .prepare(`SELECT id, title, trigger, kind FROM knowledge_entries
228
+ WHERE lifecycle IN ('active', 'staging', 'pending')
229
+ AND (${whereClause})
230
+ ORDER BY lifecycle ASC
231
+ LIMIT 20`)
232
+ .all(...params);
233
+ return rows.map((r) => ({
234
+ id: String(r.id ?? ''),
235
+ title: String(r.title ?? ''),
236
+ trigger: String(r.trigger ?? ''),
237
+ kind: String(r.kind ?? ''),
238
+ }));
239
+ }
240
+ catch {
241
+ return [];
242
+ }
243
+ }
244
+ /** Generate a structural summary for the agent */
245
+ static #generateModuleSummary(mod, layerName, fileGroups, recipes, neighbors) {
246
+ const lines = [];
247
+ // Identity
248
+ lines.push(`${mod.name} is a ${layerName} layer module (role: ${mod.refinedRole}, confidence: ${(mod.roleConfidence * 100).toFixed(0)}%).`);
249
+ // Structure
250
+ const groupDesc = fileGroups.map((g) => `${g.group}(${g.count})`).join(', ');
251
+ lines.push(`Contains ${mod.fileCount} files in ${fileGroups.length} groups: ${groupDesc}.`);
252
+ // Dependencies
253
+ const dependsOn = neighbors.filter((n) => n.direction === 'out').map((n) => n.name);
254
+ const usedBy = neighbors.filter((n) => n.direction === 'in').map((n) => n.name);
255
+ if (dependsOn.length > 0) {
256
+ lines.push(`Depends on: ${dependsOn.join(', ')}.`);
257
+ }
258
+ if (usedBy.length > 0) {
259
+ lines.push(`Used by: ${usedBy.join(', ')}.`);
260
+ }
261
+ if (dependsOn.length === 0 && usedBy.length === 0) {
262
+ lines.push('No dependency edges recorded (consider running a full bootstrap scan).');
263
+ }
264
+ // Knowledge coverage
265
+ lines.push(`Knowledge coverage: ${recipes.length} recipes matched, ${(mod.coverageRatio * 100).toFixed(0)}% estimated coverage.`);
266
+ if (recipes.length > 0) {
267
+ const recipeList = recipes
268
+ .slice(0, 5)
269
+ .map((r) => r.title)
270
+ .join('; ');
271
+ lines.push(`Key recipes: ${recipeList}.`);
272
+ }
273
+ if (mod.coverageRatio < 0.5) {
274
+ lines.push('Coverage is below 50% — consider submitting knowledge for uncovered file groups.');
275
+ }
276
+ return lines.join(' ');
134
277
  }
135
278
  /**
136
279
  * 获取知识空白区
@@ -6,9 +6,9 @@
6
6
  *
7
7
  * @module BM25Scorer
8
8
  */
9
- import type { BM25Document, BM25SearchResult } from './SearchTypes.js';
9
+ import type { BM25Document, BM25SearchResult, Scorer } from './SearchTypes.js';
10
10
  /** BM25 评分器 */
11
- export declare class BM25Scorer {
11
+ export declare class BM25Scorer implements Scorer {
12
12
  _idIndex: Map<string, number>;
13
13
  _totalLength: number;
14
14
  avgLength: number;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * CoarseRanker — 粗排器
3
- * 基于 E-E-A-T 标准的 5 维加权排序(BM25 + Semantic + Quality + Freshness + Popularity)
3
+ * 多维加权排序(Recall + Semantic + Freshness + Popularity)
4
+ * Quality 维度保留但默认权重 0 — 待场景化区分后按需启用
4
5
  */
5
6
  interface RankerCandidate {
6
- bm25Score?: number;
7
+ recallScore?: number;
7
8
  score?: number;
8
9
  semanticScore?: number;
9
10
  title?: string;
@@ -23,7 +24,7 @@ interface RankerCandidate {
23
24
  export declare class CoarseRanker {
24
25
  #private;
25
26
  constructor(options?: {
26
- bm25Weight?: number;
27
+ recallWeight?: number;
27
28
  semanticWeight?: number;
28
29
  qualityWeight?: number;
29
30
  freshnessWeight?: number;
@@ -31,19 +32,19 @@ export declare class CoarseRanker {
31
32
  });
32
33
  /**
33
34
  * 粗排
34
- * @param candidates 需有 bm25Score、semanticScore 等字段
35
+ * @param candidates 需有 recallScore、semanticScore 等字段
35
36
  * @returns sorted with coarseScore
36
37
  */
37
38
  rank(candidates: RankerCandidate[]): {
38
39
  coarseScore: number;
39
40
  coarseSignals: {
40
- bm25: number;
41
+ recall: number;
41
42
  semantic: number;
42
43
  quality: number;
43
44
  freshness: number;
44
45
  popularity: number;
45
46
  };
46
- bm25Score?: number;
47
+ recallScore?: number;
47
48
  score?: number;
48
49
  semanticScore?: number;
49
50
  title?: string;
@@ -1,21 +1,22 @@
1
1
  /**
2
2
  * CoarseRanker — 粗排器
3
- * 基于 E-E-A-T 标准的 5 维加权排序(BM25 + Semantic + Quality + Freshness + Popularity)
3
+ * 多维加权排序(Recall + Semantic + Freshness + Popularity)
4
+ * Quality 维度保留但默认权重 0 — 待场景化区分后按需启用
4
5
  */
5
6
  export class CoarseRanker {
6
7
  #weights;
7
8
  constructor(options = {}) {
8
9
  this.#weights = {
9
- bm25: options.bm25Weight ?? 0.3,
10
+ recall: options.recallWeight ?? 0.45,
10
11
  semantic: options.semanticWeight ?? 0.3,
11
- quality: options.qualityWeight ?? 0.2,
12
- freshness: options.freshnessWeight ?? 0.1,
12
+ quality: options.qualityWeight ?? 0,
13
+ freshness: options.freshnessWeight ?? 0.15,
13
14
  popularity: options.popularityWeight ?? 0.1,
14
15
  };
15
16
  }
16
17
  /**
17
18
  * 粗排
18
- * @param candidates 需有 bm25Score、semanticScore 等字段
19
+ * @param candidates 需有 recallScore、semanticScore 等字段
19
20
  * @returns sorted with coarseScore
20
21
  */
21
22
  rank(candidates) {
@@ -38,16 +39,16 @@ export class CoarseRanker {
38
39
  }
39
40
  effectiveWeights.semantic = 0;
40
41
  }
41
- // BM25 分数 max-based 归一化(保留相对排序,避免 clamp 截断高分差异)
42
- const maxBm25 = candidates.reduce((m, c) => Math.max(m, c.bm25Score || c.score || 0), 0) || 1;
42
+ // 召回分数 max-based 归一化(保留相对排序,避免 clamp 截断高分差异)
43
+ const maxRecall = candidates.reduce((m, c) => Math.max(m, c.recallScore || c.score || 0), 0) || 1;
43
44
  return candidates
44
45
  .map((c) => {
45
- const bm25 = Math.min((c.bm25Score || c.score || 0) / maxBm25, 1.0);
46
+ const recall = Math.min((c.recallScore || c.score || 0) / maxRecall, 1.0);
46
47
  const semantic = this.#normalize(c.semanticScore || 0);
47
48
  const quality = this.#computeQuality(c);
48
49
  const freshness = this.#computeFreshness(c);
49
50
  const popularity = this.#computePopularity(c);
50
- const coarseScore = bm25 * effectiveWeights.bm25 +
51
+ const coarseScore = recall * effectiveWeights.recall +
51
52
  semantic * effectiveWeights.semantic +
52
53
  quality * effectiveWeights.quality +
53
54
  freshness * effectiveWeights.freshness +
@@ -55,7 +56,7 @@ export class CoarseRanker {
55
56
  return {
56
57
  ...c,
57
58
  coarseScore,
58
- coarseSignals: { bm25, semantic, quality, freshness, popularity },
59
+ coarseSignals: { recall, semantic, quality, freshness, popularity },
59
60
  };
60
61
  })
61
62
  .sort((a, b) => b.coarseScore - a.coarseScore);
@@ -0,0 +1,81 @@
1
+ /**
2
+ * FieldWeightedScorer — 加权字段匹配评分器
3
+ *
4
+ * 替代 BM25Scorer 作为结构化知识库的默认搜索评分引擎。
5
+ *
6
+ * 设计动机:
7
+ * - BM25 将所有字段拼接为文本做统计评分,tokenize 去重导致 TF 恒为 1,BM25F boost 失效
8
+ * - 对于 ~50–500 条结构化知识条目,BM25 的大规模语料假设不成立
9
+ * - FieldWeightedScorer 对每个字段独立打分并加权合并,精确匹配 > token 重叠 > IDF 加权
10
+ *
11
+ * 字段权重:
12
+ * trigger (5.0) > title (3.0) > tags (2.0) > description (1.5) > content (1.0) > facets (0.5)
13
+ *
14
+ * @module FieldWeightedScorer
15
+ */
16
+ import type { BM25SearchResult, Scorer } from './SearchTypes.js';
17
+ /** 字段加权文档内部表示 */
18
+ interface FieldWeightedDocument {
19
+ id: string;
20
+ fields: {
21
+ trigger: string;
22
+ title: string;
23
+ description: string;
24
+ tags: string[];
25
+ language: string;
26
+ category: string;
27
+ knowledgeType: string;
28
+ };
29
+ tokenizedFields: {
30
+ trigger: string[];
31
+ title: string[];
32
+ description: string[];
33
+ content: string[];
34
+ allUnique: Set<string>;
35
+ };
36
+ meta: Record<string, unknown>;
37
+ }
38
+ /**
39
+ * FieldWeightedScorer — 加权字段匹配评分器
40
+ *
41
+ * 接口与 BM25Scorer 完全兼容(实现 Scorer 接口),可作为 drop-in 替换。
42
+ */
43
+ export declare class FieldWeightedScorer implements Scorer {
44
+ avgLength: number;
45
+ docFreq: Record<string, number>;
46
+ documents: (FieldWeightedDocument | null)[];
47
+ totalDocs: number;
48
+ _idIndex: Map<string, number>;
49
+ _totalLength: number;
50
+ constructor();
51
+ /** 添加文档到索引 */
52
+ addDocument(id: string, text: string, meta?: Record<string, unknown>): void;
53
+ /**
54
+ * 移除文档(tombstone + 懒压缩)
55
+ * @returns 是否成功移除
56
+ */
57
+ removeDocument(id: string): boolean;
58
+ /** 更新文档(remove + add) */
59
+ updateDocument(id: string, text: string, meta?: Record<string, unknown>): void;
60
+ /** 检查文档是否存在 */
61
+ hasDocument(id: string): boolean;
62
+ /** 清空索引 */
63
+ clear(): void;
64
+ /** 压缩 documents 数组,清除 tombstone 空洞 */
65
+ _compact(): void;
66
+ /** 搜索:对每个文档按字段加权评分,返回降序结果 */
67
+ search(query: string, limit?: number): BM25SearchResult[];
68
+ /** 字符串级别匹配评分(用于 trigger / title) */
69
+ _stringMatchScore(query: string, field: string): number;
70
+ /** Token 集合重叠率(查询侧召回) */
71
+ _tokenOverlap(queryTokens: string[], fieldTokens: string[]): number;
72
+ /** IDF 加权 token overlap(用于长文本字段) */
73
+ _idfWeightedOverlap(queryTokens: string[], fieldTokens: string[]): number;
74
+ /** Tag 匹配评分 */
75
+ _tagScore(queryTokens: string[], tags: string[]): number;
76
+ /** Facet 匹配评分(language / category / knowledgeType) */
77
+ _facetScore(queryTokens: string[], fields: FieldWeightedDocument['fields']): number;
78
+ /** 计算 IDF(平滑,始终为正) */
79
+ _idf(token: string): number;
80
+ }
81
+ export {};