autosnippet 3.4.0 → 3.4.2

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 (49) hide show
  1. package/README.md +43 -18
  2. package/dashboard/dist/assets/{index-8b1Gf3Bb.js → index-BX6r2fiy.js} +40 -40
  3. package/dashboard/dist/assets/index-BvZcGN02.css +1 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/dist/lib/core/AstAnalyzer.js +0 -1
  6. package/dist/lib/core/ast/lang-dart.js +118 -8
  7. package/dist/lib/core/ast/lang-go.js +0 -1
  8. package/dist/lib/core/ast/lang-java.js +25 -11
  9. package/dist/lib/core/ast/lang-javascript.js +103 -17
  10. package/dist/lib/core/ast/lang-objc.d.ts +1 -1
  11. package/dist/lib/core/ast/lang-objc.js +80 -4
  12. package/dist/lib/core/ast/lang-python.js +0 -1
  13. package/dist/lib/core/ast/lang-rust.js +0 -1
  14. package/dist/lib/core/ast/lang-swift.d.ts +1 -1
  15. package/dist/lib/core/ast/lang-swift.js +184 -7
  16. package/dist/lib/core/ast/lang-typescript.js +0 -1
  17. package/dist/lib/external/ai/AiFactory.d.ts +14 -0
  18. package/dist/lib/external/ai/AiFactory.js +33 -1
  19. package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +7 -3
  20. package/dist/lib/external/ai/providers/OpenAiProvider.js +1 -1
  21. package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.d.ts +33 -1
  22. package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +392 -19
  23. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.d.ts +1 -0
  24. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -1
  25. package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.d.ts +2 -0
  26. package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +4 -0
  27. package/dist/lib/external/mcp/handlers/guard.js +11 -6
  28. package/dist/lib/http/routes/ai.js +18 -1
  29. package/dist/lib/infrastructure/vector/IndexingPipeline.js +6 -1
  30. package/dist/lib/injection/modules/AiModule.js +22 -1
  31. package/dist/lib/service/bootstrap/BootstrapTaskManager.d.ts +7 -0
  32. package/dist/lib/service/bootstrap/BootstrapTaskManager.js +17 -0
  33. package/dist/lib/service/guard/ComplianceReporter.js +5 -1
  34. package/dist/lib/service/guard/GuardCheckEngine.d.ts +12 -1
  35. package/dist/lib/service/guard/GuardCheckEngine.js +36 -4
  36. package/dist/lib/service/guard/GuardCodeChecks.js +27 -9
  37. package/dist/lib/service/guard/SourceFileCollector.d.ts +3 -2
  38. package/dist/lib/service/guard/SourceFileCollector.js +3 -3
  39. package/dist/lib/service/search/SearchEngine.js +165 -61
  40. package/dist/lib/service/task/PrimeSearchPipeline.js +17 -2
  41. package/dist/lib/service/vector/VectorService.js +10 -1
  42. package/dist/lib/shared/LanguageService.d.ts +12 -0
  43. package/dist/lib/shared/LanguageService.js +85 -0
  44. package/dist/lib/shared/schemas/http-requests.d.ts +4 -0
  45. package/dist/lib/shared/schemas/http-requests.js +4 -0
  46. package/dist/lib/types/project-snapshot.d.ts +1 -0
  47. package/package.json +1 -1
  48. package/resources/grammars/tree-sitter-dart.wasm +0 -0
  49. package/dashboard/dist/assets/index-DHJ1Dj7u.css +0 -1
@@ -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;
@@ -275,6 +275,10 @@ const SCAN_SKIP_DIRS = Object.freeze(new Set([
275
275
  '.cargo',
276
276
  ]));
277
277
  // ═══════════════════════════════════════════════════════════
278
+ // 7.5) 通用测试目录模式(路径中包含典型测试目录名)
279
+ // ═══════════════════════════════════════════════════════════
280
+ const TEST_DIR_PATTERN = /(?:^|[/\\])(?:tests?|__tests__|spec|__mocks__|testdata|test_driver|integration_test|e2e)[/\\]/;
281
+ // ═══════════════════════════════════════════════════════════
278
282
  // Lazy caches
279
283
  // ═══════════════════════════════════════════════════════════
280
284
  let _sourceExtRegex = null;
@@ -567,6 +571,14 @@ export class LanguageService {
567
571
  langSet.add(lang);
568
572
  }
569
573
  }
574
+ // 启发式: node 与其他生态共存时,JS/TS 通常只是构建工具,去掉
575
+ if (nonGeneric.length > 1 && nonGeneric.includes('node')) {
576
+ const hasOther = nonGeneric.some((e) => e !== 'node');
577
+ if (hasOther) {
578
+ langSet.delete('javascript');
579
+ langSet.delete('typescript');
580
+ }
581
+ }
570
582
  if (langSet.size > 0) {
571
583
  return [...langSet];
572
584
  }
