autosnippet 3.3.9 → 3.4.1

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 (68) hide show
  1. package/README.md +44 -19
  2. package/config/default.json +1 -1
  3. package/dashboard/dist/assets/{index-DEU4tJtP.js → index-BX6r2fiy.js} +40 -40
  4. package/dashboard/dist/assets/index-BvZcGN02.css +1 -0
  5. package/dashboard/dist/index.html +2 -2
  6. package/dist/lib/agent/AgentRuntime.js +13 -1
  7. package/dist/lib/agent/AgentRuntimeTypes.d.ts +2 -0
  8. package/dist/lib/agent/PipelineStrategy.js +32 -2
  9. package/dist/lib/agent/context/ContextWindow.d.ts +2 -1
  10. package/dist/lib/agent/context/ContextWindow.js +18 -4
  11. package/dist/lib/agent/context/ExplorationTracker.js +6 -1
  12. package/dist/lib/agent/context/exploration/ExplorationStrategies.js +2 -1
  13. package/dist/lib/agent/core/LoopContext.d.ts +3 -0
  14. package/dist/lib/agent/core/LoopContext.js +3 -0
  15. package/dist/lib/agent/domain/EpisodicConsolidator.d.ts +5 -0
  16. package/dist/lib/agent/domain/EpisodicConsolidator.js +60 -5
  17. package/dist/lib/agent/domain/insight-analyst.d.ts +16 -0
  18. package/dist/lib/agent/domain/insight-analyst.js +38 -0
  19. package/dist/lib/agent/domain/insight-gate.js +12 -0
  20. package/dist/lib/agent/memory/MemoryConsolidator.js +17 -0
  21. package/dist/lib/core/AstAnalyzer.js +0 -1
  22. package/dist/lib/core/ast/lang-dart.js +0 -1
  23. package/dist/lib/core/ast/lang-go.js +0 -1
  24. package/dist/lib/core/ast/lang-java.js +0 -1
  25. package/dist/lib/core/ast/lang-javascript.js +0 -1
  26. package/dist/lib/core/ast/lang-objc.js +0 -1
  27. package/dist/lib/core/ast/lang-python.js +0 -1
  28. package/dist/lib/core/ast/lang-rust.js +0 -1
  29. package/dist/lib/core/ast/lang-swift.js +0 -1
  30. package/dist/lib/core/ast/lang-typescript.js +0 -1
  31. package/dist/lib/domain/dimension/DimensionRegistry.d.ts +6 -4
  32. package/dist/lib/domain/dimension/DimensionRegistry.js +19 -23
  33. package/dist/lib/external/ai/AiFactory.d.ts +14 -0
  34. package/dist/lib/external/ai/AiFactory.js +33 -1
  35. package/dist/lib/external/ai/AiProvider.d.ts +2 -0
  36. package/dist/lib/external/ai/AiProvider.js +4 -0
  37. package/dist/lib/external/ai/providers/ClaudeProvider.d.ts +1 -1
  38. package/dist/lib/external/ai/providers/ClaudeProvider.js +6 -2
  39. package/dist/lib/external/ai/providers/GoogleGeminiProvider.d.ts +1 -1
  40. package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +13 -5
  41. package/dist/lib/external/ai/providers/OpenAiProvider.d.ts +1 -1
  42. package/dist/lib/external/ai/providers/OpenAiProvider.js +8 -4
  43. package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +10 -2
  44. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.d.ts +1 -0
  45. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +39 -5
  46. package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.d.ts +2 -0
  47. package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +4 -0
  48. package/dist/lib/external/mcp/handlers/guard.js +11 -6
  49. package/dist/lib/http/routes/ai.js +20 -3
  50. package/dist/lib/infrastructure/vector/IndexingPipeline.js +6 -1
  51. package/dist/lib/injection/modules/AiModule.js +22 -1
  52. package/dist/lib/service/bootstrap/BootstrapTaskManager.d.ts +7 -0
  53. package/dist/lib/service/bootstrap/BootstrapTaskManager.js +17 -0
  54. package/dist/lib/service/guard/ComplianceReporter.js +5 -1
  55. package/dist/lib/service/guard/GuardCheckEngine.d.ts +12 -1
  56. package/dist/lib/service/guard/GuardCheckEngine.js +36 -4
  57. package/dist/lib/service/guard/GuardCodeChecks.js +27 -9
  58. package/dist/lib/service/guard/SourceFileCollector.d.ts +3 -2
  59. package/dist/lib/service/guard/SourceFileCollector.js +3 -3
  60. package/dist/lib/service/search/SearchEngine.js +165 -61
  61. package/dist/lib/service/task/PrimeSearchPipeline.js +17 -2
  62. package/dist/lib/service/vector/VectorService.js +10 -1
  63. package/dist/lib/shared/LanguageService.d.ts +12 -0
  64. package/dist/lib/shared/LanguageService.js +85 -0
  65. package/dist/lib/shared/schemas/http-requests.d.ts +4 -0
  66. package/dist/lib/shared/schemas/http-requests.js +4 -0
  67. package/package.json +1 -1
  68. package/dashboard/dist/assets/index-DHJ1Dj7u.css +0 -1
