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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Rules Registry
|
|
3
|
+
*
|
|
4
|
+
* Pluggable rule system for code quality checks.
|
|
5
|
+
* Rules can be defined in-code or loaded from config files (YAML/JSON).
|
|
6
|
+
*
|
|
7
|
+
* @module lib/quality-rules
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Rule Registry
|
|
15
|
+
* Manages quality check rules
|
|
16
|
+
*/
|
|
17
|
+
class RuleRegistry {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.rules = new Map();
|
|
20
|
+
this._registerBuiltInRules();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register a rule
|
|
25
|
+
* @param {string} id - Rule identifier
|
|
26
|
+
* @param {Object} definition - Rule definition
|
|
27
|
+
*/
|
|
28
|
+
register(id, definition) {
|
|
29
|
+
const rule = {
|
|
30
|
+
id,
|
|
31
|
+
name: definition.name || id,
|
|
32
|
+
description: definition.description || '',
|
|
33
|
+
severity: definition.severity || 'warn',
|
|
34
|
+
enabled: definition.enabled !== false,
|
|
35
|
+
check: definition.check,
|
|
36
|
+
fix: definition.fix || null,
|
|
37
|
+
config: definition.config || {}
|
|
38
|
+
};
|
|
39
|
+
this.rules.set(id, rule);
|
|
40
|
+
return rule;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get rule by ID
|
|
45
|
+
* @param {string} id - Rule identifier
|
|
46
|
+
* @returns {Object|null} Rule object
|
|
47
|
+
*/
|
|
48
|
+
get(id) {
|
|
49
|
+
return this.rules.get(id) || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if rule exists
|
|
54
|
+
* @param {string} id - Rule identifier
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
has(id) {
|
|
58
|
+
return this.rules.has(id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get all rules, optionally filtered
|
|
63
|
+
* @param {Object} filter - Filter options
|
|
64
|
+
* @returns {Array} Array of rules
|
|
65
|
+
*/
|
|
66
|
+
getAll(filter = {}) {
|
|
67
|
+
let rules = Array.from(this.rules.values());
|
|
68
|
+
|
|
69
|
+
if (filter.severity) {
|
|
70
|
+
rules = rules.filter(r => r.severity === filter.severity);
|
|
71
|
+
}
|
|
72
|
+
if (filter.enabled !== undefined) {
|
|
73
|
+
rules = rules.filter(r => r.enabled === filter.enabled);
|
|
74
|
+
}
|
|
75
|
+
if (filter.category) {
|
|
76
|
+
rules = rules.filter(r => r.category === filter.category);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return rules;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Enable/disable a rule
|
|
84
|
+
* @param {string} id - Rule identifier
|
|
85
|
+
* @param {boolean} enabled - Enable state
|
|
86
|
+
*/
|
|
87
|
+
setEnabled(id, enabled) {
|
|
88
|
+
const rule = this.rules.get(id);
|
|
89
|
+
if (rule) {
|
|
90
|
+
rule.enabled = enabled;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Update rule configuration
|
|
96
|
+
* @param {string} id - Rule identifier
|
|
97
|
+
* @param {Object} config - New configuration
|
|
98
|
+
*/
|
|
99
|
+
updateConfig(id, config) {
|
|
100
|
+
const rule = this.rules.get(id);
|
|
101
|
+
if (rule) {
|
|
102
|
+
rule.config = { ...rule.config, ...config };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load rules from config file
|
|
108
|
+
* @param {string} filePath - Path to rules config file
|
|
109
|
+
*/
|
|
110
|
+
loadFromFile(filePath) {
|
|
111
|
+
if (!fs.existsSync(filePath)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
116
|
+
let config;
|
|
117
|
+
|
|
118
|
+
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
|
|
119
|
+
// Try to load YAML parser
|
|
120
|
+
try {
|
|
121
|
+
const yaml = require('yaml');
|
|
122
|
+
config = yaml.parse(content);
|
|
123
|
+
} catch {
|
|
124
|
+
console.warn(`YAML parser not available, skipping ${filePath}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
config = JSON.parse(content);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Register rules from config
|
|
132
|
+
for (const ruleDef of config.rules || []) {
|
|
133
|
+
if (ruleDef.id) {
|
|
134
|
+
const existing = this.rules.get(ruleDef.id);
|
|
135
|
+
if (existing) {
|
|
136
|
+
// Update existing rule
|
|
137
|
+
Object.assign(existing, ruleDef);
|
|
138
|
+
} else {
|
|
139
|
+
// Register new rule with function check
|
|
140
|
+
this.register(ruleDef.id, ruleDef);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Register built-in rules
|
|
148
|
+
*/
|
|
149
|
+
_registerBuiltInRules() {
|
|
150
|
+
// File size rule
|
|
151
|
+
this.register('file-size-limit', {
|
|
152
|
+
name: 'File Size Limit',
|
|
153
|
+
description: 'Ensure files do not exceed size limit',
|
|
154
|
+
category: 'size',
|
|
155
|
+
severity: 'warn',
|
|
156
|
+
enabled: true,
|
|
157
|
+
config: { maxSize: 800 * 1024 }, // 800KB
|
|
158
|
+
check: (file, config) => {
|
|
159
|
+
const stats = fs.statSync(file);
|
|
160
|
+
const maxSize = config.maxSize || 800 * 1024;
|
|
161
|
+
if (stats.size > maxSize) {
|
|
162
|
+
return {
|
|
163
|
+
pass: false,
|
|
164
|
+
message: `File size (${(stats.size / 1024).toFixed(0)}KB) exceeds limit (${(maxSize / 1024).toFixed(0)}KB)`,
|
|
165
|
+
fix: 'Consider splitting the file or removing unused code'
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { pass: true };
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Line count rule
|
|
173
|
+
this.register('line-count-limit', {
|
|
174
|
+
name: 'Line Count Limit',
|
|
175
|
+
description: 'Ensure files do not exceed line count limit',
|
|
176
|
+
category: 'size',
|
|
177
|
+
severity: 'error',
|
|
178
|
+
enabled: true,
|
|
179
|
+
config: { maxLines: 800 },
|
|
180
|
+
check: (file, config) => {
|
|
181
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
182
|
+
const lines = content.split('\n').length;
|
|
183
|
+
const maxLines = config.maxLines || 800;
|
|
184
|
+
if (lines > maxLines) {
|
|
185
|
+
return {
|
|
186
|
+
pass: false,
|
|
187
|
+
message: `File (${lines} lines) exceeds line limit (${maxLines})`,
|
|
188
|
+
fix: 'Consider splitting into smaller modules'
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return { pass: true };
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Console.log detection rule
|
|
196
|
+
this.register('no-console-logs', {
|
|
197
|
+
name: 'No Console Logs',
|
|
198
|
+
description: 'Detect console.log statements in production code',
|
|
199
|
+
category: 'code-style',
|
|
200
|
+
severity: 'warn',
|
|
201
|
+
enabled: false,
|
|
202
|
+
check: (file) => {
|
|
203
|
+
const ext = path.extname(file);
|
|
204
|
+
if (!['.js', '.jsx', '.ts', '.tsx', '.cjs', '.mjs'].includes(ext)) {
|
|
205
|
+
return { pass: true, skip: true };
|
|
206
|
+
}
|
|
207
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
208
|
+
// Match console.log/debug/info/warn but not console.error
|
|
209
|
+
const matches = content.matchAll(/console\.(log|debug|info|warn)\(/g);
|
|
210
|
+
const count = [...matches].length;
|
|
211
|
+
if (count > 0) {
|
|
212
|
+
return {
|
|
213
|
+
pass: false,
|
|
214
|
+
message: `Found ${count} console statement(s)`,
|
|
215
|
+
fix: 'Remove or replace with proper logging library'
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return { pass: true };
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// TODO comments rule
|
|
223
|
+
this.register('todo-comments', {
|
|
224
|
+
name: 'TODO Comments Check',
|
|
225
|
+
description: 'Track TODO/FIXME comments in code',
|
|
226
|
+
category: 'documentation',
|
|
227
|
+
severity: 'info',
|
|
228
|
+
enabled: true,
|
|
229
|
+
check: (file) => {
|
|
230
|
+
const ext = path.extname(file);
|
|
231
|
+
if (!['.js', '.jsx', '.ts', '.tsx', '.cjs', '.mjs', '.py', '.go'].includes(ext)) {
|
|
232
|
+
return { pass: true, skip: true };
|
|
233
|
+
}
|
|
234
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
235
|
+
const todoRegex = /(?:TODO|FIXME|XXX|HACK|NOTE):?\s*(.+)/gi;
|
|
236
|
+
const todos = [...content.matchAll(todoRegex)];
|
|
237
|
+
if (todos.length > 0) {
|
|
238
|
+
return {
|
|
239
|
+
pass: true, // Just informational
|
|
240
|
+
message: `${todos.length} TODO comment(s) found`,
|
|
241
|
+
details: todos.map(m => m[1].trim())
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return { pass: true };
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Directory depth rule
|
|
249
|
+
this.register('directory-depth', {
|
|
250
|
+
name: 'Directory Depth Limit',
|
|
251
|
+
description: 'Ensure directory structure is not too deep',
|
|
252
|
+
category: 'structure',
|
|
253
|
+
severity: 'warn',
|
|
254
|
+
enabled: true,
|
|
255
|
+
config: { maxDepth: 6 },
|
|
256
|
+
check: (file, config) => {
|
|
257
|
+
const maxDepth = config.maxDepth || 6;
|
|
258
|
+
const depth = file.split(path.sep).length;
|
|
259
|
+
if (depth > maxDepth) {
|
|
260
|
+
return {
|
|
261
|
+
pass: false,
|
|
262
|
+
message: `Directory depth (${depth}) exceeds limit (${maxDepth})`,
|
|
263
|
+
fix: 'Consider flattening the directory structure'
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return { pass: true };
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Empty file rule
|
|
271
|
+
this.register('no-empty-files', {
|
|
272
|
+
name: 'No Empty Files',
|
|
273
|
+
description: 'Detect empty or near-empty files',
|
|
274
|
+
category: 'quality',
|
|
275
|
+
severity: 'warn',
|
|
276
|
+
enabled: true,
|
|
277
|
+
config: { minLines: 3 },
|
|
278
|
+
check: (file, config) => {
|
|
279
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
280
|
+
const lines = content.trim().split('\n').filter(l => l.trim());
|
|
281
|
+
const minLines = config.minLines || 3;
|
|
282
|
+
if (lines.length < minLines) {
|
|
283
|
+
return {
|
|
284
|
+
pass: false,
|
|
285
|
+
message: `File has only ${lines.length} line(s)`,
|
|
286
|
+
fix: 'Add content or remove the file'
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { pass: true };
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Trailing whitespace rule
|
|
294
|
+
this.register('no-trailing-whitespace', {
|
|
295
|
+
name: 'No Trailing Whitespace',
|
|
296
|
+
description: 'Detect trailing whitespace on lines',
|
|
297
|
+
category: 'code-style',
|
|
298
|
+
severity: 'warn',
|
|
299
|
+
enabled: true,
|
|
300
|
+
check: (file) => {
|
|
301
|
+
const ext = path.extname(file);
|
|
302
|
+
if (!['.js', '.jsx', '.ts', '.tsx', '.cjs', '.mjs', '.py', '.md', '.txt'].includes(ext)) {
|
|
303
|
+
return { pass: true, skip: true };
|
|
304
|
+
}
|
|
305
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
306
|
+
const lines = content.split('\n');
|
|
307
|
+
const trailing = [];
|
|
308
|
+
lines.forEach((line, i) => {
|
|
309
|
+
if (line !== line.trimEnd()) {
|
|
310
|
+
trailing.push(i + 1);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
if (trailing.length > 0) {
|
|
314
|
+
return {
|
|
315
|
+
pass: false,
|
|
316
|
+
message: `Trailing whitespace on ${trailing.length} line(s)`,
|
|
317
|
+
fix: 'Run code formatter to fix',
|
|
318
|
+
autoFix: true
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return { pass: true };
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Large function rule (basic)
|
|
326
|
+
this.register('function-length', {
|
|
327
|
+
name: 'Function Length Limit',
|
|
328
|
+
description: 'Functions should not exceed line limit',
|
|
329
|
+
category: 'complexity',
|
|
330
|
+
severity: 'warn',
|
|
331
|
+
enabled: false,
|
|
332
|
+
config: { maxLines: 50 },
|
|
333
|
+
check: (file, config) => {
|
|
334
|
+
const ext = path.extname(file);
|
|
335
|
+
if (!['.js', '.jsx', '.ts', '.tsx', '.cjs', '.mjs'].includes(ext)) {
|
|
336
|
+
return { pass: true, skip: true };
|
|
337
|
+
}
|
|
338
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
339
|
+
const maxLines = config.maxLines || 50;
|
|
340
|
+
|
|
341
|
+
// Simple function block detection
|
|
342
|
+
const functionPattern = /(?:function\s+\w+|const\s+\w+\s*=\s*(?:async\s*)?(?:\([^)]*\)\s*=>|\([^)]*\)\s*{))[\s\S]*?\n([\s\S]{30,})\n/g;
|
|
343
|
+
const matches = [...content.matchAll(functionPattern)];
|
|
344
|
+
|
|
345
|
+
if (matches.length > 0) {
|
|
346
|
+
return {
|
|
347
|
+
pass: false,
|
|
348
|
+
message: `Found ${matches.length} function(s) exceeding ${maxLines} lines`,
|
|
349
|
+
fix: 'Break large functions into smaller ones'
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return { pass: true };
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Global registry instance
|
|
359
|
+
const globalRegistry = new RuleRegistry();
|
|
360
|
+
|
|
361
|
+
module.exports = {
|
|
362
|
+
RuleRegistry,
|
|
363
|
+
registry: globalRegistry,
|
|
364
|
+
|
|
365
|
+
// Convenience functions using global registry
|
|
366
|
+
register: (id, def) => globalRegistry.register(id, def),
|
|
367
|
+
get: (id) => globalRegistry.get(id),
|
|
368
|
+
has: (id) => globalRegistry.has(id),
|
|
369
|
+
getAll: (filter) => globalRegistry.getAll(filter),
|
|
370
|
+
setEnabled: (id, enabled) => globalRegistry.setEnabled(id, enabled),
|
|
371
|
+
updateConfig: (id, config) => globalRegistry.updateConfig(id, config),
|
|
372
|
+
loadFromFile: (path) => globalRegistry.loadFromFile(path)
|
|
373
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sumulige-claude",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "The Best Agent Harness for Claude Code",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=16.0.0"
|
|
42
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"ajv": "^8.17.1",
|
|
45
|
+
"ajv-formats": "^3.0.1"
|
|
46
|
+
},
|
|
43
47
|
"devDependencies": {
|
|
44
48
|
"jest": "^30.2.0",
|
|
45
49
|
"mock-fs": "^5.5.0",
|