sumulige-claude 1.1.0 → 1.1.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/.claude/hooks/pre-commit.cjs +86 -0
- package/.claude/hooks/pre-push.cjs +103 -0
- package/.claude/quality-gate.json +61 -0
- package/.claude/settings.local.json +2 -1
- package/cli.js +28 -0
- package/config/quality-gate.json +61 -0
- package/lib/commands.js +208 -0
- package/lib/config-manager.js +441 -0
- package/lib/config-schema.js +408 -0
- package/lib/config-validator.js +330 -0
- package/lib/config.js +52 -1
- package/lib/errors.js +305 -0
- package/lib/quality-gate.js +431 -0
- package/lib/quality-rules.js +373 -0
- package/package.json +5 -1
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Gate Implementation
|
|
3
|
+
*
|
|
4
|
+
* Multi-level validation engine with pluggable rules.
|
|
5
|
+
* Supports multiple output formats and gate enforcement.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/quality-gate
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { registry } = require('./quality-rules');
|
|
13
|
+
const { QualityGateError } = require('./errors');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Quality Gate class
|
|
17
|
+
*/
|
|
18
|
+
class QualityGate {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} options - Gate options
|
|
21
|
+
* @param {string} options.projectDir - Project directory
|
|
22
|
+
* @param {Object} options.config - Gate configuration
|
|
23
|
+
* @param {Array} options.reporters - Output reporters
|
|
24
|
+
*/
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.projectDir = options.projectDir || process.cwd();
|
|
27
|
+
this.config = options.config || this._loadConfig();
|
|
28
|
+
this.reporters = options.reporters || [new ConsoleReporter()];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run quality gate check
|
|
33
|
+
* @param {Object} options - Check options
|
|
34
|
+
* @returns {Promise<Object>} Check results
|
|
35
|
+
*/
|
|
36
|
+
async check(options = {}) {
|
|
37
|
+
const {
|
|
38
|
+
files = null,
|
|
39
|
+
severity = this.config.severity || 'warn',
|
|
40
|
+
rules = null,
|
|
41
|
+
fix = false
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
const filesToCheck = files || this._getProjectFiles();
|
|
45
|
+
const rulesToRun = rules || this._getActiveRules();
|
|
46
|
+
|
|
47
|
+
const results = [];
|
|
48
|
+
let criticalCount = 0;
|
|
49
|
+
let errorCount = 0;
|
|
50
|
+
let warnCount = 0;
|
|
51
|
+
let infoCount = 0;
|
|
52
|
+
|
|
53
|
+
for (const file of filesToCheck) {
|
|
54
|
+
const fileResults = await this._checkFile(file, rulesToRun);
|
|
55
|
+
results.push(...fileResults);
|
|
56
|
+
|
|
57
|
+
for (const r of fileResults) {
|
|
58
|
+
switch (r.severity) {
|
|
59
|
+
case 'critical': criticalCount++; break;
|
|
60
|
+
case 'error': errorCount++; break;
|
|
61
|
+
case 'warn': warnCount++; break;
|
|
62
|
+
case 'info': infoCount++; break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const summary = {
|
|
68
|
+
total: results.length,
|
|
69
|
+
critical: criticalCount,
|
|
70
|
+
error: errorCount,
|
|
71
|
+
warn: warnCount,
|
|
72
|
+
info: infoCount,
|
|
73
|
+
filesChecked: filesToCheck.length,
|
|
74
|
+
rulesRun: rulesToRun.length
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Apply fixes if requested
|
|
78
|
+
let fixedCount = 0;
|
|
79
|
+
if (fix) {
|
|
80
|
+
fixedCount = await this._applyFixes(results.filter(r => r.autoFix));
|
|
81
|
+
summary.fixed = fixedCount;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Determine if gate passes
|
|
85
|
+
const minSeverity = this._severityLevel(severity);
|
|
86
|
+
const passed = this._hasBlockingIssues(results, minSeverity) === false;
|
|
87
|
+
|
|
88
|
+
const checkResult = {
|
|
89
|
+
passed,
|
|
90
|
+
results,
|
|
91
|
+
summary
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Run reporters
|
|
95
|
+
for (const reporter of this.reporters) {
|
|
96
|
+
reporter.report(checkResult);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return checkResult;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check single file against all rules
|
|
104
|
+
* @param {string} filePath - File path
|
|
105
|
+
* @param {Array} rules - Rules to run
|
|
106
|
+
* @returns {Promise<Array>} File check results
|
|
107
|
+
*/
|
|
108
|
+
async _checkFile(filePath, rules) {
|
|
109
|
+
const results = [];
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(filePath)) {
|
|
112
|
+
return [{
|
|
113
|
+
file: filePath,
|
|
114
|
+
rule: 'file-exists',
|
|
115
|
+
ruleName: 'File Exists',
|
|
116
|
+
severity: 'error',
|
|
117
|
+
message: 'File not found',
|
|
118
|
+
pass: false
|
|
119
|
+
}];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const rule of rules) {
|
|
123
|
+
if (!rule.enabled) continue;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const result = await rule.check(filePath, rule.config || {});
|
|
127
|
+
results.push({
|
|
128
|
+
file: filePath,
|
|
129
|
+
rule: rule.id,
|
|
130
|
+
ruleName: rule.name,
|
|
131
|
+
severity: rule.severity,
|
|
132
|
+
message: result.message,
|
|
133
|
+
pass: result.pass,
|
|
134
|
+
skip: result.skip || false,
|
|
135
|
+
fix: result.fix || rule.fix,
|
|
136
|
+
autoFix: result.autoFix || false,
|
|
137
|
+
details: result.details
|
|
138
|
+
});
|
|
139
|
+
} catch (e) {
|
|
140
|
+
results.push({
|
|
141
|
+
file: filePath,
|
|
142
|
+
rule: rule.id,
|
|
143
|
+
ruleName: rule.name,
|
|
144
|
+
severity: 'error',
|
|
145
|
+
message: `Rule execution error: ${e.message}`,
|
|
146
|
+
pass: false
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Apply automatic fixes
|
|
156
|
+
* @param {Array} fixableResults - Results with autoFix flag
|
|
157
|
+
* @returns {Promise<number>} Number of fixes applied
|
|
158
|
+
*/
|
|
159
|
+
async _applyFixes(fixableResults) {
|
|
160
|
+
let fixedCount = 0;
|
|
161
|
+
|
|
162
|
+
for (const result of fixableResults) {
|
|
163
|
+
try {
|
|
164
|
+
const content = fs.readFileSync(result.file, 'utf-8');
|
|
165
|
+
let fixed = content;
|
|
166
|
+
|
|
167
|
+
// Trailing whitespace fix
|
|
168
|
+
if (result.rule === 'no-trailing-whitespace') {
|
|
169
|
+
fixed = content.replace(/[ \t]+$/gm, '');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (fixed !== content) {
|
|
173
|
+
fs.writeFileSync(result.file, fixed, 'utf-8');
|
|
174
|
+
fixedCount++;
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// Skip files that can't be fixed
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return fixedCount;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get all project files
|
|
186
|
+
* @returns {Array<string>} File paths
|
|
187
|
+
*/
|
|
188
|
+
_getProjectFiles() {
|
|
189
|
+
const files = [];
|
|
190
|
+
const ignoreDirs = new Set([
|
|
191
|
+
'node_modules', '.git', 'dist', 'build', '.next',
|
|
192
|
+
'coverage', '.nyc_output', '.cache', 'vendor'
|
|
193
|
+
]);
|
|
194
|
+
const checkExts = new Set([
|
|
195
|
+
'.js', '.ts', '.jsx', '.tsx', '.cjs', '.mjs',
|
|
196
|
+
'.json', '.md', '.py', '.go', '.rs'
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
const scanDir = (dir, depth = 0) => {
|
|
200
|
+
if (depth > 10) return; // Max depth limit
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
204
|
+
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const fullPath = path.join(dir, entry.name);
|
|
207
|
+
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
if (!ignoreDirs.has(entry.name)) {
|
|
210
|
+
scanDir(fullPath, depth + 1);
|
|
211
|
+
}
|
|
212
|
+
} else if (entry.isFile()) {
|
|
213
|
+
const ext = path.extname(entry.name);
|
|
214
|
+
if (checkExts.has(ext)) {
|
|
215
|
+
files.push(fullPath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// Skip directories we can't read
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
scanDir(this.projectDir);
|
|
225
|
+
return files;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get active rules from config
|
|
230
|
+
* @returns {Array} Active rules
|
|
231
|
+
*/
|
|
232
|
+
_getActiveRules() {
|
|
233
|
+
const rules = registry.getAll({ enabled: true });
|
|
234
|
+
const configured = this.config.rules || [];
|
|
235
|
+
|
|
236
|
+
// Apply config overrides
|
|
237
|
+
for (const override of configured) {
|
|
238
|
+
const rule = registry.get(override.id);
|
|
239
|
+
if (rule) {
|
|
240
|
+
if (override.enabled !== undefined) {
|
|
241
|
+
rule.enabled = override.enabled;
|
|
242
|
+
}
|
|
243
|
+
if (override.severity) {
|
|
244
|
+
rule.severity = override.severity;
|
|
245
|
+
}
|
|
246
|
+
if (override.config) {
|
|
247
|
+
rule.config = { ...rule.config, ...override.config };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return rules.filter(r => r.enabled);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Load quality gate config
|
|
257
|
+
* @returns {Object} Configuration
|
|
258
|
+
*/
|
|
259
|
+
_loadConfig() {
|
|
260
|
+
const configPath = path.join(this.projectDir, '.claude', 'quality-gate.json');
|
|
261
|
+
|
|
262
|
+
if (fs.existsSync(configPath)) {
|
|
263
|
+
try {
|
|
264
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
265
|
+
} catch {
|
|
266
|
+
// Fall back to default
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Default config
|
|
271
|
+
return {
|
|
272
|
+
enabled: true,
|
|
273
|
+
severity: 'warn',
|
|
274
|
+
rules: [
|
|
275
|
+
{ id: 'line-count-limit', enabled: true, severity: 'error' },
|
|
276
|
+
{ id: 'file-size-limit', enabled: true, severity: 'warn' },
|
|
277
|
+
{ id: 'no-empty-files', enabled: true, severity: 'warn' },
|
|
278
|
+
{ id: 'no-trailing-whitespace', enabled: true, severity: 'warn' }
|
|
279
|
+
],
|
|
280
|
+
gates: {
|
|
281
|
+
preCommit: true,
|
|
282
|
+
prePush: true,
|
|
283
|
+
onToolUse: false
|
|
284
|
+
},
|
|
285
|
+
reporting: {
|
|
286
|
+
format: 'console'
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Convert severity string to number
|
|
293
|
+
* @param {string} severity - Severity level
|
|
294
|
+
* @returns {number} Numeric level
|
|
295
|
+
*/
|
|
296
|
+
_severityLevel(severity) {
|
|
297
|
+
const levels = { info: 0, warn: 1, error: 2, critical: 3 };
|
|
298
|
+
return levels[severity] || 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if results contain blocking issues
|
|
303
|
+
* @param {Array} results - Check results
|
|
304
|
+
* @param {number} minSeverity - Minimum blocking severity
|
|
305
|
+
* @returns {boolean} Has blocking issues
|
|
306
|
+
*/
|
|
307
|
+
_hasBlockingIssues(results, minSeverity) {
|
|
308
|
+
return results.some(r =>
|
|
309
|
+
!r.pass &&
|
|
310
|
+
!r.skip &&
|
|
311
|
+
this._severityLevel(r.severity) >= minSeverity
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Console reporter
|
|
318
|
+
*/
|
|
319
|
+
class ConsoleReporter {
|
|
320
|
+
report(checkResult) {
|
|
321
|
+
const { passed, results, summary } = checkResult;
|
|
322
|
+
|
|
323
|
+
console.log('\n' + '='.repeat(60));
|
|
324
|
+
console.log('Quality Gate Report');
|
|
325
|
+
console.log('='.repeat(60));
|
|
326
|
+
console.log(`Status: ${passed ? 'PASS' : 'FAIL'}`);
|
|
327
|
+
console.log(`Files: ${summary.filesChecked}`);
|
|
328
|
+
console.log(`Issues: ${summary.total} (${summary.critical} critical, ${summary.error} errors, ${summary.warn} warnings)`);
|
|
329
|
+
console.log('='.repeat(60));
|
|
330
|
+
|
|
331
|
+
// Group by file
|
|
332
|
+
const byFile = {};
|
|
333
|
+
for (const result of results) {
|
|
334
|
+
if (!result.pass && !result.skip) {
|
|
335
|
+
const relPath = path.relative(process.cwd(), result.file);
|
|
336
|
+
if (!byFile[relPath]) byFile[relPath] = [];
|
|
337
|
+
byFile[relPath].push(result);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for (const [file, issues] of Object.entries(byFile)) {
|
|
342
|
+
console.log(`\n${file}:`);
|
|
343
|
+
for (const issue of issues) {
|
|
344
|
+
const icon = { critical: 'X', error: 'E', warn: 'W', info: 'I' }[issue.severity];
|
|
345
|
+
console.log(` [${icon}] ${issue.ruleName}: ${issue.message}`);
|
|
346
|
+
if (issue.fix) {
|
|
347
|
+
console.log(` Fix: ${issue.fix}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* JSON reporter
|
|
358
|
+
*/
|
|
359
|
+
class JsonReporter {
|
|
360
|
+
report(checkResult) {
|
|
361
|
+
console.log(JSON.stringify(checkResult, null, 2));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Markdown reporter
|
|
367
|
+
*/
|
|
368
|
+
class MarkdownReporter {
|
|
369
|
+
report(checkResult) {
|
|
370
|
+
const { passed, results, summary } = checkResult;
|
|
371
|
+
|
|
372
|
+
let output = `# Quality Gate Report\n\n`;
|
|
373
|
+
output += `**Status**: ${passed ? 'PASS :white_check_mark:' : 'FAIL :x:'}\n\n`;
|
|
374
|
+
output += `## Summary\n\n`;
|
|
375
|
+
output += `- Files: ${summary.filesChecked}\n`;
|
|
376
|
+
output += `- Issues: ${summary.total}\n`;
|
|
377
|
+
output += ` - Critical: ${summary.critical}\n`;
|
|
378
|
+
output += ` - Errors: ${summary.error}\n`;
|
|
379
|
+
output += ` - Warnings: ${summary.warn}\n\n`;
|
|
380
|
+
|
|
381
|
+
if (results.length > 0) {
|
|
382
|
+
output += `## Issues\n\n`;
|
|
383
|
+
const byFile = {};
|
|
384
|
+
for (const result of results.filter(r => !r.pass && !r.skip)) {
|
|
385
|
+
const relPath = path.relative(process.cwd(), result.file);
|
|
386
|
+
if (!byFile[relPath]) byFile[relPath] = [];
|
|
387
|
+
byFile[relPath].push(result);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const [file, issues] of Object.entries(byFile)) {
|
|
391
|
+
output += `### ${file}\n`;
|
|
392
|
+
for (const issue of issues) {
|
|
393
|
+
output += `- **${issue.ruleName}** (${issue.severity}): ${issue.message}\n`;
|
|
394
|
+
if (issue.fix) {
|
|
395
|
+
output += ` - Fix: ${issue.fix}\n`;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
output += '\n';
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log(output);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Check quality gate and throw if failed
|
|
408
|
+
* @param {Object} options - Check options
|
|
409
|
+
* @throws {QualityGateError} If check fails
|
|
410
|
+
*/
|
|
411
|
+
async function checkOrThrow(options = {}) {
|
|
412
|
+
const gate = new QualityGate(options);
|
|
413
|
+
const result = await gate.check(options);
|
|
414
|
+
|
|
415
|
+
if (!result.passed) {
|
|
416
|
+
throw new QualityGateError(
|
|
417
|
+
'Quality gate check failed',
|
|
418
|
+
result
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = {
|
|
426
|
+
QualityGate,
|
|
427
|
+
ConsoleReporter,
|
|
428
|
+
JsonReporter,
|
|
429
|
+
MarkdownReporter,
|
|
430
|
+
checkOrThrow
|
|
431
|
+
};
|