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.
- package/README.md +43 -18
- package/dashboard/dist/assets/{index-8b1Gf3Bb.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/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/external/ai/AiFactory.d.ts +14 -0
- package/dist/lib/external/ai/AiFactory.js +33 -1
- package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +7 -3
- package/dist/lib/external/ai/providers/OpenAiProvider.js +1 -1
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.d.ts +1 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -1
- 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 +18 -1
- 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
|
@@ -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
|
-
//
|
|
633
|
-
//
|
|
634
|
-
|
|
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 || [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// 读取失败跳过
|