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.
- package/README.md +44 -19
- package/config/default.json +1 -1
- package/dashboard/dist/assets/{index-DEU4tJtP.js → index-BX6r2fiy.js} +40 -40
- package/dashboard/dist/assets/index-BvZcGN02.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/lib/agent/AgentRuntime.js +13 -1
- package/dist/lib/agent/AgentRuntimeTypes.d.ts +2 -0
- package/dist/lib/agent/PipelineStrategy.js +32 -2
- package/dist/lib/agent/context/ContextWindow.d.ts +2 -1
- package/dist/lib/agent/context/ContextWindow.js +18 -4
- package/dist/lib/agent/context/ExplorationTracker.js +6 -1
- package/dist/lib/agent/context/exploration/ExplorationStrategies.js +2 -1
- package/dist/lib/agent/core/LoopContext.d.ts +3 -0
- package/dist/lib/agent/core/LoopContext.js +3 -0
- package/dist/lib/agent/domain/EpisodicConsolidator.d.ts +5 -0
- package/dist/lib/agent/domain/EpisodicConsolidator.js +60 -5
- package/dist/lib/agent/domain/insight-analyst.d.ts +16 -0
- package/dist/lib/agent/domain/insight-analyst.js +38 -0
- package/dist/lib/agent/domain/insight-gate.js +12 -0
- package/dist/lib/agent/memory/MemoryConsolidator.js +17 -0
- package/dist/lib/core/AstAnalyzer.js +0 -1
- package/dist/lib/core/ast/lang-dart.js +0 -1
- package/dist/lib/core/ast/lang-go.js +0 -1
- package/dist/lib/core/ast/lang-java.js +0 -1
- package/dist/lib/core/ast/lang-javascript.js +0 -1
- package/dist/lib/core/ast/lang-objc.js +0 -1
- package/dist/lib/core/ast/lang-python.js +0 -1
- package/dist/lib/core/ast/lang-rust.js +0 -1
- package/dist/lib/core/ast/lang-swift.js +0 -1
- package/dist/lib/core/ast/lang-typescript.js +0 -1
- package/dist/lib/domain/dimension/DimensionRegistry.d.ts +6 -4
- package/dist/lib/domain/dimension/DimensionRegistry.js +19 -23
- package/dist/lib/external/ai/AiFactory.d.ts +14 -0
- package/dist/lib/external/ai/AiFactory.js +33 -1
- package/dist/lib/external/ai/AiProvider.d.ts +2 -0
- package/dist/lib/external/ai/AiProvider.js +4 -0
- package/dist/lib/external/ai/providers/ClaudeProvider.d.ts +1 -1
- package/dist/lib/external/ai/providers/ClaudeProvider.js +6 -2
- package/dist/lib/external/ai/providers/GoogleGeminiProvider.d.ts +1 -1
- package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +13 -5
- package/dist/lib/external/ai/providers/OpenAiProvider.d.ts +1 -1
- package/dist/lib/external/ai/providers/OpenAiProvider.js +8 -4
- package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +10 -2
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.d.ts +1 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +39 -5
- package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.d.ts +2 -0
- package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +4 -0
- package/dist/lib/external/mcp/handlers/guard.js +11 -6
- package/dist/lib/http/routes/ai.js +20 -3
- package/dist/lib/infrastructure/vector/IndexingPipeline.js +6 -1
- package/dist/lib/injection/modules/AiModule.js +22 -1
- package/dist/lib/service/bootstrap/BootstrapTaskManager.d.ts +7 -0
- package/dist/lib/service/bootstrap/BootstrapTaskManager.js +17 -0
- package/dist/lib/service/guard/ComplianceReporter.js +5 -1
- package/dist/lib/service/guard/GuardCheckEngine.d.ts +12 -1
- package/dist/lib/service/guard/GuardCheckEngine.js +36 -4
- package/dist/lib/service/guard/GuardCodeChecks.js +27 -9
- package/dist/lib/service/guard/SourceFileCollector.d.ts +3 -2
- package/dist/lib/service/guard/SourceFileCollector.js +3 -3
- package/dist/lib/service/search/SearchEngine.js +165 -61
- package/dist/lib/service/task/PrimeSearchPipeline.js +17 -2
- package/dist/lib/service/vector/VectorService.js +10 -1
- package/dist/lib/shared/LanguageService.d.ts +12 -0
- package/dist/lib/shared/LanguageService.js +85 -0
- package/dist/lib/shared/schemas/http-requests.d.ts +4 -0
- package/dist/lib/shared/schemas/http-requests.js +4 -0
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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:
|
|
453
|
-
title: metadata.title ||
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|