@@ -647,5 +659,78 @@ export class LanguageService {
647
659
  }
648
660
  return [...langSet];
649
661
  }
662
+ // ═══════════════════════════════════════════════════════════
663
+ // 9) 测试文件判定 — 统一入口
664
+ // ═══════════════════════════════════════════════════════════
665
+ /**
666
+ * 判定文件路径是否为测试文件
667
+ *
668
+ * 两层判定:
669
+ * 1. 语言特定的文件名模式(_test.go, .test.ts, test_*.py 等)
670
+ * 2. 通用测试目录模式(test/, tests/, __tests__/, spec/ 等)
671
+ *
672
+ * @param filePath 文件路径(相对或绝对均可)
673
+ * @param [language] 已知语言 ID,省略时从扩展名推断
674
+ * @returns 是否为测试文件
675
+ */
676
+ static isTestFile(filePath, language) {
677
+ if (!filePath) {
678
+ return false;
679
+ }
680
+ const name = filePath.split(/[/\\]/).pop() || '';
681
+ const lang = language || LanguageService.inferLang(name);
682
+ // ── 1. 语言特定的文件名模式 ──
683
+ switch (lang) {
684
+ case 'go':
685
+ if (name.endsWith('_test.go')) {
686
+ return true;
687
+ }
688
+ break;
689
+ case 'swift':
690
+ if (name.endsWith('Tests.swift') || name.endsWith('Test.swift')) {
691
+ return true;
692
+ }
693
+ break;
694
+ case 'rust':
695
+ if (name.endsWith('_test.rs') || name.startsWith('test_')) {
696
+ return true;
697
+ }
698
+ break;
699
+ case 'javascript':
700
+ case 'typescript':
701
+ if (/\.(test|spec)\.(js|ts|jsx|tsx|mjs|mts)$/.test(name)) {
702
+ return true;
703
+ }
704
+ break;
705
+ case 'python':
706
+ if (name.startsWith('test_') || name.endsWith('_test.py')) {
707
+ return true;
708
+ }
709
+ break;
710
+ case 'java':
711
+ case 'kotlin':
712
+ if (name.endsWith('Test.java') ||
713
+ name.endsWith('Test.kt') ||
714
+ name.endsWith('Tests.java') ||
715
+ name.endsWith('Tests.kt')) {
716
+ return true;
717
+ }
718
+ break;
719
+ case 'ruby':
720
+ if (name.endsWith('_spec.rb') || name.endsWith('_test.rb') || name.startsWith('test_')) {
721
+ return true;
722
+ }
723
+ break;
724
+ case 'dart':
725
+ if (name.endsWith('_test.dart')) {
726
+ return true;
727
+ }
728
+ break;
729
+ default:
730
+ break;
731
+ }
732
+ // ── 2. 通用测试目录模式 ──
733
+ return TEST_DIR_PATTERN.test(filePath);
734
+ }
650
735
  }
651
736
  export default LanguageService;
@@ -303,6 +303,10 @@ export declare const AiEnvConfigBody: z.ZodObject<{
303
303
  model: z.ZodOptional<z.ZodString>;
304
304
  apiKey: z.ZodOptional<z.ZodString>;
305
305
  proxy: z.ZodOptional<z.ZodString>;
306
+ embedProvider: z.ZodOptional<z.ZodString>;
307
+ embedModel: z.ZodOptional<z.ZodString>;
308
+ embedBaseUrl: z.ZodOptional<z.ZodString>;
309
+ embedApiKey: z.ZodOptional<z.ZodString>;
306
310
  }, z.core.$strip>;
307
311
  export declare const ExtractPathBody: z.ZodObject<{
308
312
  relativePath: z.ZodString;
@@ -303,6 +303,10 @@ export const AiEnvConfigBody = z.object({
303
303
  model: z.string().optional(),
304
304
  apiKey: z.string().optional(),
305
305
  proxy: z.string().optional(),
306
+ embedProvider: z.string().optional(),
307
+ embedModel: z.string().optional(),
308
+ embedBaseUrl: z.string().optional(),
309
+ embedApiKey: z.string().optional(),
306
310
  });
307
311
  // ═══ Extract Routes ══════════════════════════════
308
312
  export const ExtractPathBody = z.object({
@@ -63,6 +63,7 @@ export interface AstSummary {
63
63
  }
64
64
  export interface AstClassInfo {
65
65
  name: string;
66
+ kind?: string;
66
67
  superclass?: string;
67
68
  methodCount?: number;
68
69
  methods?: unknown[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
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",