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.
@@ -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
+ };