autosnippet 3.4.0 → 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 (43) 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 +0 -1
  7. package/dist/lib/core/ast/lang-go.js +0 -1
  8. package/dist/lib/core/ast/lang-java.js +0 -1
  9. package/dist/lib/core/ast/lang-javascript.js +0 -1
  10. package/dist/lib/core/ast/lang-objc.js +0 -1
  11. package/dist/lib/core/ast/lang-python.js +0 -1
  12. package/dist/lib/core/ast/lang-rust.js +0 -1
  13. package/dist/lib/core/ast/lang-swift.js +0 -1
  14. package/dist/lib/core/ast/lang-typescript.js +0 -1
  15. package/dist/lib/external/ai/AiFactory.d.ts +14 -0
  16. package/dist/lib/external/ai/AiFactory.js +33 -1
  17. package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +7 -3
  18. package/dist/lib/external/ai/providers/OpenAiProvider.js +1 -1
  19. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.d.ts +1 -0
  20. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -1
  21. package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.d.ts +2 -0
  22. package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +4 -0
  23. package/dist/lib/external/mcp/handlers/guard.js +11 -6
  24. package/dist/lib/http/routes/ai.js +18 -1
  25. package/dist/lib/infrastructure/vector/IndexingPipeline.js +6 -1
  26. package/dist/lib/injection/modules/AiModule.js +22 -1
  27. package/dist/lib/service/bootstrap/BootstrapTaskManager.d.ts +7 -0
  28. package/dist/lib/service/bootstrap/BootstrapTaskManager.js +17 -0
  29. package/dist/lib/service/guard/ComplianceReporter.js +5 -1
  30. package/dist/lib/service/guard/GuardCheckEngine.d.ts +12 -1
  31. package/dist/lib/service/guard/GuardCheckEngine.js +36 -4
  32. package/dist/lib/service/guard/GuardCodeChecks.js +27 -9
  33. package/dist/lib/service/guard/SourceFileCollector.d.ts +3 -2
  34. package/dist/lib/service/guard/SourceFileCollector.js +3 -3
  35. package/dist/lib/service/search/SearchEngine.js +165 -61
  36. package/dist/lib/service/task/PrimeSearchPipeline.js +17 -2
  37. package/dist/lib/service/vector/VectorService.js +10 -1
  38. package/dist/lib/shared/LanguageService.d.ts +12 -0
  39. package/dist/lib/shared/LanguageService.js +85 -0
  40. package/dist/lib/shared/schemas/http-requests.d.ts +4 -0
  41. package/dist/lib/shared/schemas/http-requests.js +4 -0
  42. package/package.json +1 -1
  43. package/dashboard/dist/assets/index-DHJ1Dj7u.css +0 -1
@@ -17,7 +17,7 @@ export class OpenAiProvider extends AiProvider {
17
17
  this.model = config.model || 'gpt-5.4-mini';
18
18
  this.apiKey = config.apiKey || process.env.ASD_OPENAI_API_KEY || '';
19
19
  this.baseUrl = config.baseUrl || OPENAI_BASE;
20
- this.embedModel = config.embedModel || 'text-embedding-3-small';
20
+ this.embedModel = config.embedModel || process.env.ASD_EMBED_MODEL || 'text-embedding-3-small';
21
21
  this.logger = Logger.getInstance();
22
22
  }
23
23
  /**
@@ -68,6 +68,7 @@ interface BootstrapFileEntry {
68
68
  /** Task manager minimal shape */
69
69
  interface TaskManagerLike {
70
70
  isSessionValid(sessionId: string): boolean;
71
+ getSessionAbortSignal?(): AbortSignal | null;
71
72
  emitProgress?(event: string, data: Record<string, unknown>): void;
72
73
  [key: string]: unknown;
73
74
  }
@@ -113,6 +113,7 @@ export async function fillDimensionsV3(view, dimensions) {
113
113
  /* not available */
114
114
  }
115
115
  const sessionId = view.bootstrapSession?.id ?? '';
116
+ const sessionAbortSignal = taskManager?.getSessionAbortSignal?.() ?? null;
116
117
  const isIncremental = incrementalPlan?.canIncremental && incrementalPlan?.mode === 'incremental';
117
118
  const emitter = new BootstrapEventEmitter(ctx.container);
118
119
  logger.info(`[Insight-v3] ═══ fillDimensionsV3 entered — ${isIncremental ? 'INCREMENTAL' : 'FULL'} pipeline`);
