autosnippet 2.12.0 → 2.13.0
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 +111 -37
- package/bin/cli.js +144 -0
- package/config/default.json +5 -0
- package/lib/core/AstAnalyzer.js +179 -0
- package/lib/external/mcp/handlers/guard.js +7 -1
- package/lib/http/routes/guardRules.js +27 -0
- package/lib/injection/ServiceContainer.js +29 -0
- package/lib/service/automation/handlers/GuardHandler.js +11 -0
- package/lib/service/guard/ComplianceReporter.js +335 -0
- package/lib/service/guard/GuardCheckEngine.js +171 -16
- package/lib/service/guard/GuardFeedbackLoop.js +125 -0
- package/lib/service/guard/GuardService.js +15 -3
- package/lib/service/guard/RuleLearner.js +116 -1
- package/lib/service/guard/SourceFileCollector.js +94 -0
- package/lib/service/guard/ViolationsStore.js +59 -1
- package/package.json +1 -1
- package/templates/guard-ci.yml +80 -0
- package/templates/pre-commit-guard.sh +33 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardFeedbackLoop — Guard ↔ Recipe 闭环联动
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* 1. 对比当前和历史 violations,检测已修复的违规
|
|
6
|
+
* 2. 已修复违规如有 fixSuggestion → 自动 confirmUsage(记录 Recipe 使用)
|
|
7
|
+
* 3. 集成到 guardAuditFiles MCP handler 和 GuardHandler (FileWatcher)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
11
|
+
|
|
12
|
+
export class GuardFeedbackLoop {
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('./ViolationsStore.js').ViolationsStore} violationsStore
|
|
15
|
+
* @param {import('../quality/FeedbackCollector.js').FeedbackCollector} feedbackCollector
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
* @param {import('./GuardCheckEngine.js').GuardCheckEngine} [options.guardCheckEngine] - 用于查找规则
|
|
18
|
+
*/
|
|
19
|
+
constructor(violationsStore, feedbackCollector, options = {}) {
|
|
20
|
+
this.violationsStore = violationsStore;
|
|
21
|
+
this.feedbackCollector = feedbackCollector;
|
|
22
|
+
this.guardCheckEngine = options.guardCheckEngine || null;
|
|
23
|
+
this.logger = Logger.getInstance();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 对比当前和历史 violations,检测已修复的违规
|
|
28
|
+
* @param {{ violations: Array<{ruleId: string}> }} currentResult - 本次检查结果
|
|
29
|
+
* @param {string} filePath - 文件路径
|
|
30
|
+
* @returns {Array<{ ruleId: string, filePath: string, fixRecipeId: string }>} 已修复且有 Recipe 关联的列表
|
|
31
|
+
*/
|
|
32
|
+
detectFixedViolations(currentResult, filePath) {
|
|
33
|
+
if (!this.violationsStore) return [];
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const previousRuns = this.violationsStore.getRunsByFile(filePath);
|
|
37
|
+
if (previousRuns.length === 0) return [];
|
|
38
|
+
|
|
39
|
+
// 取最近一次运行结果
|
|
40
|
+
const lastRun = previousRuns[previousRuns.length - 1];
|
|
41
|
+
const lastRuleIds = new Set((lastRun.violations || []).map(v => v.ruleId));
|
|
42
|
+
const currentRuleIds = new Set((currentResult.violations || []).map(v => v.ruleId));
|
|
43
|
+
|
|
44
|
+
const fixed = [];
|
|
45
|
+
for (const ruleId of lastRuleIds) {
|
|
46
|
+
if (!currentRuleIds.has(ruleId)) {
|
|
47
|
+
// 该规则的违规已消失 → 修复了
|
|
48
|
+
const fixRecipeId = this._findFixRecipe(ruleId, lastRun.violations);
|
|
49
|
+
if (fixRecipeId) {
|
|
50
|
+
fixed.push({ ruleId, filePath, fixRecipeId });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return fixed;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
this.logger.debug(`[GuardFeedbackLoop] detectFixedViolations error: ${err.message}`);
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 对已修复的违规自动确认使用
|
|
64
|
+
* @param {Array<{ ruleId: string, filePath: string, fixRecipeId: string }>} fixedList
|
|
65
|
+
*/
|
|
66
|
+
autoConfirmUsage(fixedList) {
|
|
67
|
+
if (!this.feedbackCollector || !fixedList?.length) return;
|
|
68
|
+
|
|
69
|
+
for (const { ruleId, fixRecipeId, filePath } of fixedList) {
|
|
70
|
+
try {
|
|
71
|
+
this.feedbackCollector.record('insert', fixRecipeId, {
|
|
72
|
+
source: 'guard_fix_detection',
|
|
73
|
+
automatic: true,
|
|
74
|
+
ruleId,
|
|
75
|
+
filePath,
|
|
76
|
+
});
|
|
77
|
+
this.logger.info(`[GuardFeedbackLoop] Auto-confirmed usage: recipe=${fixRecipeId} from fixing rule=${ruleId}`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.logger.debug(`[GuardFeedbackLoop] autoConfirmUsage error: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 一站式处理:检测修复 + 自动确认
|
|
86
|
+
* 供 MCP handler 和 GuardHandler 集成调用
|
|
87
|
+
* @param {{ violations: Array }} currentResult
|
|
88
|
+
* @param {string} filePath
|
|
89
|
+
*/
|
|
90
|
+
processFixDetection(currentResult, filePath) {
|
|
91
|
+
const fixed = this.detectFixedViolations(currentResult, filePath);
|
|
92
|
+
if (fixed.length > 0) {
|
|
93
|
+
this.autoConfirmUsage(fixed);
|
|
94
|
+
this.logger.info(`[GuardFeedbackLoop] Detected ${fixed.length} fixed violations in ${filePath}`);
|
|
95
|
+
}
|
|
96
|
+
return fixed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 从 violation 或 GuardCheckEngine 查找 fixRecipeId
|
|
101
|
+
*/
|
|
102
|
+
_findFixRecipe(ruleId, violations) {
|
|
103
|
+
// 先从 violation 本身的 fixSuggestion 查找
|
|
104
|
+
for (const v of (violations || [])) {
|
|
105
|
+
if (v.ruleId === ruleId && v.fixSuggestion) {
|
|
106
|
+
return v.fixSuggestion.replace(/^recipe:/, '');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 再从 GuardCheckEngine 的规则定义中查找
|
|
111
|
+
if (this.guardCheckEngine) {
|
|
112
|
+
try {
|
|
113
|
+
const rules = this.guardCheckEngine.getRules();
|
|
114
|
+
const rule = rules.find(r => r.id === ruleId);
|
|
115
|
+
if (rule?.fixSuggestion) {
|
|
116
|
+
return rule.fixSuggestion.replace(/^recipe:/, '');
|
|
117
|
+
}
|
|
118
|
+
} catch { /* ignore */ }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default GuardFeedbackLoop;
|
|
@@ -44,9 +44,12 @@ export class GuardService {
|
|
|
44
44
|
preconditions: [],
|
|
45
45
|
sideEffects: [],
|
|
46
46
|
guards: [{
|
|
47
|
-
pattern: data.pattern,
|
|
47
|
+
...(data.pattern ? { pattern: data.pattern } : {}),
|
|
48
48
|
severity: data.severity || 'warning',
|
|
49
49
|
message: data.description || '',
|
|
50
|
+
type: data.type || 'regex',
|
|
51
|
+
...(data.astQuery ? { astQuery: data.astQuery } : {}),
|
|
52
|
+
...(data.fixSuggestion ? { fixSuggestion: data.fixSuggestion } : {}),
|
|
50
53
|
}],
|
|
51
54
|
},
|
|
52
55
|
tags: data.languages || [],
|
|
@@ -241,6 +244,7 @@ export class GuardService {
|
|
|
241
244
|
|
|
242
245
|
/**
|
|
243
246
|
* 验证创建输入
|
|
247
|
+
* type='regex' 时 pattern 必须提供;type='ast' 时 astQuery 必须提供
|
|
244
248
|
*/
|
|
245
249
|
_validateCreateInput(data) {
|
|
246
250
|
if (!data.name || data.name.trim().length === 0) {
|
|
@@ -249,8 +253,16 @@ export class GuardService {
|
|
|
249
253
|
if (!data.description || data.description.trim().length === 0) {
|
|
250
254
|
throw new ValidationError('Rule description is required');
|
|
251
255
|
}
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
|
|
257
|
+
const ruleType = data.type || 'regex';
|
|
258
|
+
if (ruleType === 'ast') {
|
|
259
|
+
if (!data.astQuery || !data.astQuery.queryType) {
|
|
260
|
+
throw new ValidationError('AST query with queryType is required for type=ast rules');
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
if (!data.pattern || data.pattern.trim().length === 0) {
|
|
264
|
+
throw new ValidationError('Pattern is required for regex rules');
|
|
265
|
+
}
|
|
254
266
|
}
|
|
255
267
|
}
|
|
256
268
|
}
|
|
@@ -31,7 +31,9 @@ export class RuleLearner {
|
|
|
31
31
|
recordTrigger(ruleId, context = {}) {
|
|
32
32
|
const stat = this.#ensureStat(ruleId);
|
|
33
33
|
stat.triggers++;
|
|
34
|
-
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
stat.lastTriggered = now;
|
|
36
|
+
if (!stat.firstTriggered) stat.firstTriggered = now;
|
|
35
37
|
this.#save();
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -123,6 +125,118 @@ export class RuleLearner {
|
|
|
123
125
|
this.#save();
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/**
|
|
129
|
+
* 基于历史数据提出规则优化建议
|
|
130
|
+
* 策略 1: 高误报规则 → 建议调整
|
|
131
|
+
* 策略 2: 高触发且高精度 → 建议创建项目特化版本
|
|
132
|
+
* @returns {Array<{ type: string, ruleId: string, message: string, confidence: number, evidence: object }>}
|
|
133
|
+
*/
|
|
134
|
+
suggestRules() {
|
|
135
|
+
const suggestions = [];
|
|
136
|
+
|
|
137
|
+
// 策略 1: 从高误报规则推导改进建议
|
|
138
|
+
const problematic = this.getProblematicRules();
|
|
139
|
+
for (const p of problematic) {
|
|
140
|
+
if (p.recommendation === 'tune') {
|
|
141
|
+
suggestions.push({
|
|
142
|
+
type: 'tune_existing',
|
|
143
|
+
ruleId: p.ruleId,
|
|
144
|
+
message: `规则 ${p.ruleId} 误报率 ${(p.metrics.falsePositiveRate * 100).toFixed(0)}%,建议调整正则或收窄语言范围`,
|
|
145
|
+
confidence: 0.7,
|
|
146
|
+
evidence: p.metrics,
|
|
147
|
+
});
|
|
148
|
+
} else if (p.recommendation === 'disable') {
|
|
149
|
+
suggestions.push({
|
|
150
|
+
type: 'disable',
|
|
151
|
+
ruleId: p.ruleId,
|
|
152
|
+
message: `规则 ${p.ruleId} 误报率 ${(p.metrics.falsePositiveRate * 100).toFixed(0)}%,建议禁用`,
|
|
153
|
+
confidence: 0.8,
|
|
154
|
+
evidence: p.metrics,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 策略 2: 高触发 + 高精度内置规则 → 建议创建项目定制版
|
|
160
|
+
const allStats = this.getAllStats();
|
|
161
|
+
for (const [ruleId, stat] of Object.entries(allStats)) {
|
|
162
|
+
if (stat.triggers > 50 && (stat.metrics?.precision ?? 1) > 0.8) {
|
|
163
|
+
suggestions.push({
|
|
164
|
+
type: 'specialize',
|
|
165
|
+
ruleId,
|
|
166
|
+
message: `规则 ${ruleId} 触发 ${stat.triggers} 次且精准度高 (${((stat.metrics?.precision ?? 1) * 100).toFixed(0)}%),建议创建项目定制版本`,
|
|
167
|
+
confidence: 0.6,
|
|
168
|
+
evidence: stat.metrics,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 策略 3: 长期无触发的规则 → 可能不适用
|
|
174
|
+
for (const [ruleId, stat] of Object.entries(allStats)) {
|
|
175
|
+
if (stat.triggers === 0 && stat.lastTriggered) {
|
|
176
|
+
const daysSinceLastTrigger = (Date.now() - new Date(stat.lastTriggered).getTime()) / 86400000;
|
|
177
|
+
if (daysSinceLastTrigger > 30) {
|
|
178
|
+
suggestions.push({
|
|
179
|
+
type: 'review_unused',
|
|
180
|
+
ruleId,
|
|
181
|
+
message: `规则 ${ruleId} 超过 30 天未触发,建议审查是否仍需保留`,
|
|
182
|
+
confidence: 0.4,
|
|
183
|
+
evidence: { daysSinceLastTrigger: Math.round(daysSinceLastTrigger), triggers: stat.triggers },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return suggestions.sort((a, b) => b.confidence - a.confidence);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 追踪规则创建后的效果
|
|
194
|
+
* 对比首次触发后的表现,判断规则是否有效
|
|
195
|
+
* @param {string} ruleId
|
|
196
|
+
* @returns {{ status: string, triggers: number, precision: number, recommendation: string, daysSinceFirstTrigger?: number }}
|
|
197
|
+
*/
|
|
198
|
+
trackRuleEffectiveness(ruleId) {
|
|
199
|
+
const stat = this.#data.ruleStats[ruleId];
|
|
200
|
+
if (!stat) return { status: 'no_data', triggers: 0, precision: 1, recommendation: 'monitor' };
|
|
201
|
+
|
|
202
|
+
const firstTriggered = stat.firstTriggered || stat.lastTriggered;
|
|
203
|
+
if (!firstTriggered) return { status: 'no_triggers', triggers: 0, precision: 1, recommendation: 'monitor' };
|
|
204
|
+
|
|
205
|
+
const daysSinceFirstTrigger = (Date.now() - new Date(firstTriggered).getTime()) / 86400000;
|
|
206
|
+
|
|
207
|
+
// 不足 14 天 → 观察期
|
|
208
|
+
if (daysSinceFirstTrigger < 14) {
|
|
209
|
+
return {
|
|
210
|
+
status: 'monitoring',
|
|
211
|
+
triggers: stat.triggers,
|
|
212
|
+
precision: this.getMetrics(ruleId).precision,
|
|
213
|
+
recommendation: 'wait',
|
|
214
|
+
daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const metrics = this.getMetrics(ruleId);
|
|
219
|
+
|
|
220
|
+
// 14 天后判定
|
|
221
|
+
if (metrics.precision < 0.5 && stat.triggers >= PROBLEMATIC_THRESHOLD.minTriggers) {
|
|
222
|
+
return {
|
|
223
|
+
status: 'ineffective',
|
|
224
|
+
triggers: stat.triggers,
|
|
225
|
+
precision: metrics.precision,
|
|
226
|
+
recommendation: 'review_or_disable',
|
|
227
|
+
daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
status: 'effective',
|
|
233
|
+
triggers: stat.triggers,
|
|
234
|
+
precision: metrics.precision,
|
|
235
|
+
recommendation: 'keep',
|
|
236
|
+
daysSinceFirstTrigger: Math.round(daysSinceFirstTrigger),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
126
240
|
// ─── 私有 ─────────────────────────────────────────────
|
|
127
241
|
|
|
128
242
|
#ensureStat(ruleId) {
|
|
@@ -132,6 +246,7 @@ export class RuleLearner {
|
|
|
132
246
|
correct: 0,
|
|
133
247
|
falsePositive: 0,
|
|
134
248
|
falseNegative: 0,
|
|
249
|
+
firstTriggered: null,
|
|
135
250
|
lastTriggered: null,
|
|
136
251
|
lastFeedback: null,
|
|
137
252
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SourceFileCollector — 递归收集项目源文件
|
|
3
|
+
*
|
|
4
|
+
* 从 GuardHandler 提取的公共工具,供 ComplianceReporter / guard:ci / guard:staged 复用
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join, extname } from 'node:path';
|
|
8
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
9
|
+
|
|
10
|
+
/** 支持审计的源文件扩展名 */
|
|
11
|
+
export const SOURCE_EXTS = new Set([
|
|
12
|
+
'.m', '.mm', '.h', '.swift',
|
|
13
|
+
'.c', '.cpp', '.cc', '.cxx', '.hpp',
|
|
14
|
+
'.js', '.ts', '.jsx', '.tsx',
|
|
15
|
+
'.java', '.kt', '.py', '.rb', '.go', '.rs',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/** 跳过的目录 */
|
|
19
|
+
const SKIP_DIRS = new Set([
|
|
20
|
+
'node_modules', '.git', 'build', 'DerivedData',
|
|
21
|
+
'Pods', '.build', 'vendor', 'dist', '.next',
|
|
22
|
+
'Carthage', 'xcuserdata', '__pycache__',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 递归收集目录下所有源文件路径
|
|
27
|
+
* @param {string} dir - 根目录
|
|
28
|
+
* @param {object} options
|
|
29
|
+
* @param {Set<string>} [options.extensions] - 允许的扩展名(默认 SOURCE_EXTS)
|
|
30
|
+
* @param {Set<string>} [options.skipDirs] - 跳过的目录名(默认 SKIP_DIRS)
|
|
31
|
+
* @param {number} [options.maxFiles] - 最大文件数量(默认无限制)
|
|
32
|
+
* @returns {Promise<string[]>} 文件路径列表
|
|
33
|
+
*/
|
|
34
|
+
export async function collectSourceFiles(dir, options = {}) {
|
|
35
|
+
const {
|
|
36
|
+
extensions = SOURCE_EXTS,
|
|
37
|
+
skipDirs = SKIP_DIRS,
|
|
38
|
+
maxFiles = Infinity,
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
const files = [];
|
|
42
|
+
|
|
43
|
+
async function walk(currentDir) {
|
|
44
|
+
if (files.length >= maxFiles) return;
|
|
45
|
+
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
49
|
+
} catch {
|
|
50
|
+
return; // 权限不足等情况跳过
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (files.length >= maxFiles) return;
|
|
55
|
+
if (entry.name.startsWith('.') && entry.name !== '.') continue;
|
|
56
|
+
|
|
57
|
+
const fullPath = join(currentDir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (!skipDirs.has(entry.name)) {
|
|
60
|
+
await walk(fullPath);
|
|
61
|
+
}
|
|
62
|
+
} else if (entry.isFile() && extensions.has(extname(entry.name).toLowerCase())) {
|
|
63
|
+
files.push(fullPath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await walk(dir);
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 收集源文件并读取内容
|
|
74
|
+
* @param {string} dir - 根目录
|
|
75
|
+
* @param {object} options - collectSourceFiles 选项
|
|
76
|
+
* @returns {Promise<Array<{path: string, content: string}>>}
|
|
77
|
+
*/
|
|
78
|
+
export async function collectSourceFilesWithContent(dir, options = {}) {
|
|
79
|
+
const paths = await collectSourceFiles(dir, options);
|
|
80
|
+
const results = [];
|
|
81
|
+
|
|
82
|
+
for (const filePath of paths) {
|
|
83
|
+
try {
|
|
84
|
+
const content = await readFile(filePath, 'utf-8');
|
|
85
|
+
results.push({ path: filePath, content });
|
|
86
|
+
} catch {
|
|
87
|
+
// 读取失败跳过
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default { collectSourceFiles, collectSourceFilesWithContent, SOURCE_EXTS };
|
|
@@ -77,7 +77,7 @@ export class ViolationsStore {
|
|
|
77
77
|
*/
|
|
78
78
|
getRecentRuns(n = 20) {
|
|
79
79
|
const rows = this.#db.prepare(
|
|
80
|
-
'SELECT * FROM guard_violations ORDER BY created_at DESC LIMIT ?'
|
|
80
|
+
'SELECT * FROM guard_violations ORDER BY created_at DESC, rowid DESC LIMIT ?'
|
|
81
81
|
).all(n);
|
|
82
82
|
return rows.reverse().map(r => this.#rowToRun(r));
|
|
83
83
|
}
|
|
@@ -104,6 +104,64 @@ export class ViolationsStore {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 按规则 ID 聚合统计
|
|
109
|
+
* 利用 SQLite json_each 展开 violations_json 数组
|
|
110
|
+
* @returns {Array<{ruleId: string, severity: string, count: number}>}
|
|
111
|
+
*/
|
|
112
|
+
getStatsByRule() {
|
|
113
|
+
try {
|
|
114
|
+
return this.#db.prepare(`
|
|
115
|
+
SELECT
|
|
116
|
+
json_extract(j.value, '$.ruleId') AS ruleId,
|
|
117
|
+
json_extract(j.value, '$.severity') AS severity,
|
|
118
|
+
COUNT(*) AS count
|
|
119
|
+
FROM guard_violations gv, json_each(gv.violations_json) j
|
|
120
|
+
WHERE json_extract(j.value, '$.ruleId') IS NOT NULL
|
|
121
|
+
GROUP BY ruleId, severity
|
|
122
|
+
ORDER BY count DESC
|
|
123
|
+
`).all();
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 获取趋势数据 — 对比最近两次运行
|
|
131
|
+
* @returns {{ errorsChange: number, warningsChange: number, latestErrors: number, latestWarnings: number, previousErrors: number, previousWarnings: number }}
|
|
132
|
+
*/
|
|
133
|
+
getTrend() {
|
|
134
|
+
const recent = this.getRecentRuns(2);
|
|
135
|
+
if (recent.length < 2) {
|
|
136
|
+
const latest = recent[0]?.violations || [];
|
|
137
|
+
return {
|
|
138
|
+
errorsChange: 0,
|
|
139
|
+
warningsChange: 0,
|
|
140
|
+
latestErrors: latest.filter(v => v.severity === 'error').length,
|
|
141
|
+
latestWarnings: latest.filter(v => v.severity === 'warning').length,
|
|
142
|
+
previousErrors: 0,
|
|
143
|
+
previousWarnings: 0,
|
|
144
|
+
hasHistory: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const [prev, latest] = recent;
|
|
149
|
+
const latestErrors = latest.violations.filter(v => v.severity === 'error').length;
|
|
150
|
+
const latestWarnings = latest.violations.filter(v => v.severity === 'warning').length;
|
|
151
|
+
const previousErrors = prev.violations.filter(v => v.severity === 'error').length;
|
|
152
|
+
const previousWarnings = prev.violations.filter(v => v.severity === 'warning').length;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
errorsChange: latestErrors - previousErrors,
|
|
156
|
+
warningsChange: latestWarnings - previousWarnings,
|
|
157
|
+
latestErrors,
|
|
158
|
+
latestWarnings,
|
|
159
|
+
previousErrors,
|
|
160
|
+
previousWarnings,
|
|
161
|
+
hasHistory: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
107
165
|
// ─── 清除 ─────────────────────────────────────────────
|
|
108
166
|
|
|
109
167
|
/**
|
package/package.json
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# AutoSnippet Guard CI/CD Check
|
|
2
|
+
# 在 push 和 PR 时自动运行 Guard 合规检查
|
|
3
|
+
#
|
|
4
|
+
# 使用方法:将此文件复制到项目的 .github/workflows/ 目录
|
|
5
|
+
|
|
6
|
+
name: AutoSnippet Guard Check
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main, develop]
|
|
11
|
+
pull_request:
|
|
12
|
+
branches: [main, develop]
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
guard:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
name: Guard Compliance Check
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Setup Node.js
|
|
24
|
+
uses: actions/setup-node@v4
|
|
25
|
+
with:
|
|
26
|
+
node-version: '20'
|
|
27
|
+
|
|
28
|
+
- name: Install AutoSnippet
|
|
29
|
+
run: npm install -g autosnippet
|
|
30
|
+
|
|
31
|
+
- name: Initialize workspace
|
|
32
|
+
run: asd setup --dir .
|
|
33
|
+
|
|
34
|
+
- name: Run Guard CI check
|
|
35
|
+
run: |
|
|
36
|
+
asd guard:ci . \
|
|
37
|
+
--report json \
|
|
38
|
+
--output guard-report.json \
|
|
39
|
+
--max-warnings 20 \
|
|
40
|
+
--min-score 70
|
|
41
|
+
|
|
42
|
+
- name: Upload Guard Report
|
|
43
|
+
uses: actions/upload-artifact@v4
|
|
44
|
+
if: always()
|
|
45
|
+
with:
|
|
46
|
+
name: guard-report
|
|
47
|
+
path: guard-report.json
|
|
48
|
+
retention-days: 30
|
|
49
|
+
|
|
50
|
+
- name: Comment PR with results
|
|
51
|
+
if: github.event_name == 'pull_request' && always()
|
|
52
|
+
uses: actions/github-script@v7
|
|
53
|
+
with:
|
|
54
|
+
script: |
|
|
55
|
+
const fs = require('fs');
|
|
56
|
+
try {
|
|
57
|
+
const report = JSON.parse(fs.readFileSync('guard-report.json', 'utf8'));
|
|
58
|
+
const { qualityGate, summary } = report;
|
|
59
|
+
const icon = qualityGate.status === 'PASS' ? '✅' : qualityGate.status === 'WARN' ? '⚠️' : '❌';
|
|
60
|
+
|
|
61
|
+
const body = [
|
|
62
|
+
`## ${icon} Guard Compliance Report`,
|
|
63
|
+
'',
|
|
64
|
+
`| Metric | Value |`,
|
|
65
|
+
`|--------|-------|`,
|
|
66
|
+
`| Quality Gate | ${qualityGate.status} (Score: ${qualityGate.score}/100) |`,
|
|
67
|
+
`| Files Scanned | ${summary.filesScanned} |`,
|
|
68
|
+
`| Errors | ${summary.errors} |`,
|
|
69
|
+
`| Warnings | ${summary.warnings} |`,
|
|
70
|
+
].join('\n');
|
|
71
|
+
|
|
72
|
+
await github.rest.issues.createComment({
|
|
73
|
+
issue_number: context.issue.number,
|
|
74
|
+
owner: context.repo.owner,
|
|
75
|
+
repo: context.repo.repo,
|
|
76
|
+
body,
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log('Could not post PR comment:', err.message);
|
|
80
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# AutoSnippet Guard pre-commit hook
|
|
3
|
+
#
|
|
4
|
+
# 安装方法:
|
|
5
|
+
# cp templates/pre-commit-guard.sh .git/hooks/pre-commit
|
|
6
|
+
# chmod +x .git/hooks/pre-commit
|
|
7
|
+
#
|
|
8
|
+
# 或使用 husky:
|
|
9
|
+
# npx husky add .husky/pre-commit "sh templates/pre-commit-guard.sh"
|
|
10
|
+
|
|
11
|
+
# 检查 asd 是否可用
|
|
12
|
+
if ! command -v asd &> /dev/null; then
|
|
13
|
+
echo "⚠️ AutoSnippet (asd) not found. Skipping Guard check."
|
|
14
|
+
echo " Install: npm install -g autosnippet"
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "🛡️ Running Guard check on staged files..."
|
|
19
|
+
|
|
20
|
+
# 运行 guard:staged 检查
|
|
21
|
+
asd guard:staged --fail-on-error
|
|
22
|
+
|
|
23
|
+
EXIT_CODE=$?
|
|
24
|
+
|
|
25
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
26
|
+
echo ""
|
|
27
|
+
echo "❌ Guard check failed. Fix violations before committing."
|
|
28
|
+
echo " Use 'git commit --no-verify' to bypass (not recommended)."
|
|
29
|
+
exit $EXIT_CODE
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "✅ Guard check passed."
|
|
33
|
+
exit 0
|