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,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.0",
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",