@@ -623,7 +624,7 @@ export async function fillDimensionsV3(view, dimensions) {
623
624
  // 外层超时 = 安全网 (各阶段已有独立超时: Analyst 300s + Producer 180s + 硬缓冲 60s)
624
625
  const outerTimeoutMs = 3_600_000; // 1 小时——维度分析本身耗时长
625
626
  const runResult = await Promise.race([
626
- runtime.execute(message, { strategyContext }),
627
+ runtime.execute(message, { strategyContext, abortSignal: sessionAbortSignal }),
627
628
  new Promise((_, reject) => setTimeout(() => reject(new Error(`Bootstrap runtime timeout for "${dimId}"`)), outerTimeoutMs)),
628
629
  ]);
629
630
  // ── 提取结果 ──
@@ -39,6 +39,8 @@ interface BootstrapFileEntry {
39
39
  relativePath: string;
40
40
  content: string;
41
41
  targetName: string;
42
+ /** Whether this file belongs to a test target or matches test file naming patterns */
43
+ isTest: boolean;
42
44
  }
43
45
  /** Target item — either a plain string or an object with metadata */
44
46
  type TargetItem = string | {
@@ -67,6 +67,7 @@ export async function runPhase1_FileCollection(projectRoot, logger, options = {}
67
67
  const seenPaths = new Set();
68
68
  const allFiles = [];
69
69
  for (const t of allTargets) {
70
+ const isTestTarget = typeof t === 'object' && /^test/i.test(t.type || '');
70
71
  try {
71
72
  const fileList = await discoverer.getTargetFiles(t);
72
73
  for (const f of fileList) {
@@ -86,6 +87,7 @@ export async function runPhase1_FileCollection(projectRoot, logger, options = {}
86
87
  relativePath: f.relativePath || path.basename(fp),
87
88
  content,
88
89
  targetName: typeof t === 'string' ? t : t.name,
90
+ isTest: isTestTarget || LanguageService.isTestFile(fp),
89
91
  });
90
92
  }
91
93
  catch {
@@ -382,6 +384,7 @@ export async function runPhase3_GuardAudit(allFiles, container, logger, options
382
384
  const guardFiles = allFiles.map((f) => ({
383
385
  path: f.path,
384
386
  content: f.content,
387
+ isTest: f.isTest,
385
388
  }));
386
389
  guardAudit = guardEngine.auditFiles(guardFiles, { scope: 'project' });
387
390
  // 写入 ViolationsStore
@@ -484,6 +487,7 @@ export async function runPhase4_DimensionResolve(params) {
484
487
  const guardFiles = allFiles.map((f) => ({
485
488
  path: f.path,
486
489
  content: f.content,
490
+ isTest: f.isTest,
487
491
  }));
488
492
  guardAudit = guardEngine.auditFiles(guardFiles, { scope: 'project' });
489
493
  logger.info(`[Bootstrap] Guard re-audit with ${guardEngine.getExternalRuleCount()} Enhancement Pack rules → ${guardAudit.summary?.totalViolations ?? 0} total violations`);
@@ -10,6 +10,7 @@ import { execSync } from 'node:child_process';
10
10
  import fs from 'node:fs';
11
11
  import { readFile } from 'node:fs/promises';
12
12
  import path from 'node:path';
13
+ import { LanguageService } from '#shared/LanguageService.js';
13
14
  import { resolveProjectRoot } from '#shared/resolveProjectRoot.js';
14
15
  import { envelope } from '../envelope.js';
15
16
  // ═══ Review 轮次追踪(模块私有) ═══════════════════
@@ -93,7 +94,7 @@ export async function guardAuditFiles(ctx, args) {
93
94
  content = '';
94
95
  }
95
96
  }
96
- return { path: absPath, content };
97
+ return { path: absPath, content, isTest: LanguageService.isTestFile(absPath) };
97
98
  }));
98
99
  const result = engine.auditFiles(filesToAudit, { scope });
99
100
  // 写入 ViolationsStore + GuardFeedbackLoop
@@ -224,7 +225,7 @@ export async function guardReview(ctx, args) {
224
225
  for (const fp of filePaths) {
225
226
  try {
226
227
  const code = await readFile(fp, 'utf8');
227
- const auditResult = engine.auditFile(fp, code);
228
+ const auditResult = engine.auditFile(fp, code, { isTest: LanguageService.isTestFile(fp) });
228
229
  const violations = auditResult.violations;
229
230
  // 收集 uncertain
230
231
  if (auditResult.uncertainResults?.length) {
@@ -523,7 +524,7 @@ export async function scanProject(ctx, args) {
523
524
  content = '';
524
525
  }
525
526
  }
526
- return { path: f.path, content };
527
+ return { path: f.path, content, isTest: LanguageService.isTestFile(f.path) };
527
528
  }));
528
529
  guardAudit = engine.auditFiles(filesToAudit, { scope: 'project' });
529
530
  // 写入 ViolationsStore
@@ -629,9 +630,13 @@ async function _injectEnhancementGuardRules(engine, ctx) {
629
630
  try {
630
631
  const { initEnhancementRegistry } = await import('#core/enhancement/index.js');
631
632
  const enhReg = await initEnhancementRegistry();
632
- // 使用空语言+空框架列表获取所有已注册的 Pack(不过滤)
633
- // 这里我们注入 ALL 规则,让 GuardCheckEngine languages 字段自行过滤
634
- const allPacks = enhReg.all();
633
+ // 仅注入无框架条件的通用 Pack 规则(如 go-web 无 frameworks 条件)
634
+ // 有框架条件的 Pack(如 go-grpc 需要 frameworks: ['grpc'])由 Bootstrap Phase 4
635
+ // 通过 resolve(lang, detectedFrameworks) 精确注入,避免非 gRPC 项目出现误报
636
+ const allPacks = enhReg.all().filter((pack) => {
637
+ const cond = pack.conditions;
638
+ return !cond?.frameworks?.length;
639
+ });
635
640
  const allGuardRules = [];
636
641
  for (const pack of allPacks) {
637
642
  try {
@@ -462,6 +462,10 @@ const LLM_ENV_KEYS = [
462
462
  'ASD_CLAUDE_API_KEY',
463
463
  'ASD_DEEPSEEK_API_KEY',
464
464
  'ASD_AI_PROXY',
465
+ 'ASD_EMBED_PROVIDER',
466
+ 'ASD_EMBED_MODEL',
467
+ 'ASD_EMBED_BASE_URL',
468
+ 'ASD_EMBED_API_KEY',
465
469
  ];
466
470
  /**
467
471
  * 解析 .env 内容为 key-value(仅提取 LLM 相关变量)
@@ -523,7 +527,7 @@ router.get('/env-config', async (req, res) => {
523
527
  * Body: { provider, model, apiKey, proxy? }
524
528
  */
525
529
  router.post('/env-config', validate(AiEnvConfigBody), async (req, res) => {
526
- const { provider, model, apiKey, proxy } = req.body;
530
+ const { provider, model, apiKey, proxy, embedProvider, embedModel, embedBaseUrl, embedApiKey } = req.body;
527
531
  const envPath = _getProjectEnvPath();
528
532
  let content = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
529
533
  // 构建 key-value 更新列表
@@ -547,6 +551,19 @@ router.post('/env-config', validate(AiEnvConfigBody), async (req, res) => {
547
551
  if (keyName && apiKey) {
548
552
  updates[keyName] = apiKey;
549
553
  }
554
+ // Embedding 独立配置
555
+ if (embedProvider) {
556
+ updates.ASD_EMBED_PROVIDER = embedProvider;
557
+ if (embedModel) {
558
+ updates.ASD_EMBED_MODEL = embedModel;
559
+ }
560
+ if (embedBaseUrl) {
561
+ updates.ASD_EMBED_BASE_URL = embedBaseUrl;
562
+ }
563
+ if (embedApiKey) {
564
+ updates.ASD_EMBED_API_KEY = embedApiKey;
565
+ }
566
+ }
550
567
  // 逐条合并到 .env 内容
551
568
  for (const [k, v] of Object.entries(updates)) {
552
569
  // 匹配已有行(包括被注释的行)
@@ -44,7 +44,12 @@ export class IndexingPipeline {
44
44
  constructor(options = {}) {
45
45
  this.#vectorStore = options.vectorStore || null;
46
46
  this.#aiProvider = options.aiProvider || null;
47
- this.#scanDirs = options.scanDirs || ['recipes', `${KNOWLEDGE_BASE_DIR}/recipes`];
47
+ this.#scanDirs = options.scanDirs || [
48
+ 'recipes',
49
+ 'candidates',
50
+ `${KNOWLEDGE_BASE_DIR}/recipes`,
51
+ `${KNOWLEDGE_BASE_DIR}/candidates`,
52
+ ];
48
53
  this.#projectRoot = options.projectRoot || process.cwd();
49
54
  this.#chunkingOptions = {
50
55
  strategy: options.chunking?.strategy ?? 'auto',
@@ -72,7 +72,28 @@ export async function initialize(c) {
72
72
  // Token 追踪 AOP(manager 自身已在构造时 wire,此处延迟注入 recorder)
73
73
  // recorder 注入放到 register() 之后(tokenUsageStore 需先注册)
74
74
  // Embedding fallback: manager 的 embedFallbackInit 回调已绑定,初始化时主动触发一次
75
- const initialEmbed = createEmbedFallback(c, c.singletons.aiProvider);
75
+ // 优先使用独立的 embed provider(ASD_EMBED_PROVIDER),其次 fallback 机制
76
+ let initialEmbed = null;
77
+ try {
78
+ const aiFactory = c.singletons._aiFactory;
79
+ if (typeof aiFactory?.createEmbedProvider === 'function') {
80
+ initialEmbed = aiFactory.createEmbedProvider();
81
+ if (initialEmbed) {
82
+ logger.info('Dedicated embed provider created from ASD_EMBED_PROVIDER', {
83
+ provider: initialEmbed.name,
84
+ });
85
+ }
86
+ }
87
+ }
88
+ catch (err) {
89
+ logger.warn('Failed to create dedicated embed provider', {
90
+ error: err.message,
91
+ });
92
+ }
93
+ // 若无独立 embed provider,走旧的 fallback 逻辑
94
+ if (!initialEmbed) {
95
+ initialEmbed = createEmbedFallback(c, c.singletons.aiProvider);
96
+ }
76
97
  if (initialEmbed) {
77
98
  manager.setEmbedProvider(initialEmbed);
78
99
  c.singletons._embedProvider = initialEmbed;
@@ -116,6 +116,13 @@ export declare class BootstrapTaskManager {
116
116
  * @param [reason='Aborted by user']
117
117
  */
118
118
  abortSession(reason?: string): void;
119
+ /**
120
+ * 获取当前 session 的 AbortSignal
121
+ *
122
+ * 用于传入 AgentRuntime.execute(),使得 abortSession() 可以立即中断正在执行的 AI 调用,
123
+ * 而不是等到下一个维度边界才检测到取消。
124
+ */
125
+ getSessionAbortSignal(): AbortSignal | null;
119
126
  /**
120
127
  * 验证 sessionId 是否仍然是活跃 session
121
128
  *
@@ -119,6 +119,8 @@ export class BootstrapTaskManager {
119
119
  #currentSession = null;
120
120
  #eventBus = null;
121
121
  #signalBus = null;
122
+ /** 当前 session 的 AbortController,用于取消正在执行的 AI 调用 */
123
+ #sessionAbortController = null;
122
124
  /** 获取 RealtimeService 的 getter(延迟获取,避免循环依赖) */
123
125
  #getRealtimeService = null;
124
126
  constructor({ eventBus, signalBus, getRealtimeService } = {}) {
@@ -144,6 +146,7 @@ export class BootstrapTaskManager {
144
146
  }
145
147
  const sessionId = `bs_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
146
148
  this.#currentSession = new BootstrapSession(sessionId);
149
+ this.#sessionAbortController = new AbortController();
147
150
  for (const { id, meta } of taskDefs) {
148
151
  this.#currentSession.addTask(id, meta);
149
152
  }
@@ -177,6 +180,11 @@ export class BootstrapTaskManager {
177
180
  task.error = reason;
178
181
  }
179
182
  }
183
+ // 触发 AbortController,中断正在执行的 AI 调用
184
+ if (this.#sessionAbortController) {
185
+ this.#sessionAbortController.abort(reason);
186
+ this.#sessionAbortController = null;
187
+ }
180
188
  session.status = 'aborted';
181
189
  session.completedAt = Date.now();
182
190
  session.summary = {
@@ -209,6 +217,15 @@ export class BootstrapTaskManager {
209
217
  },
210
218
  });
211
219
  }
220
+ /**
221
+ * 获取当前 session 的 AbortSignal
222
+ *
223
+ * 用于传入 AgentRuntime.execute(),使得 abortSession() 可以立即中断正在执行的 AI 调用,
224
+ * 而不是等到下一个维度边界才检测到取消。
225
+ */
226
+ getSessionAbortSignal() {
227
+ return this.#sessionAbortController?.signal ?? null;
228
+ }
212
229
  /**
213
230
  * 验证 sessionId 是否仍然是活跃 session
214
231
  *
@@ -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
  // 读取失败跳过