musubi-sdd 6.1.2 → 6.2.0
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/package.json +1 -1
- 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/validators/design-reviewer.js +1300 -0
- package/src/validators/requirements-reviewer.js +1019 -0
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Reviewer
|
|
3
|
+
*
|
|
4
|
+
* ATAM、SOLID原則、デザインパターン、結合度・凝集度、
|
|
5
|
+
* エラーハンドリング、セキュリティの観点から設計書をレビュー
|
|
6
|
+
*
|
|
7
|
+
* @module src/validators/design-reviewer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 問題の深刻度
|
|
15
|
+
*/
|
|
16
|
+
const IssueSeverity = {
|
|
17
|
+
CRITICAL: 'critical',
|
|
18
|
+
MAJOR: 'major',
|
|
19
|
+
MINOR: 'minor',
|
|
20
|
+
SUGGESTION: 'suggestion',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 問題のカテゴリ
|
|
25
|
+
*/
|
|
26
|
+
const IssueCategory = {
|
|
27
|
+
ATAM: 'atam',
|
|
28
|
+
SOLID: 'solid',
|
|
29
|
+
PATTERN: 'pattern',
|
|
30
|
+
COUPLING: 'coupling',
|
|
31
|
+
COHESION: 'cohesion',
|
|
32
|
+
ERROR_HANDLING: 'error-handling',
|
|
33
|
+
SECURITY: 'security',
|
|
34
|
+
C4_MODEL: 'c4-model',
|
|
35
|
+
ADR: 'adr',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SOLID原則の種類
|
|
40
|
+
*/
|
|
41
|
+
const SOLIDPrinciple = {
|
|
42
|
+
SRP: 'srp', // Single Responsibility
|
|
43
|
+
OCP: 'ocp', // Open/Closed
|
|
44
|
+
LSP: 'lsp', // Liskov Substitution
|
|
45
|
+
ISP: 'isp', // Interface Segregation
|
|
46
|
+
DIP: 'dip', // Dependency Inversion
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* レビュー観点
|
|
51
|
+
*/
|
|
52
|
+
const ReviewFocus = {
|
|
53
|
+
ATAM: 'atam',
|
|
54
|
+
SOLID: 'solid',
|
|
55
|
+
PATTERNS: 'patterns',
|
|
56
|
+
COUPLING_COHESION: 'coupling-cohesion',
|
|
57
|
+
ERROR_HANDLING: 'error-handling',
|
|
58
|
+
SECURITY: 'security',
|
|
59
|
+
ALL: 'all',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 品質属性
|
|
64
|
+
*/
|
|
65
|
+
const QualityAttribute = {
|
|
66
|
+
PERFORMANCE: 'performance',
|
|
67
|
+
SECURITY: 'security',
|
|
68
|
+
AVAILABILITY: 'availability',
|
|
69
|
+
MODIFIABILITY: 'modifiability',
|
|
70
|
+
TESTABILITY: 'testability',
|
|
71
|
+
SCALABILITY: 'scalability',
|
|
72
|
+
USABILITY: 'usability',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 設計上の問題クラス
|
|
77
|
+
*/
|
|
78
|
+
class DesignIssue {
|
|
79
|
+
constructor(options = {}) {
|
|
80
|
+
this.id = options.id || `DES-${Date.now()}`;
|
|
81
|
+
this.category = options.category || IssueCategory.SOLID;
|
|
82
|
+
this.severity = options.severity || IssueSeverity.MINOR;
|
|
83
|
+
this.principle = options.principle || null; // For SOLID
|
|
84
|
+
this.title = options.title || '';
|
|
85
|
+
this.description = options.description || '';
|
|
86
|
+
this.location = options.location || '';
|
|
87
|
+
this.evidence = options.evidence || '';
|
|
88
|
+
this.recommendation = options.recommendation || '';
|
|
89
|
+
this.status = options.status || 'open';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
toJSON() {
|
|
93
|
+
return {
|
|
94
|
+
id: this.id,
|
|
95
|
+
category: this.category,
|
|
96
|
+
severity: this.severity,
|
|
97
|
+
principle: this.principle,
|
|
98
|
+
title: this.title,
|
|
99
|
+
description: this.description,
|
|
100
|
+
location: this.location,
|
|
101
|
+
evidence: this.evidence,
|
|
102
|
+
recommendation: this.recommendation,
|
|
103
|
+
status: this.status,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* レビュー結果クラス
|
|
110
|
+
*/
|
|
111
|
+
class DesignReviewResult {
|
|
112
|
+
constructor() {
|
|
113
|
+
this.issues = [];
|
|
114
|
+
this.metrics = {
|
|
115
|
+
totalIssues: 0,
|
|
116
|
+
bySeverity: {},
|
|
117
|
+
byCategory: {},
|
|
118
|
+
solidCompliance: {},
|
|
119
|
+
couplingScore: 0,
|
|
120
|
+
cohesionScore: 0,
|
|
121
|
+
securityScore: 0,
|
|
122
|
+
};
|
|
123
|
+
this.qualityGate = {
|
|
124
|
+
passed: false,
|
|
125
|
+
criteria: [],
|
|
126
|
+
};
|
|
127
|
+
this.timestamp = new Date();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
addIssue(issue) {
|
|
131
|
+
this.issues.push(issue);
|
|
132
|
+
this.updateMetrics();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
updateMetrics() {
|
|
136
|
+
this.metrics.totalIssues = this.issues.length;
|
|
137
|
+
|
|
138
|
+
// Severity別カウント
|
|
139
|
+
this.metrics.bySeverity = {};
|
|
140
|
+
Object.values(IssueSeverity).forEach(sev => {
|
|
141
|
+
this.metrics.bySeverity[sev] = this.issues.filter(i => i.severity === sev).length;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Category別カウント
|
|
145
|
+
this.metrics.byCategory = {};
|
|
146
|
+
Object.values(IssueCategory).forEach(cat => {
|
|
147
|
+
this.metrics.byCategory[cat] = this.issues.filter(i => i.category === cat).length;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// SOLID原則別カウント
|
|
151
|
+
this.metrics.solidCompliance = {};
|
|
152
|
+
Object.values(SOLIDPrinciple).forEach(principle => {
|
|
153
|
+
const violations = this.issues.filter(
|
|
154
|
+
i => i.category === IssueCategory.SOLID && i.principle === principle
|
|
155
|
+
).length;
|
|
156
|
+
this.metrics.solidCompliance[principle] = violations === 0;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
evaluateQualityGate(options = {}) {
|
|
161
|
+
const {
|
|
162
|
+
maxCritical = 0,
|
|
163
|
+
maxMajorPercent = 20,
|
|
164
|
+
requireSolidCompliance = true,
|
|
165
|
+
requireSecurityReview = true,
|
|
166
|
+
} = options;
|
|
167
|
+
|
|
168
|
+
const criteria = [];
|
|
169
|
+
|
|
170
|
+
// Critical問題チェック
|
|
171
|
+
const criticalCount = this.metrics.bySeverity[IssueSeverity.CRITICAL] || 0;
|
|
172
|
+
criteria.push({
|
|
173
|
+
name: 'No Critical Issues',
|
|
174
|
+
passed: criticalCount <= maxCritical,
|
|
175
|
+
actual: criticalCount,
|
|
176
|
+
threshold: maxCritical,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Major問題率チェック
|
|
180
|
+
const majorCount = this.metrics.bySeverity[IssueSeverity.MAJOR] || 0;
|
|
181
|
+
const majorPercent =
|
|
182
|
+
this.metrics.totalIssues > 0 ? (majorCount / this.metrics.totalIssues) * 100 : 0;
|
|
183
|
+
criteria.push({
|
|
184
|
+
name: 'Major Issues Under Threshold',
|
|
185
|
+
passed: majorPercent <= maxMajorPercent,
|
|
186
|
+
actual: Math.round(majorPercent),
|
|
187
|
+
threshold: maxMajorPercent,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// SOLID準拠チェック
|
|
191
|
+
if (requireSolidCompliance) {
|
|
192
|
+
const solidViolations = this.issues.filter(i => i.category === IssueCategory.SOLID).length;
|
|
193
|
+
criteria.push({
|
|
194
|
+
name: 'SOLID Principles Compliance',
|
|
195
|
+
passed: solidViolations === 0,
|
|
196
|
+
actual: solidViolations,
|
|
197
|
+
threshold: 0,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// セキュリティレビューチェック
|
|
202
|
+
if (requireSecurityReview) {
|
|
203
|
+
const securityIssues = this.issues.filter(
|
|
204
|
+
i => i.category === IssueCategory.SECURITY && i.severity === IssueSeverity.CRITICAL
|
|
205
|
+
).length;
|
|
206
|
+
criteria.push({
|
|
207
|
+
name: 'No Critical Security Issues',
|
|
208
|
+
passed: securityIssues === 0,
|
|
209
|
+
actual: securityIssues,
|
|
210
|
+
threshold: 0,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.qualityGate.criteria = criteria;
|
|
215
|
+
this.qualityGate.passed = criteria.every(c => c.passed);
|
|
216
|
+
|
|
217
|
+
return this.qualityGate;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
toMarkdown() {
|
|
221
|
+
let md = `# Design Review Report\n\n`;
|
|
222
|
+
md += `**Date**: ${this.timestamp.toISOString().split('T')[0]}\n\n`;
|
|
223
|
+
|
|
224
|
+
// Summary
|
|
225
|
+
md += `## Summary\n\n`;
|
|
226
|
+
md += `| Severity | Count |\n`;
|
|
227
|
+
md += `|----------|-------|\n`;
|
|
228
|
+
Object.entries(this.metrics.bySeverity).forEach(([severity, count]) => {
|
|
229
|
+
const emoji = {
|
|
230
|
+
critical: '🔴',
|
|
231
|
+
major: '🟠',
|
|
232
|
+
minor: '🟡',
|
|
233
|
+
suggestion: '🟢',
|
|
234
|
+
};
|
|
235
|
+
md += `| ${emoji[severity] || ''} ${severity} | ${count} |\n`;
|
|
236
|
+
});
|
|
237
|
+
md += `| **Total** | **${this.metrics.totalIssues}** |\n\n`;
|
|
238
|
+
|
|
239
|
+
// By Category
|
|
240
|
+
md += `## Issues by Category\n\n`;
|
|
241
|
+
md += `| Category | Count |\n`;
|
|
242
|
+
md += `|----------|-------|\n`;
|
|
243
|
+
Object.entries(this.metrics.byCategory).forEach(([category, count]) => {
|
|
244
|
+
if (count > 0) {
|
|
245
|
+
md += `| ${category} | ${count} |\n`;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
md += '\n';
|
|
249
|
+
|
|
250
|
+
// SOLID Compliance
|
|
251
|
+
md += `## SOLID Principles Compliance\n\n`;
|
|
252
|
+
md += `| Principle | Status |\n`;
|
|
253
|
+
md += `|-----------|--------|\n`;
|
|
254
|
+
const principleNames = {
|
|
255
|
+
srp: 'Single Responsibility',
|
|
256
|
+
ocp: 'Open/Closed',
|
|
257
|
+
lsp: 'Liskov Substitution',
|
|
258
|
+
isp: 'Interface Segregation',
|
|
259
|
+
dip: 'Dependency Inversion',
|
|
260
|
+
};
|
|
261
|
+
Object.entries(this.metrics.solidCompliance).forEach(([principle, compliant]) => {
|
|
262
|
+
md += `| ${principleNames[principle] || principle} | ${compliant ? '✅' : '❌'} |\n`;
|
|
263
|
+
});
|
|
264
|
+
md += '\n';
|
|
265
|
+
|
|
266
|
+
// Quality Gate
|
|
267
|
+
md += `## Quality Gate\n\n`;
|
|
268
|
+
md += `**Status**: ${this.qualityGate.passed ? '✅ PASSED' : '❌ FAILED'}\n\n`;
|
|
269
|
+
md += `| Criterion | Status | Actual | Threshold |\n`;
|
|
270
|
+
md += `|-----------|--------|--------|-----------|\n`;
|
|
271
|
+
this.qualityGate.criteria.forEach(c => {
|
|
272
|
+
md += `| ${c.name} | ${c.passed ? '✅' : '❌'} | ${c.actual} | ${c.threshold} |\n`;
|
|
273
|
+
});
|
|
274
|
+
md += '\n';
|
|
275
|
+
|
|
276
|
+
// Detailed Issues
|
|
277
|
+
md += `## Detailed Issues\n\n`;
|
|
278
|
+
this.issues.forEach(issue => {
|
|
279
|
+
md += `### ${issue.id}: ${issue.title}\n\n`;
|
|
280
|
+
md += `- **Category**: ${issue.category}\n`;
|
|
281
|
+
md += `- **Severity**: ${issue.severity}\n`;
|
|
282
|
+
if (issue.principle) {
|
|
283
|
+
md += `- **Principle**: ${issue.principle}\n`;
|
|
284
|
+
}
|
|
285
|
+
if (issue.location) {
|
|
286
|
+
md += `- **Location**: ${issue.location}\n`;
|
|
287
|
+
}
|
|
288
|
+
md += `\n**Description**: ${issue.description}\n\n`;
|
|
289
|
+
if (issue.evidence) {
|
|
290
|
+
md += `**Evidence**: "${issue.evidence}"\n\n`;
|
|
291
|
+
}
|
|
292
|
+
md += `**Recommendation**: ${issue.recommendation}\n\n`;
|
|
293
|
+
md += `---\n\n`;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return md;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
toJSON() {
|
|
300
|
+
return {
|
|
301
|
+
issues: this.issues.map(i => i.toJSON()),
|
|
302
|
+
metrics: this.metrics,
|
|
303
|
+
qualityGate: this.qualityGate,
|
|
304
|
+
timestamp: this.timestamp.toISOString(),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Design Reviewer クラス
|
|
311
|
+
*/
|
|
312
|
+
class DesignReviewer {
|
|
313
|
+
constructor(projectPath = process.cwd()) {
|
|
314
|
+
this.projectPath = projectPath;
|
|
315
|
+
|
|
316
|
+
// SOLID違反を検出するためのパターン
|
|
317
|
+
this.solidViolationPatterns = {
|
|
318
|
+
srp: [
|
|
319
|
+
/class\s+\w*(Manager|Handler|Processor|Service|Controller)\b/gi,
|
|
320
|
+
/class\s+\w*And\w*/gi,
|
|
321
|
+
/\bGod\s*(Class|Object)\b/gi,
|
|
322
|
+
],
|
|
323
|
+
ocp: [
|
|
324
|
+
/switch\s*\([^)]*type[^)]*\)/gi,
|
|
325
|
+
/if\s*\([^)]*instanceof[^)]*\)/gi,
|
|
326
|
+
/switch\s*\([^)]*\.getClass\(\)/gi,
|
|
327
|
+
],
|
|
328
|
+
lsp: [/throw\s+new\s+(NotImplementedException|UnsupportedOperationException)/gi],
|
|
329
|
+
isp: [/interface\s+\w+\s*\{[^}]{500,}\}/gi], // Large interfaces
|
|
330
|
+
dip: [
|
|
331
|
+
/new\s+[A-Z]\w+\s*\([^)]*\)/g, // Direct instantiation pattern
|
|
332
|
+
/import\s+.*\bimpl\b/gi,
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// デザインパターンの検出
|
|
337
|
+
this.patternIndicators = {
|
|
338
|
+
singleton: /\bgetInstance\b|\bINSTANCE\b|\bprivate\s+static/gi,
|
|
339
|
+
factory: /\bFactory\b|\bcreate[A-Z]\w+\(/gi,
|
|
340
|
+
observer: /\bObserver\b|\bListener\b|\bsubscribe\b|\bpublish\b/gi,
|
|
341
|
+
strategy: /\bStrategy\b|\bPolicy\b/gi,
|
|
342
|
+
decorator: /\bDecorator\b|\bWrapper\b/gi,
|
|
343
|
+
adapter: /\bAdapter\b|\bBridge\b/gi,
|
|
344
|
+
facade: /\bFacade\b/gi,
|
|
345
|
+
repository: /\bRepository\b/gi,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// セキュリティ関連キーワード
|
|
349
|
+
this.securityKeywords = {
|
|
350
|
+
authentication: /\b(auth|login|oauth|jwt|token|session|credential|password)\b/gi,
|
|
351
|
+
authorization: /\b(permission|role|access|rbac|abac|acl|policy)\b/gi,
|
|
352
|
+
encryption: /\b(encrypt|decrypt|hash|salt|aes|rsa|ssl|tls|https)\b/gi,
|
|
353
|
+
validation: /\b(validate|sanitize|escape|xss|injection|csrf)\b/gi,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// エラーハンドリングパターン
|
|
357
|
+
this.errorHandlingPatterns = {
|
|
358
|
+
emptyTry: /try\s*\{[^}]*\}\s*catch\s*\([^)]*\)\s*\{\s*\}/gi,
|
|
359
|
+
genericCatch: /catch\s*\(\s*(Exception|Error|Throwable)\s+/gi,
|
|
360
|
+
swallowException: /catch\s*\([^)]*\)\s*\{\s*\/\/\s*(ignore|swallow)/gi,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* ドキュメントを読み込む
|
|
366
|
+
*/
|
|
367
|
+
async loadDocument(documentPath) {
|
|
368
|
+
const fullPath = path.isAbsolute(documentPath)
|
|
369
|
+
? documentPath
|
|
370
|
+
: path.join(this.projectPath, documentPath);
|
|
371
|
+
|
|
372
|
+
if (!fs.existsSync(fullPath)) {
|
|
373
|
+
throw new Error(`Document not found: ${fullPath}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return fs.readFileSync(fullPath, 'utf-8');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* SOLID原則のレビュー
|
|
381
|
+
*/
|
|
382
|
+
reviewSOLID(content, _options = {}) {
|
|
383
|
+
const issues = [];
|
|
384
|
+
let issueCounter = 1;
|
|
385
|
+
|
|
386
|
+
// SRP (Single Responsibility Principle) チェック
|
|
387
|
+
this.solidViolationPatterns.srp.forEach(pattern => {
|
|
388
|
+
const matches = content.match(pattern) || [];
|
|
389
|
+
matches.forEach(match => {
|
|
390
|
+
issues.push(
|
|
391
|
+
new DesignIssue({
|
|
392
|
+
id: `DES-SOLID-${String(issueCounter++).padStart(3, '0')}`,
|
|
393
|
+
category: IssueCategory.SOLID,
|
|
394
|
+
principle: SOLIDPrinciple.SRP,
|
|
395
|
+
severity: IssueSeverity.MAJOR,
|
|
396
|
+
title: 'Potential SRP Violation',
|
|
397
|
+
description: `Class naming suggests multiple responsibilities: "${match}"`,
|
|
398
|
+
evidence: match,
|
|
399
|
+
recommendation:
|
|
400
|
+
'Consider splitting into smaller, focused classes. Each class should have only one reason to change.',
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// OCP (Open/Closed Principle) チェック
|
|
407
|
+
this.solidViolationPatterns.ocp.forEach(pattern => {
|
|
408
|
+
const matches = content.match(pattern) || [];
|
|
409
|
+
matches.forEach(match => {
|
|
410
|
+
issues.push(
|
|
411
|
+
new DesignIssue({
|
|
412
|
+
id: `DES-SOLID-${String(issueCounter++).padStart(3, '0')}`,
|
|
413
|
+
category: IssueCategory.SOLID,
|
|
414
|
+
principle: SOLIDPrinciple.OCP,
|
|
415
|
+
severity: IssueSeverity.MAJOR,
|
|
416
|
+
title: 'Potential OCP Violation',
|
|
417
|
+
description: 'Type-based switching suggests need for polymorphism',
|
|
418
|
+
evidence: match,
|
|
419
|
+
recommendation:
|
|
420
|
+
'Use Strategy pattern or polymorphism instead of switch/if-else on types.',
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// LSP (Liskov Substitution Principle) チェック
|
|
427
|
+
this.solidViolationPatterns.lsp.forEach(pattern => {
|
|
428
|
+
const matches = content.match(pattern) || [];
|
|
429
|
+
matches.forEach(match => {
|
|
430
|
+
issues.push(
|
|
431
|
+
new DesignIssue({
|
|
432
|
+
id: `DES-SOLID-${String(issueCounter++).padStart(3, '0')}`,
|
|
433
|
+
category: IssueCategory.SOLID,
|
|
434
|
+
principle: SOLIDPrinciple.LSP,
|
|
435
|
+
severity: IssueSeverity.MAJOR,
|
|
436
|
+
title: 'Potential LSP Violation',
|
|
437
|
+
description: 'NotImplementedException suggests subtype cannot substitute base type',
|
|
438
|
+
evidence: match,
|
|
439
|
+
recommendation:
|
|
440
|
+
'Ensure subclasses can fully substitute their parent classes. Consider redesigning the inheritance hierarchy.',
|
|
441
|
+
})
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ISP (Interface Segregation Principle) チェック
|
|
447
|
+
if (/interface\s+\w+\s*\{/gi.test(content)) {
|
|
448
|
+
// Check for "fat" interfaces mentioned in design
|
|
449
|
+
if (/fat\s+interface|large\s+interface|많은\s+메서드/gi.test(content)) {
|
|
450
|
+
issues.push(
|
|
451
|
+
new DesignIssue({
|
|
452
|
+
id: `DES-SOLID-${String(issueCounter++).padStart(3, '0')}`,
|
|
453
|
+
category: IssueCategory.SOLID,
|
|
454
|
+
principle: SOLIDPrinciple.ISP,
|
|
455
|
+
severity: IssueSeverity.MINOR,
|
|
456
|
+
title: 'Potential ISP Concern',
|
|
457
|
+
description: 'Document mentions large interfaces',
|
|
458
|
+
recommendation:
|
|
459
|
+
'Split large interfaces into smaller, role-specific interfaces (e.g., IReadable, IWritable).',
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// DIP (Dependency Inversion Principle) チェック
|
|
466
|
+
if (/directly\s+depend|concrete\s+class|tight\s+coupling|直接依存/gi.test(content)) {
|
|
467
|
+
issues.push(
|
|
468
|
+
new DesignIssue({
|
|
469
|
+
id: `DES-SOLID-${String(issueCounter++).padStart(3, '0')}`,
|
|
470
|
+
category: IssueCategory.SOLID,
|
|
471
|
+
principle: SOLIDPrinciple.DIP,
|
|
472
|
+
severity: IssueSeverity.MAJOR,
|
|
473
|
+
title: 'Potential DIP Violation',
|
|
474
|
+
description: 'Design mentions direct dependencies on concrete classes',
|
|
475
|
+
recommendation:
|
|
476
|
+
'Depend on abstractions (interfaces) rather than concrete implementations. Use dependency injection.',
|
|
477
|
+
})
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return issues;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* デザインパターンのレビュー
|
|
486
|
+
*/
|
|
487
|
+
reviewPatterns(content, _options = {}) {
|
|
488
|
+
const issues = [];
|
|
489
|
+
let issueCounter = 1;
|
|
490
|
+
const detectedPatterns = [];
|
|
491
|
+
|
|
492
|
+
// パターン検出
|
|
493
|
+
Object.entries(this.patternIndicators).forEach(([pattern, regex]) => {
|
|
494
|
+
if (regex.test(content)) {
|
|
495
|
+
detectedPatterns.push(pattern);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Singleton乱用チェック
|
|
500
|
+
if (detectedPatterns.includes('singleton')) {
|
|
501
|
+
const singletonCount = (content.match(/Singleton|getInstance|INSTANCE/gi) || []).length;
|
|
502
|
+
if (singletonCount > 3) {
|
|
503
|
+
issues.push(
|
|
504
|
+
new DesignIssue({
|
|
505
|
+
id: `DES-PAT-${String(issueCounter++).padStart(3, '0')}`,
|
|
506
|
+
category: IssueCategory.PATTERN,
|
|
507
|
+
severity: IssueSeverity.MAJOR,
|
|
508
|
+
title: 'Excessive Singleton Usage',
|
|
509
|
+
description: `Multiple Singleton patterns detected (${singletonCount} occurrences). May indicate global state abuse.`,
|
|
510
|
+
recommendation:
|
|
511
|
+
'Consider using Dependency Injection instead of Singletons for better testability.',
|
|
512
|
+
})
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 必要なパターンの欠如チェック
|
|
518
|
+
if (
|
|
519
|
+
/複雑.*生成|complex.*creation|オブジェクト.*生成/gi.test(content) &&
|
|
520
|
+
!detectedPatterns.includes('factory')
|
|
521
|
+
) {
|
|
522
|
+
issues.push(
|
|
523
|
+
new DesignIssue({
|
|
524
|
+
id: `DES-PAT-${String(issueCounter++).padStart(3, '0')}`,
|
|
525
|
+
category: IssueCategory.PATTERN,
|
|
526
|
+
severity: IssueSeverity.MINOR,
|
|
527
|
+
title: 'Missing Factory Pattern',
|
|
528
|
+
description: 'Complex object creation mentioned but no Factory pattern detected',
|
|
529
|
+
recommendation: 'Consider using Factory pattern to encapsulate complex object creation.',
|
|
530
|
+
})
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// イベント処理があるがObserverパターンがない
|
|
535
|
+
if (
|
|
536
|
+
/event|イベント|notification|通知/gi.test(content) &&
|
|
537
|
+
!detectedPatterns.includes('observer')
|
|
538
|
+
) {
|
|
539
|
+
issues.push(
|
|
540
|
+
new DesignIssue({
|
|
541
|
+
id: `DES-PAT-${String(issueCounter++).padStart(3, '0')}`,
|
|
542
|
+
category: IssueCategory.PATTERN,
|
|
543
|
+
severity: IssueSeverity.SUGGESTION,
|
|
544
|
+
title: 'Consider Observer Pattern',
|
|
545
|
+
description: 'Event handling mentioned. Observer pattern might be beneficial.',
|
|
546
|
+
recommendation: 'Consider Observer/Pub-Sub pattern for decoupled event notification.',
|
|
547
|
+
})
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return issues;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* 結合度・凝集度のレビュー
|
|
556
|
+
*/
|
|
557
|
+
reviewCouplingCohesion(content, _options = {}) {
|
|
558
|
+
const issues = [];
|
|
559
|
+
let issueCounter = 1;
|
|
560
|
+
|
|
561
|
+
// 高結合の兆候
|
|
562
|
+
if (/tight\s*coupling|密結合|強結合|直接.*依存/gi.test(content)) {
|
|
563
|
+
issues.push(
|
|
564
|
+
new DesignIssue({
|
|
565
|
+
id: `DES-CC-${String(issueCounter++).padStart(3, '0')}`,
|
|
566
|
+
category: IssueCategory.COUPLING,
|
|
567
|
+
severity: IssueSeverity.MAJOR,
|
|
568
|
+
title: 'High Coupling Detected',
|
|
569
|
+
description: 'Design mentions tight coupling between components',
|
|
570
|
+
recommendation: 'Reduce coupling through interfaces, events, or dependency injection.',
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// グローバル状態の使用
|
|
576
|
+
if (/global\s*(state|variable)|グローバル.*変数|共有.*状態/gi.test(content)) {
|
|
577
|
+
issues.push(
|
|
578
|
+
new DesignIssue({
|
|
579
|
+
id: `DES-CC-${String(issueCounter++).padStart(3, '0')}`,
|
|
580
|
+
category: IssueCategory.COUPLING,
|
|
581
|
+
severity: IssueSeverity.MAJOR,
|
|
582
|
+
title: 'Global State Usage',
|
|
583
|
+
description: 'Design mentions global state which creates implicit coupling',
|
|
584
|
+
recommendation: 'Avoid global state. Use explicit dependency passing instead.',
|
|
585
|
+
})
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// 低凝集の兆候
|
|
590
|
+
if (/utility\s*class|ヘルパー.*クラス|misc|その他/gi.test(content)) {
|
|
591
|
+
issues.push(
|
|
592
|
+
new DesignIssue({
|
|
593
|
+
id: `DES-CC-${String(issueCounter++).padStart(3, '0')}`,
|
|
594
|
+
category: IssueCategory.COHESION,
|
|
595
|
+
severity: IssueSeverity.MINOR,
|
|
596
|
+
title: 'Low Cohesion Indicator',
|
|
597
|
+
description: 'Utility/Helper classes often indicate low cohesion',
|
|
598
|
+
recommendation:
|
|
599
|
+
'Move utility methods to the classes that use them, or create domain-specific classes.',
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 循環依存
|
|
605
|
+
if (/circular\s*dependency|循環.*依存|相互.*依存/gi.test(content)) {
|
|
606
|
+
issues.push(
|
|
607
|
+
new DesignIssue({
|
|
608
|
+
id: `DES-CC-${String(issueCounter++).padStart(3, '0')}`,
|
|
609
|
+
category: IssueCategory.COUPLING,
|
|
610
|
+
severity: IssueSeverity.CRITICAL,
|
|
611
|
+
title: 'Circular Dependency',
|
|
612
|
+
description: 'Circular dependency mentioned in design',
|
|
613
|
+
recommendation:
|
|
614
|
+
'Break circular dependencies using interfaces, events, or extracting common code.',
|
|
615
|
+
})
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return issues;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* エラーハンドリングのレビュー
|
|
624
|
+
*/
|
|
625
|
+
reviewErrorHandling(content, _options = {}) {
|
|
626
|
+
const issues = [];
|
|
627
|
+
let issueCounter = 1;
|
|
628
|
+
|
|
629
|
+
// エラーハンドリング戦略の有無
|
|
630
|
+
if (!/error\s*handling|エラー.*ハンドリング|例外.*処理|exception/gi.test(content)) {
|
|
631
|
+
issues.push(
|
|
632
|
+
new DesignIssue({
|
|
633
|
+
id: `DES-ERR-${String(issueCounter++).padStart(3, '0')}`,
|
|
634
|
+
category: IssueCategory.ERROR_HANDLING,
|
|
635
|
+
severity: IssueSeverity.MAJOR,
|
|
636
|
+
title: 'Missing Error Handling Strategy',
|
|
637
|
+
description: 'No error handling strategy documented',
|
|
638
|
+
recommendation:
|
|
639
|
+
'Define error handling strategy including exception hierarchy, retry policies, and graceful degradation.',
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// リトライ戦略
|
|
645
|
+
if (
|
|
646
|
+
/network|API|外部.*サービス|external.*service/gi.test(content) &&
|
|
647
|
+
!/retry|リトライ|再試行|backoff/gi.test(content)
|
|
648
|
+
) {
|
|
649
|
+
issues.push(
|
|
650
|
+
new DesignIssue({
|
|
651
|
+
id: `DES-ERR-${String(issueCounter++).padStart(3, '0')}`,
|
|
652
|
+
category: IssueCategory.ERROR_HANDLING,
|
|
653
|
+
severity: IssueSeverity.MINOR,
|
|
654
|
+
title: 'Missing Retry Strategy',
|
|
655
|
+
description: 'External service integration without retry strategy',
|
|
656
|
+
recommendation: 'Add retry with exponential backoff for external service calls.',
|
|
657
|
+
})
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// サーキットブレーカー
|
|
662
|
+
if (
|
|
663
|
+
/microservice|マイクロサービス|distributed/gi.test(content) &&
|
|
664
|
+
!/circuit\s*breaker|サーキット.*ブレーカー/gi.test(content)
|
|
665
|
+
) {
|
|
666
|
+
issues.push(
|
|
667
|
+
new DesignIssue({
|
|
668
|
+
id: `DES-ERR-${String(issueCounter++).padStart(3, '0')}`,
|
|
669
|
+
category: IssueCategory.ERROR_HANDLING,
|
|
670
|
+
severity: IssueSeverity.MINOR,
|
|
671
|
+
title: 'Consider Circuit Breaker',
|
|
672
|
+
description: 'Distributed system without circuit breaker pattern',
|
|
673
|
+
recommendation: 'Implement circuit breaker pattern to prevent cascade failures.',
|
|
674
|
+
})
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// グレースフルデグラデーション
|
|
679
|
+
if (!/graceful\s*degradation|縮退運転|フォールバック|fallback/gi.test(content)) {
|
|
680
|
+
issues.push(
|
|
681
|
+
new DesignIssue({
|
|
682
|
+
id: `DES-ERR-${String(issueCounter++).padStart(3, '0')}`,
|
|
683
|
+
category: IssueCategory.ERROR_HANDLING,
|
|
684
|
+
severity: IssueSeverity.SUGGESTION,
|
|
685
|
+
title: 'Consider Graceful Degradation',
|
|
686
|
+
description: 'No graceful degradation strategy documented',
|
|
687
|
+
recommendation: 'Define fallback behaviors for when components fail.',
|
|
688
|
+
})
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return issues;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* セキュリティのレビュー
|
|
697
|
+
*/
|
|
698
|
+
reviewSecurity(content, _options = {}) {
|
|
699
|
+
const issues = [];
|
|
700
|
+
let issueCounter = 1;
|
|
701
|
+
|
|
702
|
+
// 認証
|
|
703
|
+
if (
|
|
704
|
+
/user|ユーザー|account|アカウント/gi.test(content) &&
|
|
705
|
+
!this.securityKeywords.authentication.test(content)
|
|
706
|
+
) {
|
|
707
|
+
issues.push(
|
|
708
|
+
new DesignIssue({
|
|
709
|
+
id: `DES-SEC-${String(issueCounter++).padStart(3, '0')}`,
|
|
710
|
+
category: IssueCategory.SECURITY,
|
|
711
|
+
severity: IssueSeverity.CRITICAL,
|
|
712
|
+
title: 'Missing Authentication Design',
|
|
713
|
+
description: 'User-facing system without authentication strategy documented',
|
|
714
|
+
recommendation:
|
|
715
|
+
'Define authentication method (OAuth, JWT, etc.), password policy, and MFA requirements.',
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// 認可
|
|
721
|
+
if (
|
|
722
|
+
/role|権限|permission|管理者|admin/gi.test(content) &&
|
|
723
|
+
!/authorization|認可|access\s*control|アクセス制御/gi.test(content)
|
|
724
|
+
) {
|
|
725
|
+
issues.push(
|
|
726
|
+
new DesignIssue({
|
|
727
|
+
id: `DES-SEC-${String(issueCounter++).padStart(3, '0')}`,
|
|
728
|
+
category: IssueCategory.SECURITY,
|
|
729
|
+
severity: IssueSeverity.MAJOR,
|
|
730
|
+
title: 'Missing Authorization Design',
|
|
731
|
+
description: 'Role-based features without authorization strategy',
|
|
732
|
+
recommendation:
|
|
733
|
+
'Define RBAC/ABAC model, permission hierarchy, and access control enforcement points.',
|
|
734
|
+
})
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// データ保護
|
|
739
|
+
if (
|
|
740
|
+
/personal|個人|sensitive|機密|PII|password/gi.test(content) &&
|
|
741
|
+
!this.securityKeywords.encryption.test(content)
|
|
742
|
+
) {
|
|
743
|
+
issues.push(
|
|
744
|
+
new DesignIssue({
|
|
745
|
+
id: `DES-SEC-${String(issueCounter++).padStart(3, '0')}`,
|
|
746
|
+
category: IssueCategory.SECURITY,
|
|
747
|
+
severity: IssueSeverity.CRITICAL,
|
|
748
|
+
title: 'Missing Data Protection Design',
|
|
749
|
+
description: 'Sensitive data handling without encryption strategy',
|
|
750
|
+
recommendation:
|
|
751
|
+
'Define encryption at rest/transit, key management, and data classification.',
|
|
752
|
+
})
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// 入力検証
|
|
757
|
+
if (
|
|
758
|
+
/input|入力|form|フォーム|API/gi.test(content) &&
|
|
759
|
+
!this.securityKeywords.validation.test(content)
|
|
760
|
+
) {
|
|
761
|
+
issues.push(
|
|
762
|
+
new DesignIssue({
|
|
763
|
+
id: `DES-SEC-${String(issueCounter++).padStart(3, '0')}`,
|
|
764
|
+
category: IssueCategory.SECURITY,
|
|
765
|
+
severity: IssueSeverity.MAJOR,
|
|
766
|
+
title: 'Missing Input Validation Design',
|
|
767
|
+
description: 'User input without validation/sanitization strategy',
|
|
768
|
+
recommendation:
|
|
769
|
+
'Define input validation rules, sanitization methods, and output encoding.',
|
|
770
|
+
})
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// 監査ログ
|
|
775
|
+
if (!/audit|監査|logging|ログ/gi.test(content)) {
|
|
776
|
+
issues.push(
|
|
777
|
+
new DesignIssue({
|
|
778
|
+
id: `DES-SEC-${String(issueCounter++).padStart(3, '0')}`,
|
|
779
|
+
category: IssueCategory.SECURITY,
|
|
780
|
+
severity: IssueSeverity.MINOR,
|
|
781
|
+
title: 'Missing Audit Logging Design',
|
|
782
|
+
description: 'No audit logging strategy documented',
|
|
783
|
+
recommendation:
|
|
784
|
+
'Define security-relevant events to log, log retention policy, and log protection.',
|
|
785
|
+
})
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return issues;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* C4モデルのレビュー
|
|
794
|
+
*/
|
|
795
|
+
reviewC4Model(content, _options = {}) {
|
|
796
|
+
const issues = [];
|
|
797
|
+
let issueCounter = 1;
|
|
798
|
+
|
|
799
|
+
// C4ダイアグラムの有無
|
|
800
|
+
const hasContext = /context\s*diagram|コンテキスト.*図|システム.*境界/gi.test(content);
|
|
801
|
+
const hasContainer = /container\s*diagram|コンテナ.*図|アプリケーション.*構成/gi.test(content);
|
|
802
|
+
const hasComponent = /component\s*diagram|コンポーネント.*図/gi.test(content);
|
|
803
|
+
|
|
804
|
+
if (!hasContext) {
|
|
805
|
+
issues.push(
|
|
806
|
+
new DesignIssue({
|
|
807
|
+
id: `DES-C4-${String(issueCounter++).padStart(3, '0')}`,
|
|
808
|
+
category: IssueCategory.C4_MODEL,
|
|
809
|
+
severity: IssueSeverity.MAJOR,
|
|
810
|
+
title: 'Missing Context Diagram',
|
|
811
|
+
description: 'C4 Context diagram not found',
|
|
812
|
+
recommendation:
|
|
813
|
+
'Add Context diagram showing system boundary, actors, and external systems.',
|
|
814
|
+
})
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (!hasContainer) {
|
|
819
|
+
issues.push(
|
|
820
|
+
new DesignIssue({
|
|
821
|
+
id: `DES-C4-${String(issueCounter++).padStart(3, '0')}`,
|
|
822
|
+
category: IssueCategory.C4_MODEL,
|
|
823
|
+
severity: IssueSeverity.MAJOR,
|
|
824
|
+
title: 'Missing Container Diagram',
|
|
825
|
+
description: 'C4 Container diagram not found',
|
|
826
|
+
recommendation:
|
|
827
|
+
'Add Container diagram showing applications, databases, and their interactions.',
|
|
828
|
+
})
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (!hasComponent) {
|
|
833
|
+
issues.push(
|
|
834
|
+
new DesignIssue({
|
|
835
|
+
id: `DES-C4-${String(issueCounter++).padStart(3, '0')}`,
|
|
836
|
+
category: IssueCategory.C4_MODEL,
|
|
837
|
+
severity: IssueSeverity.MINOR,
|
|
838
|
+
title: 'Missing Component Diagram',
|
|
839
|
+
description: 'C4 Component diagram not found',
|
|
840
|
+
recommendation: 'Add Component diagram for key containers showing internal structure.',
|
|
841
|
+
})
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return issues;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* ADRのレビュー
|
|
850
|
+
*/
|
|
851
|
+
reviewADR(content, _options = {}) {
|
|
852
|
+
const issues = [];
|
|
853
|
+
let issueCounter = 1;
|
|
854
|
+
|
|
855
|
+
// ADRの基本構造チェック
|
|
856
|
+
const hasStatus = /status:\s*(proposed|accepted|deprecated|superseded)/gi.test(content);
|
|
857
|
+
const hasContext = /##\s*context|##\s*背景|##\s*コンテキスト/gi.test(content);
|
|
858
|
+
const hasDecision = /##\s*decision|##\s*決定/gi.test(content);
|
|
859
|
+
const hasConsequences = /##\s*consequences|##\s*結果|##\s*影響/gi.test(content);
|
|
860
|
+
const hasAlternatives = /##\s*alternatives|##\s*代替案|options\s*considered/gi.test(content);
|
|
861
|
+
|
|
862
|
+
if (!hasStatus) {
|
|
863
|
+
issues.push(
|
|
864
|
+
new DesignIssue({
|
|
865
|
+
id: `DES-ADR-${String(issueCounter++).padStart(3, '0')}`,
|
|
866
|
+
category: IssueCategory.ADR,
|
|
867
|
+
severity: IssueSeverity.MINOR,
|
|
868
|
+
title: 'Missing ADR Status',
|
|
869
|
+
description: 'ADR status not specified',
|
|
870
|
+
recommendation: 'Add status: proposed/accepted/deprecated/superseded.',
|
|
871
|
+
})
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (!hasContext) {
|
|
876
|
+
issues.push(
|
|
877
|
+
new DesignIssue({
|
|
878
|
+
id: `DES-ADR-${String(issueCounter++).padStart(3, '0')}`,
|
|
879
|
+
category: IssueCategory.ADR,
|
|
880
|
+
severity: IssueSeverity.MAJOR,
|
|
881
|
+
title: 'Missing ADR Context',
|
|
882
|
+
description: 'ADR context/background not documented',
|
|
883
|
+
recommendation: 'Add Context section explaining the problem/situation.',
|
|
884
|
+
})
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (!hasDecision) {
|
|
889
|
+
issues.push(
|
|
890
|
+
new DesignIssue({
|
|
891
|
+
id: `DES-ADR-${String(issueCounter++).padStart(3, '0')}`,
|
|
892
|
+
category: IssueCategory.ADR,
|
|
893
|
+
severity: IssueSeverity.CRITICAL,
|
|
894
|
+
title: 'Missing ADR Decision',
|
|
895
|
+
description: 'ADR decision not clearly stated',
|
|
896
|
+
recommendation: 'Add Decision section clearly stating what was decided.',
|
|
897
|
+
})
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (!hasConsequences) {
|
|
902
|
+
issues.push(
|
|
903
|
+
new DesignIssue({
|
|
904
|
+
id: `DES-ADR-${String(issueCounter++).padStart(3, '0')}`,
|
|
905
|
+
category: IssueCategory.ADR,
|
|
906
|
+
severity: IssueSeverity.MAJOR,
|
|
907
|
+
title: 'Missing ADR Consequences',
|
|
908
|
+
description: 'ADR consequences not documented',
|
|
909
|
+
recommendation: 'Add Consequences section with both positive and negative impacts.',
|
|
910
|
+
})
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (!hasAlternatives) {
|
|
915
|
+
issues.push(
|
|
916
|
+
new DesignIssue({
|
|
917
|
+
id: `DES-ADR-${String(issueCounter++).padStart(3, '0')}`,
|
|
918
|
+
category: IssueCategory.ADR,
|
|
919
|
+
severity: IssueSeverity.MINOR,
|
|
920
|
+
title: 'Missing ADR Alternatives',
|
|
921
|
+
description: 'ADR alternatives considered not documented',
|
|
922
|
+
recommendation: 'Add Alternatives section showing other options that were considered.',
|
|
923
|
+
})
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return issues;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* 総合レビュー
|
|
932
|
+
*/
|
|
933
|
+
async review(documentPath, options = {}) {
|
|
934
|
+
const content = await this.loadDocument(documentPath);
|
|
935
|
+
const focus = options.focus || [ReviewFocus.ALL];
|
|
936
|
+
const isAllFocus = focus.includes(ReviewFocus.ALL);
|
|
937
|
+
|
|
938
|
+
const result = new DesignReviewResult();
|
|
939
|
+
|
|
940
|
+
// 各観点でレビュー
|
|
941
|
+
if (isAllFocus || focus.includes(ReviewFocus.SOLID)) {
|
|
942
|
+
const solidIssues = this.reviewSOLID(content, options);
|
|
943
|
+
solidIssues.forEach(issue => result.addIssue(issue));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (isAllFocus || focus.includes(ReviewFocus.PATTERNS)) {
|
|
947
|
+
const patternIssues = this.reviewPatterns(content, options);
|
|
948
|
+
patternIssues.forEach(issue => result.addIssue(issue));
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (isAllFocus || focus.includes(ReviewFocus.COUPLING_COHESION)) {
|
|
952
|
+
const ccIssues = this.reviewCouplingCohesion(content, options);
|
|
953
|
+
ccIssues.forEach(issue => result.addIssue(issue));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (isAllFocus || focus.includes(ReviewFocus.ERROR_HANDLING)) {
|
|
957
|
+
const errorIssues = this.reviewErrorHandling(content, options);
|
|
958
|
+
errorIssues.forEach(issue => result.addIssue(issue));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (isAllFocus || focus.includes(ReviewFocus.SECURITY)) {
|
|
962
|
+
const securityIssues = this.reviewSecurity(content, options);
|
|
963
|
+
securityIssues.forEach(issue => result.addIssue(issue));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// C4とADRは特定のドキュメントタイプの場合のみ
|
|
967
|
+
if (options.checkC4 || /c4|architecture|アーキテクチャ/gi.test(content)) {
|
|
968
|
+
const c4Issues = this.reviewC4Model(content, options);
|
|
969
|
+
c4Issues.forEach(issue => result.addIssue(issue));
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (options.checkADR || /ADR|decision\s*record|意思決定/gi.test(content)) {
|
|
973
|
+
const adrIssues = this.reviewADR(content, options);
|
|
974
|
+
adrIssues.forEach(issue => result.addIssue(issue));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
result.evaluateQualityGate(options.qualityGateOptions);
|
|
978
|
+
|
|
979
|
+
return result;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* レビュー結果に基づいてドキュメントを修正
|
|
984
|
+
* @param {string} documentPath - 修正対象のドキュメントパス
|
|
985
|
+
* @param {Array} corrections - 修正指示の配列
|
|
986
|
+
* @param {Object} options - オプション
|
|
987
|
+
* @returns {Object} 修正結果
|
|
988
|
+
*/
|
|
989
|
+
async applyCorrections(documentPath, corrections, options = {}) {
|
|
990
|
+
const fullPath = path.isAbsolute(documentPath)
|
|
991
|
+
? documentPath
|
|
992
|
+
: path.join(this.projectPath, documentPath);
|
|
993
|
+
|
|
994
|
+
if (!fs.existsSync(fullPath)) {
|
|
995
|
+
throw new Error(`Document not found: ${fullPath}`);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// バックアップ作成
|
|
999
|
+
if (options.createBackup !== false) {
|
|
1000
|
+
const backupPath = `${fullPath}.backup`;
|
|
1001
|
+
fs.copyFileSync(fullPath, backupPath);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
let content = fs.readFileSync(fullPath, 'utf-8');
|
|
1005
|
+
const appliedChanges = [];
|
|
1006
|
+
const rejectedFindings = [];
|
|
1007
|
+
const adrsCreated = [];
|
|
1008
|
+
|
|
1009
|
+
for (const correction of corrections) {
|
|
1010
|
+
const { issueId, action, newDesign, reason } = correction;
|
|
1011
|
+
|
|
1012
|
+
switch (action) {
|
|
1013
|
+
case 'accept': {
|
|
1014
|
+
// 推奨を適用
|
|
1015
|
+
const issue = this._findIssueInContent(content, issueId);
|
|
1016
|
+
if (issue && issue.evidence && issue.recommendation) {
|
|
1017
|
+
content = content.replace(issue.evidence, issue.recommendation);
|
|
1018
|
+
appliedChanges.push({
|
|
1019
|
+
issueId,
|
|
1020
|
+
action: 'accepted',
|
|
1021
|
+
category: issue.category,
|
|
1022
|
+
original: issue.evidence,
|
|
1023
|
+
corrected: issue.recommendation,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
case 'modify': {
|
|
1030
|
+
// カスタム修正を適用
|
|
1031
|
+
const modifyIssue = this._findIssueInContent(content, issueId);
|
|
1032
|
+
if (modifyIssue && modifyIssue.evidence && newDesign) {
|
|
1033
|
+
content = content.replace(modifyIssue.evidence, newDesign);
|
|
1034
|
+
appliedChanges.push({
|
|
1035
|
+
issueId,
|
|
1036
|
+
action: 'modified',
|
|
1037
|
+
category: modifyIssue.category,
|
|
1038
|
+
original: modifyIssue.evidence,
|
|
1039
|
+
corrected: newDesign,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
case 'reject':
|
|
1046
|
+
rejectedFindings.push({
|
|
1047
|
+
issueId,
|
|
1048
|
+
reason: reason || 'No reason provided',
|
|
1049
|
+
});
|
|
1050
|
+
break;
|
|
1051
|
+
|
|
1052
|
+
case 'reject-with-adr':
|
|
1053
|
+
// ADRを作成して却下
|
|
1054
|
+
rejectedFindings.push({
|
|
1055
|
+
issueId,
|
|
1056
|
+
reason: reason || 'See ADR',
|
|
1057
|
+
hasADR: true,
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
if (options.generateADRs !== false) {
|
|
1061
|
+
const adr = this._generateADR(issueId, reason, options.adrPath);
|
|
1062
|
+
adrsCreated.push(adr);
|
|
1063
|
+
}
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// 変更履歴を追加
|
|
1069
|
+
const changeHistoryEntry = this._generateChangeHistoryEntry(appliedChanges);
|
|
1070
|
+
if (changeHistoryEntry && !content.includes('## Change History')) {
|
|
1071
|
+
content += `\n\n## Change History\n\n${changeHistoryEntry}`;
|
|
1072
|
+
} else if (changeHistoryEntry) {
|
|
1073
|
+
content = content.replace(
|
|
1074
|
+
/## Change History\n/,
|
|
1075
|
+
`## Change History\n\n${changeHistoryEntry}`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ファイルを保存
|
|
1080
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
1081
|
+
|
|
1082
|
+
// 日本語版も更新
|
|
1083
|
+
if (options.updateJapanese !== false) {
|
|
1084
|
+
const jaPath = fullPath.replace(/\.md$/, '.ja.md');
|
|
1085
|
+
if (fs.existsSync(jaPath)) {
|
|
1086
|
+
let jaContent = fs.readFileSync(jaPath, 'utf-8');
|
|
1087
|
+
for (const change of appliedChanges) {
|
|
1088
|
+
if (jaContent.includes(change.original)) {
|
|
1089
|
+
jaContent = jaContent.replace(change.original, change.corrected);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
fs.writeFileSync(jaPath, jaContent, 'utf-8');
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// 再レビューして品質ゲートを更新
|
|
1097
|
+
const updatedResult = await this.review(documentPath, options.reviewOptions || {});
|
|
1098
|
+
|
|
1099
|
+
return {
|
|
1100
|
+
success: true,
|
|
1101
|
+
changesApplied: appliedChanges,
|
|
1102
|
+
rejectedFindings,
|
|
1103
|
+
adrsCreated,
|
|
1104
|
+
updatedQualityGate: updatedResult.qualityGate,
|
|
1105
|
+
updatedMetrics: updatedResult.metrics,
|
|
1106
|
+
updatedSolidCompliance: updatedResult.metrics.solidCompliance,
|
|
1107
|
+
filesModified: [
|
|
1108
|
+
fullPath,
|
|
1109
|
+
options.createBackup !== false ? `${fullPath}.backup` : null,
|
|
1110
|
+
options.updateJapanese !== false && fs.existsSync(fullPath.replace(/\.md$/, '.ja.md'))
|
|
1111
|
+
? fullPath.replace(/\.md$/, '.ja.md')
|
|
1112
|
+
: null,
|
|
1113
|
+
...adrsCreated.map(adr => adr.path),
|
|
1114
|
+
].filter(Boolean),
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* 修正レポートを生成
|
|
1120
|
+
*/
|
|
1121
|
+
generateCorrectionReport(correctionResult) {
|
|
1122
|
+
const {
|
|
1123
|
+
changesApplied,
|
|
1124
|
+
rejectedFindings,
|
|
1125
|
+
adrsCreated,
|
|
1126
|
+
updatedQualityGate,
|
|
1127
|
+
updatedSolidCompliance,
|
|
1128
|
+
filesModified,
|
|
1129
|
+
} = correctionResult;
|
|
1130
|
+
|
|
1131
|
+
let report = `## 📝 Design Correction Report\n\n`;
|
|
1132
|
+
report += `**Correction Date**: ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
1133
|
+
|
|
1134
|
+
// Changes Applied
|
|
1135
|
+
report += `### Changes Applied\n\n`;
|
|
1136
|
+
if (changesApplied.length > 0) {
|
|
1137
|
+
report += `| Issue ID | Category | Action | Summary |\n`;
|
|
1138
|
+
report += `|----------|----------|--------|----------|\n`;
|
|
1139
|
+
changesApplied.forEach(change => {
|
|
1140
|
+
const summary = change.corrected.substring(0, 40) + '...';
|
|
1141
|
+
report += `| ${change.issueId} | ${change.category} | ${change.action} | ${summary} |\n`;
|
|
1142
|
+
});
|
|
1143
|
+
} else {
|
|
1144
|
+
report += `No changes applied.\n`;
|
|
1145
|
+
}
|
|
1146
|
+
report += `\n`;
|
|
1147
|
+
|
|
1148
|
+
// ADRs Created
|
|
1149
|
+
if (adrsCreated.length > 0) {
|
|
1150
|
+
report += `### ADRs Created\n\n`;
|
|
1151
|
+
report += `| ADR ID | Issue | Decision |\n`;
|
|
1152
|
+
report += `|--------|-------|----------|\n`;
|
|
1153
|
+
adrsCreated.forEach(adr => {
|
|
1154
|
+
report += `| ${adr.id} | ${adr.issueId} | ${adr.decision} |\n`;
|
|
1155
|
+
});
|
|
1156
|
+
report += `\n`;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Rejected Findings
|
|
1160
|
+
report += `### Rejected Findings\n\n`;
|
|
1161
|
+
if (rejectedFindings.length > 0) {
|
|
1162
|
+
report += `| Issue ID | Justification | ADR |\n`;
|
|
1163
|
+
report += `|----------|---------------|-----|\n`;
|
|
1164
|
+
rejectedFindings.forEach(finding => {
|
|
1165
|
+
const hasADR = finding.hasADR ? '✅' : '-';
|
|
1166
|
+
report += `| ${finding.issueId} | ${finding.reason} | ${hasADR} |\n`;
|
|
1167
|
+
});
|
|
1168
|
+
} else {
|
|
1169
|
+
report += `No findings rejected.\n`;
|
|
1170
|
+
}
|
|
1171
|
+
report += `\n`;
|
|
1172
|
+
|
|
1173
|
+
// SOLID Compliance
|
|
1174
|
+
if (updatedSolidCompliance) {
|
|
1175
|
+
report += `### Updated SOLID Compliance\n\n`;
|
|
1176
|
+
report += `| Principle | Status |\n`;
|
|
1177
|
+
report += `|-----------|--------|\n`;
|
|
1178
|
+
const principleNames = {
|
|
1179
|
+
srp: 'Single Responsibility',
|
|
1180
|
+
ocp: 'Open/Closed',
|
|
1181
|
+
lsp: 'Liskov Substitution',
|
|
1182
|
+
isp: 'Interface Segregation',
|
|
1183
|
+
dip: 'Dependency Inversion',
|
|
1184
|
+
};
|
|
1185
|
+
Object.entries(updatedSolidCompliance).forEach(([principle, compliant]) => {
|
|
1186
|
+
report += `| ${principleNames[principle] || principle} | ${compliant ? '✅' : '❌'} |\n`;
|
|
1187
|
+
});
|
|
1188
|
+
report += `\n`;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Quality Gate
|
|
1192
|
+
report += `### Updated Quality Gate\n\n`;
|
|
1193
|
+
report += `**Status**: ${updatedQualityGate.passed ? '✅ PASSED' : '❌ FAILED'}\n\n`;
|
|
1194
|
+
report += `| Criterion | Status |\n`;
|
|
1195
|
+
report += `|-----------|--------|\n`;
|
|
1196
|
+
updatedQualityGate.criteria.forEach(c => {
|
|
1197
|
+
report += `| ${c.name} | ${c.passed ? '✅' : '❌'} (${c.actual}/${c.threshold}) |\n`;
|
|
1198
|
+
});
|
|
1199
|
+
report += `\n`;
|
|
1200
|
+
|
|
1201
|
+
// Files Modified
|
|
1202
|
+
report += `### Files Modified\n\n`;
|
|
1203
|
+
filesModified.forEach((file, index) => {
|
|
1204
|
+
report += `${index + 1}. \`${file}\`\n`;
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
return report;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* 内部: 問題情報を取得(簡易実装)
|
|
1212
|
+
*/
|
|
1213
|
+
_findIssueInContent(_content, issueId) {
|
|
1214
|
+
// 実際の実装ではレビュー結果から問題を検索
|
|
1215
|
+
return {
|
|
1216
|
+
id: issueId,
|
|
1217
|
+
category: 'unknown',
|
|
1218
|
+
evidence: '',
|
|
1219
|
+
recommendation: '',
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* 内部: ADRを生成
|
|
1225
|
+
*/
|
|
1226
|
+
_generateADR(issueId, reason, adrPath) {
|
|
1227
|
+
const adrId = `ADR-${Date.now()}`;
|
|
1228
|
+
const date = new Date().toISOString().split('T')[0];
|
|
1229
|
+
|
|
1230
|
+
const adrContent = `# ${adrId}: Design Decision for ${issueId}
|
|
1231
|
+
|
|
1232
|
+
## Status
|
|
1233
|
+
|
|
1234
|
+
Accepted
|
|
1235
|
+
|
|
1236
|
+
## Context
|
|
1237
|
+
|
|
1238
|
+
During design review, issue ${issueId} was identified. After analysis, the team decided to accept the current design with documented rationale.
|
|
1239
|
+
|
|
1240
|
+
## Decision
|
|
1241
|
+
|
|
1242
|
+
${reason || 'The current design is acceptable for the project requirements.'}
|
|
1243
|
+
|
|
1244
|
+
## Consequences
|
|
1245
|
+
|
|
1246
|
+
### Positive
|
|
1247
|
+
- Decision is documented and traceable
|
|
1248
|
+
- Team alignment on design approach
|
|
1249
|
+
|
|
1250
|
+
### Negative
|
|
1251
|
+
- May require revisiting in future iterations
|
|
1252
|
+
|
|
1253
|
+
## Date
|
|
1254
|
+
|
|
1255
|
+
${date}
|
|
1256
|
+
`;
|
|
1257
|
+
|
|
1258
|
+
const adrFilePath = adrPath
|
|
1259
|
+
? path.join(adrPath, `${adrId}-${issueId.toLowerCase()}.md`)
|
|
1260
|
+
: `docs/adr/${adrId}-${issueId.toLowerCase()}.md`;
|
|
1261
|
+
|
|
1262
|
+
// 実際のファイル書き込みは呼び出し元で行う
|
|
1263
|
+
return {
|
|
1264
|
+
id: adrId,
|
|
1265
|
+
issueId,
|
|
1266
|
+
decision: reason || 'Accepted current design',
|
|
1267
|
+
path: adrFilePath,
|
|
1268
|
+
content: adrContent,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* 内部: 変更履歴エントリを生成
|
|
1274
|
+
*/
|
|
1275
|
+
_generateChangeHistoryEntry(appliedChanges) {
|
|
1276
|
+
if (appliedChanges.length === 0) return null;
|
|
1277
|
+
|
|
1278
|
+
const date = new Date().toISOString().split('T')[0];
|
|
1279
|
+
let entry = `### ${date} - Design Review Corrections\n\n`;
|
|
1280
|
+
entry += `| Issue ID | Category | Change Type |\n`;
|
|
1281
|
+
entry += `|----------|----------|-------------|\n`;
|
|
1282
|
+
appliedChanges.forEach(change => {
|
|
1283
|
+
entry += `| ${change.issueId} | ${change.category} | ${change.action} |\n`;
|
|
1284
|
+
});
|
|
1285
|
+
entry += `\n`;
|
|
1286
|
+
|
|
1287
|
+
return entry;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
module.exports = {
|
|
1292
|
+
DesignReviewer,
|
|
1293
|
+
DesignReviewResult,
|
|
1294
|
+
DesignIssue,
|
|
1295
|
+
IssueSeverity,
|
|
1296
|
+
IssueCategory,
|
|
1297
|
+
SOLIDPrinciple,
|
|
1298
|
+
ReviewFocus,
|
|
1299
|
+
QualityAttribute,
|
|
1300
|
+
};
|