musubi-sdd 6.1.2 → 6.2.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/README.ja.md +60 -1
- package/README.md +60 -1
- package/bin/musubi-dashboard.js +340 -0
- package/package.json +3 -2
- package/src/cli/dashboard-cli.js +536 -0
- package/src/constitutional/checker.js +633 -0
- package/src/constitutional/ci-reporter.js +336 -0
- package/src/constitutional/index.js +22 -0
- package/src/constitutional/phase-minus-one.js +404 -0
- package/src/constitutional/steering-sync.js +473 -0
- package/src/dashboard/index.js +20 -0
- package/src/dashboard/sprint-planner.js +361 -0
- package/src/dashboard/sprint-reporter.js +378 -0
- package/src/dashboard/transition-recorder.js +209 -0
- package/src/dashboard/workflow-dashboard.js +434 -0
- package/src/enterprise/error-recovery.js +524 -0
- package/src/enterprise/experiment-report.js +573 -0
- package/src/enterprise/index.js +57 -4
- package/src/enterprise/rollback-manager.js +584 -0
- package/src/enterprise/tech-article.js +509 -0
- package/src/orchestration/builtin-skills.js +425 -0
- package/src/templates/agents/claude-code/skills/design-reviewer/SKILL.md +1135 -0
- package/src/templates/agents/claude-code/skills/requirements-reviewer/SKILL.md +997 -0
- package/src/traceability/extractor.js +294 -0
- package/src/traceability/gap-detector.js +230 -0
- package/src/traceability/index.js +15 -0
- package/src/traceability/matrix-storage.js +368 -0
- package/src/validators/design-reviewer.js +1300 -0
- package/src/validators/requirements-reviewer.js +1019 -0
|
@@ -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
|
+
};
|