musubi-sdd 6.2.0 → 6.2.2

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,633 @@
1
+ /**
2
+ * Constitutional Checker
3
+ *
4
+ * Validates compliance with Constitutional Articles.
5
+ *
6
+ * Requirement: IMP-6.2-005-01
7
+ * Design: Section 5.1
8
+ */
9
+
10
+ const fs = require('fs').promises;
11
+ const path = require('path');
12
+
13
+ /**
14
+ * Constitutional Articles
15
+ */
16
+ const ARTICLES = {
17
+ I: {
18
+ id: 'I',
19
+ name: 'Specification First',
20
+ description: 'All changes must be traceable to specifications',
21
+ keywords: ['REQ-', 'IMP-', 'FEAT-', 'specification', 'requirement']
22
+ },
23
+ II: {
24
+ id: 'II',
25
+ name: 'Quality Gate',
26
+ description: 'Code must pass quality gates before merge',
27
+ keywords: ['test', 'coverage', 'lint', 'quality']
28
+ },
29
+ III: {
30
+ id: 'III',
31
+ name: 'Test-First',
32
+ description: 'Tests should be written before or alongside implementation',
33
+ keywords: ['test', 'spec', 'describe', 'it(']
34
+ },
35
+ IV: {
36
+ id: 'IV',
37
+ name: 'Incremental Delivery',
38
+ description: 'Features should be delivered incrementally',
39
+ keywords: ['sprint', 'iteration', 'milestone']
40
+ },
41
+ V: {
42
+ id: 'V',
43
+ name: 'Consistency',
44
+ description: 'Code style and patterns must be consistent',
45
+ keywords: ['eslint', 'prettier', 'style']
46
+ },
47
+ VI: {
48
+ id: 'VI',
49
+ name: 'Change Tracking',
50
+ description: 'All changes must be tracked and documented',
51
+ keywords: ['changelog', 'commit', 'version']
52
+ },
53
+ VII: {
54
+ id: 'VII',
55
+ name: 'Simplicity',
56
+ description: 'Prefer simple solutions over complex ones',
57
+ thresholds: {
58
+ maxFileLines: 500,
59
+ maxFunctionLines: 50,
60
+ maxCyclomaticComplexity: 10,
61
+ maxDependencies: 10
62
+ }
63
+ },
64
+ VIII: {
65
+ id: 'VIII',
66
+ name: 'Anti-Abstraction',
67
+ description: 'Avoid premature abstraction',
68
+ patterns: [
69
+ /abstract\s+class/i,
70
+ /implements\s+\w+Factory/i,
71
+ /extends\s+Base\w+/i
72
+ ]
73
+ },
74
+ IX: {
75
+ id: 'IX',
76
+ name: 'Documentation',
77
+ description: 'Code must be documented',
78
+ keywords: ['jsdoc', '@param', '@returns', '@description']
79
+ }
80
+ };
81
+
82
+ /**
83
+ * Violation severity levels
84
+ */
85
+ const SEVERITY = {
86
+ CRITICAL: 'critical',
87
+ HIGH: 'high',
88
+ MEDIUM: 'medium',
89
+ LOW: 'low'
90
+ };
91
+
92
+ /**
93
+ * ConstitutionalChecker
94
+ *
95
+ * Validates code against Constitutional Articles.
96
+ */
97
+ class ConstitutionalChecker {
98
+ /**
99
+ * @param {Object} config - Configuration options
100
+ */
101
+ constructor(config = {}) {
102
+ this.config = {
103
+ articleVII: ARTICLES.VII.thresholds,
104
+ ...config
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Check file for constitutional violations
110
+ * @param {string} filePath - File path to check
111
+ * @returns {Promise<Object>} Check result
112
+ */
113
+ async checkFile(filePath) {
114
+ const content = await fs.readFile(filePath, 'utf-8');
115
+ const violations = [];
116
+
117
+ // Article I: Specification First
118
+ const specViolation = this.checkArticleI(content, filePath);
119
+ if (specViolation) violations.push(specViolation);
120
+
121
+ // Article III: Test-First (for non-test files)
122
+ if (!filePath.includes('.test.') && !filePath.includes('.spec.')) {
123
+ const testViolation = await this.checkArticleIII(filePath);
124
+ if (testViolation) violations.push(testViolation);
125
+ }
126
+
127
+ // Article VII: Simplicity
128
+ const simplicityViolations = this.checkArticleVII(content, filePath);
129
+ violations.push(...simplicityViolations);
130
+
131
+ // Article VIII: Anti-Abstraction
132
+ const abstractionViolations = this.checkArticleVIII(content, filePath);
133
+ violations.push(...abstractionViolations);
134
+
135
+ // Article IX: Documentation
136
+ const docViolation = this.checkArticleIX(content, filePath);
137
+ if (docViolation) violations.push(docViolation);
138
+
139
+ return {
140
+ filePath,
141
+ violations,
142
+ passed: violations.length === 0,
143
+ checkedAt: new Date().toISOString()
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Check Article I: Specification First
149
+ * @param {string} content - File content
150
+ * @param {string} filePath - File path
151
+ * @returns {Object|null} Violation or null
152
+ */
153
+ checkArticleI(content, filePath) {
154
+ // Check if file has requirement reference
155
+ const hasReqRef = ARTICLES.I.keywords.some(kw =>
156
+ content.includes(kw)
157
+ );
158
+
159
+ // Skip check for certain file types
160
+ const skipPatterns = [
161
+ /\.test\./,
162
+ /\.spec\./,
163
+ /\.config\./,
164
+ /index\./,
165
+ /package\.json/
166
+ ];
167
+
168
+ if (skipPatterns.some(p => p.test(filePath))) {
169
+ return null;
170
+ }
171
+
172
+ if (!hasReqRef) {
173
+ return {
174
+ article: 'I',
175
+ articleName: ARTICLES.I.name,
176
+ severity: SEVERITY.MEDIUM,
177
+ message: 'ファイルに要件参照(REQ-XXX、IMP-XXX等)がありません',
178
+ filePath,
179
+ suggestion: 'コードコメントまたはJSDocに関連する要件IDを追加してください'
180
+ };
181
+ }
182
+
183
+ return null;
184
+ }
185
+
186
+ /**
187
+ * Check Article III: Test-First
188
+ * @param {string} filePath - Source file path
189
+ * @returns {Promise<Object|null>} Violation or null
190
+ */
191
+ async checkArticleIII(filePath) {
192
+ // Derive test file path
193
+ const dir = path.dirname(filePath);
194
+ const ext = path.extname(filePath);
195
+ const base = path.basename(filePath, ext);
196
+
197
+ const testPaths = [
198
+ path.join(dir, `${base}.test${ext}`),
199
+ path.join(dir, `${base}.spec${ext}`),
200
+ path.join(dir, '__tests__', `${base}.test${ext}`),
201
+ filePath.replace('/src/', '/tests/').replace(ext, `.test${ext}`)
202
+ ];
203
+
204
+ for (const testPath of testPaths) {
205
+ try {
206
+ await fs.access(testPath);
207
+ return null; // Test file exists
208
+ } catch {
209
+ // Continue checking
210
+ }
211
+ }
212
+
213
+ return {
214
+ article: 'III',
215
+ articleName: ARTICLES.III.name,
216
+ severity: SEVERITY.HIGH,
217
+ message: '対応するテストファイルがありません',
218
+ filePath,
219
+ suggestion: `テストファイル(例: ${base}.test${ext})を作成してください`
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Check Article VII: Simplicity
225
+ * @param {string} content - File content
226
+ * @param {string} filePath - File path
227
+ * @returns {Array} Violations
228
+ */
229
+ checkArticleVII(content, filePath) {
230
+ const violations = [];
231
+ const lines = content.split('\n');
232
+ const thresholds = this.config.articleVII;
233
+
234
+ // Check file length
235
+ if (lines.length > thresholds.maxFileLines) {
236
+ violations.push({
237
+ article: 'VII',
238
+ articleName: ARTICLES.VII.name,
239
+ severity: SEVERITY.HIGH,
240
+ message: `ファイルが長すぎます(${lines.length}行 > ${thresholds.maxFileLines}行)`,
241
+ filePath,
242
+ suggestion: 'ファイルを複数のモジュールに分割してください'
243
+ });
244
+ }
245
+
246
+ // Check function length (simple heuristic)
247
+ const functionMatches = content.match(/(?:function\s+\w+|(?:async\s+)?(?:\w+\s*=\s*)?(?:async\s+)?(?:function|\([^)]*\)\s*=>|\w+\s*\([^)]*\)\s*{))/g);
248
+ if (functionMatches && functionMatches.length > 0) {
249
+ // Count functions with many lines (rough estimate)
250
+ const longFunctions = this.findLongFunctions(content, thresholds.maxFunctionLines);
251
+ for (const fn of longFunctions) {
252
+ violations.push({
253
+ article: 'VII',
254
+ articleName: ARTICLES.VII.name,
255
+ severity: SEVERITY.MEDIUM,
256
+ message: `関数 "${fn.name}" が長すぎます(約${fn.lines}行 > ${thresholds.maxFunctionLines}行)`,
257
+ filePath,
258
+ line: fn.startLine,
259
+ suggestion: '関数をより小さな関数に分割してください'
260
+ });
261
+ }
262
+ }
263
+
264
+ // Check dependencies (require/import count)
265
+ const imports = content.match(/(?:require\s*\(|import\s+)/g) || [];
266
+ if (imports.length > thresholds.maxDependencies) {
267
+ violations.push({
268
+ article: 'VII',
269
+ articleName: ARTICLES.VII.name,
270
+ severity: SEVERITY.MEDIUM,
271
+ message: `依存関係が多すぎます(${imports.length}個 > ${thresholds.maxDependencies}個)`,
272
+ filePath,
273
+ suggestion: '依存関係を見直し、必要に応じてモジュールを再構成してください'
274
+ });
275
+ }
276
+
277
+ return violations;
278
+ }
279
+
280
+ /**
281
+ * Find functions that exceed line limit
282
+ * @param {string} content - File content
283
+ * @param {number} maxLines - Maximum lines
284
+ * @returns {Array} Long functions
285
+ */
286
+ findLongFunctions(content, maxLines) {
287
+ const longFunctions = [];
288
+ const lines = content.split('\n');
289
+
290
+ // Simple bracket matching for function detection
291
+ const functionPattern = /(?:async\s+)?(?:function\s+(\w+)|(\w+)\s*(?:=|:)\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))/g;
292
+ let match;
293
+
294
+ while ((match = functionPattern.exec(content)) !== null) {
295
+ const fnName = match[1] || match[2] || 'anonymous';
296
+ const startIndex = match.index;
297
+ const startLine = content.substring(0, startIndex).split('\n').length;
298
+
299
+ // Find function end (simple brace counting)
300
+ let braceCount = 0;
301
+ let started = false;
302
+ let endLine = startLine;
303
+
304
+ for (let i = startLine - 1; i < lines.length; i++) {
305
+ const line = lines[i];
306
+ for (const char of line) {
307
+ if (char === '{') {
308
+ braceCount++;
309
+ started = true;
310
+ } else if (char === '}') {
311
+ braceCount--;
312
+ }
313
+ }
314
+ if (started && braceCount === 0) {
315
+ endLine = i + 1;
316
+ break;
317
+ }
318
+ }
319
+
320
+ const lineCount = endLine - startLine + 1;
321
+ if (lineCount > maxLines) {
322
+ longFunctions.push({
323
+ name: fnName,
324
+ startLine,
325
+ lines: lineCount
326
+ });
327
+ }
328
+ }
329
+
330
+ return longFunctions;
331
+ }
332
+
333
+ /**
334
+ * Check Article VIII: Anti-Abstraction
335
+ * @param {string} content - File content
336
+ * @param {string} filePath - File path
337
+ * @returns {Array} Violations
338
+ */
339
+ checkArticleVIII(content, filePath) {
340
+ const violations = [];
341
+
342
+ for (const pattern of ARTICLES.VIII.patterns) {
343
+ const match = content.match(pattern);
344
+ if (match) {
345
+ violations.push({
346
+ article: 'VIII',
347
+ articleName: ARTICLES.VIII.name,
348
+ severity: SEVERITY.HIGH,
349
+ message: `早すぎる抽象化の可能性: "${match[0]}"`,
350
+ filePath,
351
+ suggestion: '具体的な実装から始め、必要に応じて後から抽象化してください'
352
+ });
353
+ }
354
+ }
355
+
356
+ return violations;
357
+ }
358
+
359
+ /**
360
+ * Check Article IX: Documentation
361
+ * @param {string} content - File content
362
+ * @param {string} filePath - File path
363
+ * @returns {Object|null} Violation or null
364
+ */
365
+ checkArticleIX(content, filePath) {
366
+ // Check for JSDoc presence
367
+ const hasJSDoc = content.includes('/**') && content.includes('*/');
368
+ const hasDescription = ARTICLES.IX.keywords.some(kw => content.includes(kw));
369
+
370
+ // Skip test files
371
+ if (filePath.includes('.test.') || filePath.includes('.spec.')) {
372
+ return null;
373
+ }
374
+
375
+ if (!hasJSDoc || !hasDescription) {
376
+ return {
377
+ article: 'IX',
378
+ articleName: ARTICLES.IX.name,
379
+ severity: SEVERITY.LOW,
380
+ message: 'ドキュメンテーションが不足しています',
381
+ filePath,
382
+ suggestion: 'JSDocコメントを追加してください'
383
+ };
384
+ }
385
+
386
+ return null;
387
+ }
388
+
389
+ /**
390
+ * Check multiple files
391
+ * @param {Array} filePaths - File paths to check
392
+ * @returns {Promise<Object>} Check results
393
+ */
394
+ async checkFiles(filePaths) {
395
+ const results = [];
396
+ let totalViolations = 0;
397
+ const violationsByArticle = {};
398
+
399
+ for (const filePath of filePaths) {
400
+ try {
401
+ const result = await this.checkFile(filePath);
402
+ results.push(result);
403
+ totalViolations += result.violations.length;
404
+
405
+ for (const v of result.violations) {
406
+ if (!violationsByArticle[v.article]) {
407
+ violationsByArticle[v.article] = 0;
408
+ }
409
+ violationsByArticle[v.article]++;
410
+ }
411
+ } catch (error) {
412
+ results.push({
413
+ filePath,
414
+ error: error.message,
415
+ violations: [],
416
+ passed: false
417
+ });
418
+ }
419
+ }
420
+
421
+ return {
422
+ results,
423
+ summary: {
424
+ filesChecked: filePaths.length,
425
+ filesPassed: results.filter(r => r.passed).length,
426
+ filesFailed: results.filter(r => !r.passed).length,
427
+ totalViolations,
428
+ violationsByArticle
429
+ },
430
+ checkedAt: new Date().toISOString()
431
+ };
432
+ }
433
+
434
+ /**
435
+ * Check directory recursively
436
+ * @param {string} directory - Directory to check
437
+ * @param {Object} options - Options
438
+ * @returns {Promise<Object>} Check results
439
+ */
440
+ async checkDirectory(directory, options = {}) {
441
+ const extensions = options.extensions || ['.js', '.ts'];
442
+ const exclude = options.exclude || ['node_modules', '.git', 'dist', 'coverage'];
443
+
444
+ const files = await this.findFiles(directory, extensions, exclude);
445
+ return await this.checkFiles(files);
446
+ }
447
+
448
+ /**
449
+ * Find files recursively
450
+ * @param {string} dir - Directory
451
+ * @param {Array} extensions - File extensions
452
+ * @param {Array} exclude - Exclude patterns
453
+ * @returns {Promise<Array>} File paths
454
+ */
455
+ async findFiles(dir, extensions, exclude) {
456
+ const files = [];
457
+
458
+ try {
459
+ const entries = await fs.readdir(dir, { withFileTypes: true });
460
+
461
+ for (const entry of entries) {
462
+ const fullPath = path.join(dir, entry.name);
463
+
464
+ if (exclude.some(e => entry.name.includes(e))) {
465
+ continue;
466
+ }
467
+
468
+ if (entry.isDirectory()) {
469
+ const subFiles = await this.findFiles(fullPath, extensions, exclude);
470
+ files.push(...subFiles);
471
+ } else if (extensions.some(ext => entry.name.endsWith(ext))) {
472
+ files.push(fullPath);
473
+ }
474
+ }
475
+ } catch {
476
+ // Ignore errors
477
+ }
478
+
479
+ return files;
480
+ }
481
+
482
+ /**
483
+ * Check if merge should be blocked
484
+ * @param {Object} results - Check results
485
+ * @returns {Object} Block decision
486
+ */
487
+ shouldBlockMerge(results) {
488
+ const criticalViolations = results.results
489
+ .flatMap(r => r.violations)
490
+ .filter(v => v.severity === SEVERITY.CRITICAL);
491
+
492
+ const highViolations = results.results
493
+ .flatMap(r => r.violations)
494
+ .filter(v => v.severity === SEVERITY.HIGH);
495
+
496
+ // Block on Article VII or VIII high violations
497
+ const phaseMinusOneViolations = results.results
498
+ .flatMap(r => r.violations)
499
+ .filter(v => (v.article === 'VII' || v.article === 'VIII') &&
500
+ (v.severity === SEVERITY.HIGH || v.severity === SEVERITY.CRITICAL));
501
+
502
+ return {
503
+ shouldBlock: criticalViolations.length > 0 || phaseMinusOneViolations.length > 0,
504
+ reason: criticalViolations.length > 0
505
+ ? 'クリティカルな違反があります'
506
+ : phaseMinusOneViolations.length > 0
507
+ ? 'Article VII/VIII違反によりPhase -1 Gateレビューが必要です'
508
+ : null,
509
+ criticalCount: criticalViolations.length,
510
+ highCount: highViolations.length,
511
+ requiresPhaseMinusOne: phaseMinusOneViolations.length > 0
512
+ };
513
+ }
514
+
515
+ /**
516
+ * Generate compliance report
517
+ * @param {Object} results - Check results
518
+ * @returns {string} Markdown report
519
+ */
520
+ generateReport(results) {
521
+ const lines = [];
522
+ const blockDecision = this.shouldBlockMerge(results);
523
+
524
+ lines.push('# Constitutional Compliance Report');
525
+ lines.push('');
526
+ lines.push(`**Generated:** ${results.checkedAt}`);
527
+ lines.push('');
528
+
529
+ // Summary
530
+ lines.push('## Summary');
531
+ lines.push('');
532
+ lines.push('| Metric | Value |');
533
+ lines.push('|--------|-------|');
534
+ lines.push(`| Files Checked | ${results.summary.filesChecked} |`);
535
+ lines.push(`| Files Passed | ${results.summary.filesPassed} |`);
536
+ lines.push(`| Files Failed | ${results.summary.filesFailed} |`);
537
+ lines.push(`| Total Violations | ${results.summary.totalViolations} |`);
538
+ lines.push('');
539
+
540
+ // Block decision
541
+ if (blockDecision.shouldBlock) {
542
+ lines.push('## ⛔ Merge Blocked');
543
+ lines.push('');
544
+ lines.push(`**Reason:** ${blockDecision.reason}`);
545
+ if (blockDecision.requiresPhaseMinusOne) {
546
+ lines.push('');
547
+ lines.push('> Phase -1 Gate レビューが必要です。System Architectの承認を得てください。');
548
+ }
549
+ lines.push('');
550
+ } else {
551
+ lines.push('## ✅ Merge Allowed');
552
+ lines.push('');
553
+ }
554
+
555
+ // Violations by Article
556
+ lines.push('## Violations by Article');
557
+ lines.push('');
558
+ for (const [article, count] of Object.entries(results.summary.violationsByArticle)) {
559
+ const articleInfo = ARTICLES[article];
560
+ lines.push(`- **Article ${article}** (${articleInfo?.name || 'Unknown'}): ${count} violations`);
561
+ }
562
+ lines.push('');
563
+
564
+ // Detailed violations
565
+ if (results.summary.totalViolations > 0) {
566
+ lines.push('## Detailed Violations');
567
+ lines.push('');
568
+
569
+ for (const result of results.results) {
570
+ if (result.violations.length > 0) {
571
+ lines.push(`### ${result.filePath}`);
572
+ lines.push('');
573
+ for (const v of result.violations) {
574
+ const emoji = v.severity === SEVERITY.CRITICAL ? '🔴' :
575
+ v.severity === SEVERITY.HIGH ? '🟠' :
576
+ v.severity === SEVERITY.MEDIUM ? '🟡' : '🟢';
577
+ lines.push(`${emoji} **Article ${v.article}** (${v.severity}): ${v.message}`);
578
+ if (v.line) {
579
+ lines.push(` - Line: ${v.line}`);
580
+ }
581
+ lines.push(` - Suggestion: ${v.suggestion}`);
582
+ lines.push('');
583
+ }
584
+ }
585
+ }
586
+ }
587
+
588
+ return lines.join('\n');
589
+ }
590
+
591
+ /**
592
+ * Save check results
593
+ * @param {string} featureId - Feature ID
594
+ * @param {Object} results - Check results
595
+ */
596
+ async saveResults(featureId, results) {
597
+ await this.ensureStorageDir();
598
+ const filePath = path.join(this.config.storageDir, `${featureId}.json`);
599
+ await fs.writeFile(filePath, JSON.stringify(results, null, 2), 'utf-8');
600
+ }
601
+
602
+ /**
603
+ * Load check results
604
+ * @param {string} featureId - Feature ID
605
+ * @returns {Promise<Object|null>} Results or null
606
+ */
607
+ async loadResults(featureId) {
608
+ try {
609
+ const filePath = path.join(this.config.storageDir, `${featureId}.json`);
610
+ const content = await fs.readFile(filePath, 'utf-8');
611
+ return JSON.parse(content);
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Ensure storage directory exists
619
+ */
620
+ async ensureStorageDir() {
621
+ try {
622
+ await fs.access(this.config.storageDir);
623
+ } catch {
624
+ await fs.mkdir(this.config.storageDir, { recursive: true });
625
+ }
626
+ }
627
+ }
628
+
629
+ module.exports = {
630
+ ConstitutionalChecker,
631
+ ARTICLES,
632
+ SEVERITY
633
+ };