@@ -93,7 +93,11 @@ export class ComplianceReporter {
93
93
  try {
94
94
  const { initEnhancementRegistry } = await import('#core/enhancement/index.js');
95
95
  const enhReg = await initEnhancementRegistry();
96
- const allPacks = enhReg.all();
96
+ // 仅注入无框架条件的通用 Pack(有框架条件的由 Bootstrap resolve() 精确注入)
97
+ const allPacks = enhReg.all().filter((pack) => {
98
+ const cond = pack.conditions;
99
+ return !cond?.frameworks?.length;
100
+ });
97
101
  const allGuardRules = [];
98
102
  for (const pack of allPacks) {
99
103
  try {
@@ -29,8 +29,10 @@ interface BuiltInRule {
29
29
  fixSuggestion?: string;
30
30
  excludePaths?: RegExp;
31
31
  excludeLinePatterns?: string[];
32
+ excludePrevLinePatterns?: string[];
32
33
  skipComments?: boolean;
33
34
  skipTestBlocks?: boolean;
35
+ skipTestFiles?: boolean;
34
36
  }
35
37
  interface GuardRule {
36
38
  id: string;
@@ -46,8 +48,11 @@ interface GuardRule {
46
48
  fixSuggestion?: string | null;
47
49
  excludePaths?: RegExp | string;
48
50
  excludeLinePatterns?: string[];
51
+ excludePrevLinePatterns?: string[];
49
52
  skipComments?: boolean;
50
53
  skipTestBlocks?: boolean;
54
+ /** When true, this rule is skipped for test files (detected by LanguageService.isTestFile) */
55
+ skipTestFiles?: boolean;
51
56
  astQuery?: {
52
57
  queryType: string;
53
58
  params?: Record<string, string>;
@@ -108,6 +113,8 @@ interface AuditFileResult {
108
113
  interface AuditFilesInput {
109
114
  path: string;
110
115
  content: string;
116
+ /** Pre-computed test file flag from LanguageService.isTestFile */
117
+ isTest?: boolean;
111
118
  }
112
119
  export { detectLanguage } from './GuardPatternUtils.js';
113
120
  /** GuardCheckEngine - 核心检查引擎 */
@@ -146,12 +153,13 @@ export declare class GuardCheckEngine {
146
153
  * 对代码运行静态检查
147
154
  * @param code 源代码
148
155
  * @param language 'objc'|'swift'|'javascript' 等
149
- * @param options {scope, filePath}
156
+ * @param options {scope, filePath, isTest}
150
157
  * @returns >}
151
158
  */
152
159
  checkCode(code: string, language: string, options?: {
153
160
  scope?: string | null;
154
161
  filePath?: string;
162
+ isTest?: boolean;
155
163
  }): {
156
164
  reasoning: {
157
165
  whatViolated: string;
@@ -223,6 +231,7 @@ export declare class GuardCheckEngine {
223
231
  */
224
232
  auditFile(filePath: string, code: string, options?: {
225
233
  scope?: string;
234
+ isTest?: boolean;
226
235
  }): AuditFileResult;
227
236
  /**
228
237
  * 批量文件审计
@@ -237,6 +246,8 @@ export declare class GuardCheckEngine {
237
246
  crossFileViolations: import("./GuardCrossFileChecks.js").CrossFileViolation[];
238
247
  summary: {
239
248
  filesChecked: number;
249
+ testFiles: number;
250
+ productionFiles: number;
240
251
  totalViolations: number;
241
252
  totalErrors: number;
242
253
  totalUncertain: number;
@@ -261,6 +261,7 @@ const BUILT_IN_RULES = {
261
261
  languages: ['go'],
262
262
  dimension: 'file',
263
263
  category: 'correctness',
264
+ skipTestFiles: true,
264
265
  },
265
266
  'go-no-err-ignored': {
266
267
  message: '错误值不应用 _ 忽略,应处理或明确标注',
@@ -270,6 +271,11 @@ const BUILT_IN_RULES = {
270
271
  dimension: 'file',
271
272
  category: 'correctness',
272
273
  excludePaths: /(?:^|[/\\])(?:tests?|testdata|_test)[/\\]|_test\.go$/,
274
+ excludeLinePatterns: [
275
+ '\\.\\([^)]*\\)', // type assertion: val, _ := expr.(Type) — _ 是 bool ok,不是 error
276
+ 'RegisterFlagCompletionFunc', // cobra flag completion: flag 名由同函数字面量保证,不会失败
277
+ 'MarkFlagRequired', // cobra flag setup: 同上
278
+ ],
273
279
  },
274
280
  'go-no-init-abuse': {
275
281
  message: 'init() 函数副作用难以追踪,避免在 init 中执行复杂逻辑',
@@ -287,6 +293,14 @@ const BUILT_IN_RULES = {
287
293
  dimension: 'file',
288
294
  category: 'style',
289
295
  excludePaths: /(?:^|[/\\])(?:tests?|testdata)[/\\]|_test\.go$/,
296
+ excludeLinePatterns: [
297
+ '\\bembed\\.', // //go:embed requires package-level var
298
+ '\\bsync\\.', // sync.Map, sync.Once, sync.Mutex etc. are designed as package-level vars
299
+ '\\batomic\\.', // atomic.Pointer, atomic.Value etc.
300
+ ],
301
+ excludePrevLinePatterns: [
302
+ '//go:embed', // //go:embed directive on previous line requires package-level var
303
+ ],
290
304
  },
291
305
  // ══════════════════════════════════════════════════════════
292
306
  // Dart (Flutter)
@@ -623,7 +637,11 @@ export class GuardCheckEngine {
623
637
  ...(rule.excludePaths ? { excludePaths: rule.excludePaths } : {}),
624
638
  ...(rule.skipComments ? { skipComments: true } : {}),
625
639
  ...(rule.skipTestBlocks ? { skipTestBlocks: true } : {}),
640
+ ...(rule.skipTestFiles ? { skipTestFiles: true } : {}),
626
641
  ...(rule.excludeLinePatterns ? { excludeLinePatterns: rule.excludeLinePatterns } : {}),
642
+ ...(rule.excludePrevLinePatterns
643
+ ? { excludePrevLinePatterns: rule.excludePrevLinePatterns }
644
+ : {}),
627
645
  });
628
646
  }
629
647
  }
@@ -662,11 +680,11 @@ export class GuardCheckEngine {
662
680
  * 对代码运行静态检查
663
681
  * @param code 源代码
664
682
  * @param language 'objc'|'swift'|'javascript' 等
665
- * @param options {scope, filePath}
683
+ * @param options {scope, filePath, isTest}
666
684
  * @returns >}
667
685
  */
668
686
  checkCode(code, language, options = {}) {
669
- const { scope = null, filePath = '' } = options;
687
+ const { scope = null, filePath = '', isTest = false } = options;
670
688
  const violations = [];
671
689
  // 获取匹配语言的规则
672
690
  let rules = this.getRules(language);
@@ -680,6 +698,10 @@ export class GuardCheckEngine {
680
698
  return !re.test(filePath);
681
699
  });
682
700
  }
701
+ // 按 skipTestFiles 标记过滤测试文件
702
+ if (isTest) {
703
+ rules = rules.filter((r) => !r.skipTestFiles);
704
+ }
683
705
  // 如果有 scope,按层级过滤:project ⊇ target ⊇ file
684
706
  // project 范围包含所有维度的规则;target 包含 file+target;file 仅匹配 file
685
707
  // 'universal' 维度在所有 scope 下都生效
@@ -719,6 +741,7 @@ export class GuardCheckEngine {
719
741
  // 合并内置 + 配置级排除行模式
720
742
  const ruleId = rule.id || rule.name;
721
743
  const excludeLineRegexes = this._getExcludeLineRegexes(ruleId, rule.excludeLinePatterns);
744
+ const excludePrevLineRegexes = this._getExcludeLineRegexes(`${ruleId}:prev`, rule.excludePrevLinePatterns);
722
745
  for (let i = 0; i < lines.length; i++) {
723
746
  // skipComments: 跳过注释行(doc comments / 行注释 / 块注释内)
724
747
  if (shouldSkipComments && commentLines[i]) {
@@ -733,6 +756,12 @@ export class GuardCheckEngine {
733
756
  if (excludeLineRegexes.length > 0 && excludeLineRegexes.some((ep) => ep.test(lines[i]))) {
734
757
  continue;
735
758
  }
759
+ // excludePrevLinePatterns: 跳过前一行匹配排除模式的行(//go:embed 等指令注释)
760
+ if (excludePrevLineRegexes.length > 0 &&
761
+ i > 0 &&
762
+ excludePrevLineRegexes.some((ep) => ep.test(lines[i - 1]))) {
763
+ continue;
764
+ }
736
765
  violations.push({
737
766
  ruleId: rule.id || rule.name,
738
767
  message: rule.message,
@@ -1358,8 +1387,8 @@ export class GuardCheckEngine {
1358
1387
  const results = [];
1359
1388
  let totalViolations = 0;
1360
1389
  let totalErrors = 0;
1361
- for (const { path: filePath, content } of files) {
1362
- const result = this.auditFile(filePath, content, options);
1390
+ for (const { path: filePath, content, isTest } of files) {
1391
+ const result = this.auditFile(filePath, content, { ...options, isTest });
1363
1392
  results.push(result);
1364
1393
  totalViolations += result.summary.total;
1365
1394
  totalErrors += result.summary.errors;
@@ -1370,8 +1399,11 @@ export class GuardCheckEngine {
1370
1399
  });
1371
1400
  totalViolations += crossFileViolations.length;
1372
1401
  totalErrors += crossFileViolations.filter((v) => v.severity === 'error').length;
1402
+ const testFileCount = files.filter((f) => f.isTest).length;
1373
1403
  const summary = {
1374
1404
  filesChecked: results.length,
1405
+ testFiles: testFileCount,
1406
+ productionFiles: results.length - testFileCount,
1375
1407
  totalViolations,
1376
1408
  totalErrors,
1377
1409
  totalUncertain: results.reduce((s, r) => s + r.summary.uncertain, 0),
@@ -90,6 +90,7 @@ export function runCodeLevelChecks(code, language, lines, options = {}) {
90
90
  // ── Go ──
91
91
  if (language === 'go') {
92
92
  // defer 在循环内检查 — defer 在函数结束时才执行,循环内 defer 可能资源泄露
93
+ // 排除 go func() { defer ... } 模式(goroutine 内的 defer 是安全的)
93
94
  if (!isDisabled('go-defer-in-loop')) {
94
95
  let inLoop = false;
95
96
  for (let i = 0; i < lines.length; i++) {
@@ -98,15 +99,32 @@ export function runCodeLevelChecks(code, language, lines, options = {}) {
98
99
  inLoop = true;
99
100
  }
100
101
  if (inLoop && /^\s*defer\s/.test(lines[i])) {
101
- violations.push({
102
- ruleId: 'go-defer-in-loop',
103
- message: 'defer 在循环内会延迟到函数返回时才执行,可能导致资源泄露或大量堆积',
104
- severity: 'warning',
105
- line: i + 1,
106
- snippet: lines[i].trim().slice(0, 120),
107
- dimension: 'file',
108
- fixSuggestion: '将循环体提取到独立函数中,或手动调用 Close()',
109
- });
102
+ // 回溯找最近的作用域开场 { — 判断 defer 是否在匿名函数/goroutine 内
103
+ let insideAnonymousFunc = false;
104
+ let braceBalance = 0;
105
+ for (let j = i - 1; j >= 0; j--) {
106
+ const prev = lines[j];
107
+ braceBalance += (prev.match(/\}/g) || []).length;
108
+ braceBalance -= (prev.match(/\{/g) || []).length;
109
+ if (braceBalance < 0) {
110
+ // 找到包裹 defer 的最近 { — 检查该行是否包含 func 关键字
111
+ if (/\bfunc\b/.test(prev)) {
112
+ insideAnonymousFunc = true;
113
+ }
114
+ break;
115
+ }
116
+ }
117
+ if (!insideAnonymousFunc) {
118
+ violations.push({
119
+ ruleId: 'go-defer-in-loop',
120
+ message: 'defer 在循环内会延迟到函数返回时才执行,可能导致资源泄露或大量堆积',
121
+ severity: 'warning',
122
+ line: i + 1,
123
+ snippet: lines[i].trim().slice(0, 120),
124
+ dimension: 'file',
125
+ fixSuggestion: '将循环体提取到独立函数中,或手动调用 Close()',
126
+ });
127
+ }
110
128
  }
111
129
  // 简化: 遇到 } 且缩进回到顶层,认为循环结束
112
130
  if (inLoop && trimmed === '}' && (lines[i].match(/^\t/) || lines[i].match(/^}/))) {
@@ -19,10 +19,10 @@ export declare function collectSourceFiles(dir: string, options?: {
19
19
  maxFiles?: number;
20
20
  }): Promise<string[]>;
21
21
  /**
22
- * 收集源文件并读取内容
22
+ * 收集源文件并读取内容(带测试文件标记)
23
23
  * @param dir 根目录
24
24
  * @param options collectSourceFiles 选项
25
- * @returns >>}
25
+ * @returns { path, content, isTest }[]
26
26
  */
27
27
  export declare function collectSourceFilesWithContent(dir: string, options?: {
28
28
  extensions?: Set<string>;
@@ -31,6 +31,7 @@ export declare function collectSourceFilesWithContent(dir: string, options?: {
31
31
  }): Promise<{
32
32
  path: string;
33
33
  content: string;
34
+ isTest: boolean;
34
35
  }[]>;
35
36
  declare const _default: {
36
37
  collectSourceFiles: typeof collectSourceFiles;
@@ -82,10 +82,10 @@ export async function collectSourceFiles(dir, options = {}) {
82
82
  return files;
83
83
  }
84
84
  /**
85
- * 收集源文件并读取内容
85
+ * 收集源文件并读取内容(带测试文件标记)
86
86
  * @param dir 根目录
87
87
  * @param options collectSourceFiles 选项
88
- * @returns >>}
88
+ * @returns { path, content, isTest }[]
89
89
  */
90
90
  export async function collectSourceFilesWithContent(dir, options = {}) {
91
91
  const paths = await collectSourceFiles(dir, options);
@@ -93,7 +93,7 @@ export async function collectSourceFilesWithContent(dir, options = {}) {
93
93
  for (const filePath of paths) {
94
94
  try {
95
95
  const content = await readFile(filePath, 'utf-8');
96
- results.push({ path: filePath, content });
96
+ results.push({ path: filePath, content, isTest: LanguageService.isTestFile(filePath) });
97
97
  }
98
98
  catch {
99
99
  // 读取失败跳过
@@ -110,6 +110,7 @@ export class SearchEngine {
110
110
  async search(query, options = {}) {
111
111
  const { type = 'all', limit = 20, mode = 'keyword', context } = options;
112
112
  const shouldRank = options.rank ?? mode !== 'keyword';
113
+ const tSearchStart = performance.now();
113
114
  if (!query || !query.trim()) {
114
115
  return { items: [], total: 0, query };
115
116
  }
@@ -132,60 +133,62 @@ export class SearchEngine {
132
133
  let actualMode = mode;
133
134
  switch (mode) {
134
135
  case 'auto': {
135
- // 缓存 FieldWeighted 结果, 避免 RRF 降级时重复计算
136
- let cachedScorerItems = null;
137
- const getScorerResults = () => {
138
- if (!cachedScorerItems) {
139
- cachedScorerItems = this._scorerSearch(query, type, recallLimit);
140
- }
141
- return cachedScorerItems;
142
- };
143
- // 优先使用 VectorService hybridSearch (统一 RRF 融合)
144
- if (this.vectorService) {
145
- try {
146
- const sparseItems = getScorerResults();
147
- const rrfResults = await this.vectorService.hybridSearch(query, {
148
- topK: recallLimit,
149
- alpha: this._fusionSemanticWeight,
150
- sparseSearchFn: () => sparseItems,
136
+ // ── Weighted-First + Confidence Gate ──
137
+ // 先跑 weighted(~40ms),评估是否需要 embed(2-22s)
138
+ const weightedItems = this._scorerSearch(query, type, recallLimit);
139
+ const confidence = this.#computeWeightedConfidence(query, weightedItems, limit);
140
+ if (confidence >= 60 || !this.vectorService) {
141
+ // 高 confidence: weighted 已足够,跳过 embed
142
+ results = weightedItems;
143
+ actualMode = `auto(weighted-only,conf=${confidence})`;
144
+ this.logger.info(`[QueryRouter] skip-semantic: conf=${confidence} topScore=${weightedItems[0]?.score ?? 0} query="${query}"`);
145
+ break;
146
+ }
147
+ // confidence: 投入 embed,RRF 融合
148
+ // 自适应 alpha:confidence 越低 semantic 权重越高
149
+ // conf=0 → alpha=0.75, conf=30 → alpha=0.575, conf=55 → alpha=0.42
150
+ const adaptiveAlpha = this._fusionSemanticWeight + (0.75 - this._fusionSemanticWeight) * (1 - confidence / 60);
151
+ this.logger.info(`[QueryRouter] invoke-semantic: conf=${confidence} alpha=${adaptiveAlpha.toFixed(2)} topScore=${weightedItems[0]?.score ?? 0} query="${query}"`);
152
+ try {
153
+ const rrfResults = await this.vectorService.hybridSearch(query, {
154
+ topK: recallLimit,
155
+ alpha: adaptiveAlpha,
156
+ sparseSearchFn: () => weightedItems,
157
+ });
158
+ if (rrfResults.length > 0) {
159
+ results = rrfResults.map((r) => {
160
+ const base = r.data?.item ||
161
+ r.data ||
162
+ {};
163
+ const baseMeta = (base.metadata || {});
164
+ return {
165
+ id: r.id,
166
+ title: (base.title ||
167
+ baseMeta.title ||
168
+ r.id),
169
+ type: (base.type || 'recipe'),
170
+ kind: (base.kind ||
171
+ baseMeta.kind ||
172
+ 'pattern'),
173
+ status: (base.status ||
174
+ baseMeta.status ||
175
+ 'active'),
176
+ score: Math.round(r.score * 1000) / 1000,
177
+ content: base.content,
178
+ description: base.description,
179
+ };
151
180
  });
152
- if (rrfResults.length > 0) {
153
- results = rrfResults.map((r) => {
154
- const base = r.data?.item ||
155
- r.data ||
156
- {};
157
- const baseMeta = (base.metadata || {});
158
- return {
159
- id: r.id,
160
- title: (base.title ||
161
- baseMeta.title ||
162
- r.id),
163
- type: (base.type || 'recipe'),
164
- kind: (base.kind ||
165
- baseMeta.kind ||
166
- 'pattern'),
167
- status: (base.status ||
168
- baseMeta.status ||
169
- 'active'),
170
- score: Math.round(r.score * 1000) / 1000,
171
- content: base.content,
172
- description: base.description,
173
- };
174
- });
175
- this._supplementDetails(results);
176
- actualMode = 'auto(rrf)';
177
- break;
178
- }
179
- }
180
- catch {
181
- // VectorService RRF 失败, 降级到 min-max 融合
181
+ this._supplementDetails(results);
182
+ actualMode = `auto(rrf,conf=${confidence},α=${adaptiveAlpha.toFixed(2)})`;
183
+ break;
182
184
  }
183
185
  }
184
- // 降级: VectorService 不可用或 RRF 零结果 → 纯 FieldWeighted
185
- // 旧版在此做 BM25+semantic min-max 融合,但当 VectorService 不可用时
186
- // semantic 通常也会失败,最终退化为纯 FieldWeighted。简化为直接走 scorer。
187
- results = getScorerResults();
188
- actualMode = 'auto(weighted-only)';
186
+ catch {
187
+ // VectorService RRF 失败, 降级
188
+ }
189
+ // 降级: embed 失败 → 返回已有的 weighted 结果
190
+ results = weightedItems;
191
+ actualMode = `auto(weighted-fallback,conf=${confidence})`;
189
192
  break;
190
193
  }
191
194
  case 'weighted':
@@ -215,6 +218,9 @@ export class SearchEngine {
215
218
  type,
216
219
  ranked: shouldRank && results.length > 0,
217
220
  };
221
+ // ── 搜索计时日志 ──
222
+ const tSearchEnd = performance.now();
223
+ this.logger.info(`Search completed: mode=${actualMode} total=${results.length} time=${Math.round(tSearchEnd - tSearchStart)}ms ranked=${response.ranked} query="${query}"`);
218
224
  if (options.groupByKind) {
219
225
  response.byKind = { rule: [], pattern: [], fact: [] };
220
226
  for (const r of results) {
@@ -448,15 +454,20 @@ export class SearchEngine {
448
454
  let results = vectorResults.map((vr) => {
449
455
  const item = vr.item;
450
456
  const metadata = (item.metadata || {});
457
+ const rawId = item.id || '';
458
+ // 从 vector ID 提取 DB entryId: "entry_<uuid>" → "<uuid>"
459
+ const entryId = metadata.entryId || rawId.replace(/^entry_/, '');
451
460
  return {
452
- id: item.id || metadata.entryId || '',
453
- title: metadata.title || item.id || '',
461
+ id: entryId,
462
+ title: metadata.title || entryId,
454
463
  type: 'recipe',
455
464
  kind: metadata.kind || 'pattern',
456
465
  status: metadata.status || 'active',
457
466
  score: Math.round(vr.score * 1000) / 1000,
458
467
  };
459
468
  });
469
+ // 按 entryId 去重 — 同一 Recipe 的多个 chunk 只保留最高分
470
+ results = this.#deduplicateByEntryId(results);
460
471
  if (type !== 'all') {
461
472
  results = results.filter((r) => {
462
473
  if (type === 'rule') {
@@ -505,14 +516,20 @@ export class SearchEngine {
505
516
  vectorResults = await this.vectorStore.query(queryEmbedding, limit * 2);
506
517
  }
507
518
  if (vectorResults && vectorResults.length > 0) {
508
- let results = vectorResults.map((vr) => ({
509
- id: vr.id,
510
- title: vr.metadata?.title || vr.id,
511
- type: 'recipe',
512
- kind: vr.metadata?.kind || 'pattern',
513
- status: vr.metadata?.status || 'active',
514
- score: Math.round((vr.similarity || vr.score || 0) * 1000) / 1000,
515
- }));
519
+ let results = vectorResults.map((vr) => {
520
+ const rawId = vr.id || '';
521
+ const entryId = vr.metadata?.entryId || rawId.replace(/^entry_/, '');
522
+ return {
523
+ id: entryId,
524
+ title: vr.metadata?.title || entryId,
525
+ type: 'recipe',
526
+ kind: vr.metadata?.kind || 'pattern',
527
+ status: vr.metadata?.status || 'active',
528
+ score: Math.round((vr.similarity || vr.score || 0) * 1000) / 1000,
529
+ };
530
+ });
531
+ // 按 entryId 去重
532
+ results = this.#deduplicateByEntryId(results);
516
533
  if (type !== 'all') {
517
534
  results = results.filter((r) => {
518
535
  if (type === 'rule') {
@@ -542,6 +559,93 @@ export class SearchEngine {
542
559
  return { items: this._scorerSearch(query, type, limit), actualMode: 'weighted' };
543
560
  }
544
561
  }
562
+ /**
563
+ * 按 entryId 去重 — 同一 Recipe 的多个 chunk 只保留最高分的
564
+ * 解决向量搜索返回同一条目的多个 chunk 浪费结果位的问题
565
+ */
566
+ #deduplicateByEntryId(items) {
567
+ const seen = new Map();
568
+ for (const item of items) {
569
+ const existing = seen.get(item.id);
570
+ if (!existing || (item.score ?? 0) > (existing.score ?? 0)) {
571
+ seen.set(item.id, item);
572
+ }
573
+ }
574
+ return [...seen.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
575
+ }
576
+ /**
577
+ * 评估 weighted 搜索结果的 confidence,决定是否需要语义搜索
578
+ * 返回 0-100 的分数,>= 60 跳过语义
579
+ */
580
+ #computeWeightedConfidence(query, items, requestedLimit) {
581
+ let score = 0;
582
+ // ── 结果质量信号 ──
583
+ // FieldWeightedScorer 分数范围约 0-20,归一化后判断
584
+ const topScore = items[0]?.score ?? 0;
585
+ const secondScore = items[1]?.score ?? 0;
586
+ // top1 与 top2 分差大 → 明确命中
587
+ if (items.length >= 2 && topScore > 0) {
588
+ const relativeGap = (topScore - secondScore) / topScore;
589
+ if (relativeGap > 0.3) {
590
+ score += 25;
591
+ }
592
+ else if (relativeGap > 0.15) {
593
+ score += 15;
594
+ }
595
+ }
596
+ // title/trigger 匹配(子串级别)
597
+ const lq = query.toLowerCase();
598
+ const matchLevel = items.slice(0, 3).reduce((best, it) => {
599
+ const t = (it.title || '').toLowerCase();
600
+ const tr = (it.trigger || '').toLowerCase();
601
+ if (t === lq || tr === lq || tr === `@${lq}`) {
602
+ return Math.max(best, 3); // 完全匹配
603
+ }
604
+ if (t.includes(lq) || tr.includes(lq)) {
605
+ return Math.max(best, 2); // 子串匹配
606
+ }
607
+ if (lq.includes(t) && t.length > 3) {
608
+ return Math.max(best, 1); // 反向包含
609
+ }
610
+ return best;
611
+ }, 0);
612
+ if (matchLevel === 3) {
613
+ score += 50;
614
+ }
615
+ else if (matchLevel === 2) {
616
+ score += 35;
617
+ }
618
+ else if (matchLevel === 1) {
619
+ score += 15;
620
+ }
621
+ // 代码术语检测(CamelCase、snake_case、@trigger)
622
+ if (/^[A-Z][a-zA-Z0-9]+$/.test(query) ||
623
+ /^[a-z]+(_[a-z]+)+$/.test(query) ||
624
+ query.startsWith('@')) {
625
+ score += 25;
626
+ }
627
+ // 候选充足
628
+ if (items.length >= requestedLimit) {
629
+ score += 10;
630
+ }
631
+ // ── 查询特征信号(降低 confidence → 倾向调用语义)──
632
+ // 中文自然语言疑问句
633
+ if (/[如怎什为何哪]么?|是否|有没有|都有哪些|应该|需要/.test(query)) {
634
+ score -= 40;
635
+ }
636
+ // 英文自然语言问句
637
+ if (/^(how|what|why|when|where|which|can|does|is|should)\b/i.test(query)) {
638
+ score -= 40;
639
+ }
640
+ // 较长查询(可能是描述性语句)
641
+ if (query.length > 20) {
642
+ score -= 20;
643
+ }
644
+ else if (query.length > 10) {
645
+ score -= 10;
646
+ }
647
+ return Math.max(0, Math.min(100, score));
648
+ }
545
649
  /**
546
650
  * 补充详细字段(content / description / trigger / delivery 字段)— 批量 IN 查询
547
651
  * 用于向量搜索结果与 FieldWeighted 结果的一致性
@@ -108,15 +108,30 @@ export class PrimeSearchPipeline {
108
108
  const autoPromises = autoQueries.map((q) => this.#search
109
109
  .search(q, { mode: 'auto', limit: 8, rank: false, context })
110
110
  .catch(() => ({ items: [] })));
111
+ // Semantic-mode search for primary query — ensures semantic is always
112
+ // part of RRF fusion even when auto mode skips it (confidence ≥ 60)
113
+ const semanticPromise = autoQueries[0]
114
+ ? this.#search
115
+ .search(autoQueries[0], { mode: 'semantic', limit: 6, rank: false })
116
+ .catch(() => ({ items: [] }))
117
+ : Promise.resolve({ items: [] });
111
118
  // Keyword-mode searches (raw FWS scores — for cross-language synonym matching)
112
119
  const kwPromises = keywordQueries.map((q) => this.#search
113
120
  .search(q, { mode: 'keyword', limit: 8, rank: false })
114
121
  .catch(() => ({ items: [] })));
115
- const [autoResponses, kwResponses] = await Promise.all([
122
+ const [autoResponses, kwResponses, semanticResponse] = await Promise.all([
116
123
  Promise.all(autoPromises),
117
124
  Promise.all(kwPromises),
125
+ semanticPromise,
118
126
  ]);
119
- const allResponses = [...autoResponses, ...kwResponses];
127
+ // Merge: auto + semantic + keyword
128
+ const semanticItems = (semanticResponse.items ||
129
+ []);
130
+ const allResponses = [
131
+ ...autoResponses,
132
+ ...(semanticItems.length > 0 ? [semanticResponse] : []),
133
+ ...kwResponses,
134
+ ];
120
135
  // Single-query shortcut: preserve original scores from search engine.
121
136
  // RRF is pointless with one response — it just converts rank to score,
122
137
  // discarding the magnitude information from BM25/CoarseRanker.
@@ -201,13 +201,18 @@ export class VectorService {
201
201
  }
202
202
  const { topK = 10, filter = null, minScore = 0 } = opts;
203
203
  try {
204
+ const t0 = performance.now();
204
205
  const embedResult = await this.#embedProvider.embed(query);
206
+ const tEmbed = performance.now();
205
207
  const queryVector = Array.isArray(embedResult[0]) ? embedResult[0] : embedResult;
206
- return this.#vectorStore.searchVector(queryVector, {
208
+ const results = await this.#vectorStore.searchVector(queryVector, {
207
209
  topK,
208
210
  filter,
209
211
  minScore,
210
212
  });
213
+ const tHnsw = performance.now();
214
+ this.#logger.info(`[VectorService] search: embed=${Math.round(tEmbed - t0)}ms hnsw=${Math.round(tHnsw - tEmbed)}ms total=${Math.round(tHnsw - t0)}ms results=${results.length}`);
215
+ return results;
211
216
  }
212
217
  catch (err) {
213
218
  this.#logger.warn('[VectorService] search failed', {
@@ -240,6 +245,7 @@ export class VectorService {
240
245
  // Embed query — circuit breaker skips embed after repeated failures
241
246
  let queryVector = null;
242
247
  const circuitOpen = Date.now() < this.#embedCircuitOpenUntil;
248
+ const tEmbedStart = performance.now();
243
249
  if (circuitOpen) {
244
250
  this.#logger.debug('[VectorService] embed circuit open, skipping embed');
245
251
  }
@@ -267,12 +273,15 @@ export class VectorService {
267
273
  }
268
274
  }
269
275
  }
276
+ const tEmbedEnd = performance.now();
270
277
  try {
271
278
  const fused = await this.#hybridRetriever.search(query, queryVector, {
272
279
  topK,
273
280
  alpha,
274
281
  sparseSearchFn: sparseSearchFn ?? undefined,
275
282
  });
283
+ const tFuseEnd = performance.now();
284
+ this.#logger.info(`[VectorService] hybridSearch: embed=${Math.round(tEmbedEnd - tEmbedStart)}ms fuse=${Math.round(tFuseEnd - tEmbedEnd)}ms total=${Math.round(tFuseEnd - tEmbedStart)}ms hasVector=${!!queryVector} results=${fused.length} alpha=${alpha}`);
276
285
  return fused.map((r) => ({
277
286
  id: r.id || '',
278
287
  score: r.score || 0,
@@ -161,5 +161,17 @@ export declare class LanguageService {
161
161
  discovererIds?: string[];
162
162
  maxDepth?: number;
163
163
  }): unknown[];
164
+ /**
165
+ * 判定文件路径是否为测试文件
166
+ *
167
+ * 两层判定:
168
+ * 1. 语言特定的文件名模式(_test.go, .test.ts, test_*.py 等)
169
+ * 2. 通用测试目录模式(test/, tests/, __tests__/, spec/ 等)
170
+ *
171
+ * @param filePath 文件路径(相对或绝对均可)
172
+ * @param [language] 已知语言 ID,省略时从扩展名推断
173
+ * @returns 是否为测试文件
174
+ */
175
+ static isTestFile(filePath: string, language?: string): boolean;
164
176
  }
165
177
  export default LanguageService;