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,1019 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirements Reviewer
|
|
3
|
+
*
|
|
4
|
+
* Fagan Inspection と Perspective-Based Reading (PBR) を用いた
|
|
5
|
+
* 要件定義書の体系的なレビューを実施
|
|
6
|
+
*
|
|
7
|
+
* @module src/validators/requirements-reviewer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 欠陥の深刻度
|
|
15
|
+
*/
|
|
16
|
+
const DefectSeverity = {
|
|
17
|
+
CRITICAL: 'critical',
|
|
18
|
+
MAJOR: 'major',
|
|
19
|
+
MINOR: 'minor',
|
|
20
|
+
SUGGESTION: 'suggestion',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 欠陥の種類
|
|
25
|
+
*/
|
|
26
|
+
const DefectType = {
|
|
27
|
+
MISSING: 'missing',
|
|
28
|
+
INCORRECT: 'incorrect',
|
|
29
|
+
AMBIGUOUS: 'ambiguous',
|
|
30
|
+
CONFLICTING: 'conflicting',
|
|
31
|
+
REDUNDANT: 'redundant',
|
|
32
|
+
UNTESTABLE: 'untestable',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* レビューの視点
|
|
37
|
+
*/
|
|
38
|
+
const ReviewPerspective = {
|
|
39
|
+
USER: 'user',
|
|
40
|
+
DEVELOPER: 'developer',
|
|
41
|
+
TESTER: 'tester',
|
|
42
|
+
ARCHITECT: 'architect',
|
|
43
|
+
SECURITY: 'security',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* レビュー方式
|
|
48
|
+
*/
|
|
49
|
+
const ReviewMethod = {
|
|
50
|
+
FAGAN: 'fagan',
|
|
51
|
+
PBR: 'pbr',
|
|
52
|
+
COMBINED: 'combined',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 欠陥クラス
|
|
57
|
+
*/
|
|
58
|
+
class Defect {
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
this.id = options.id || `DEF-${Date.now()}`;
|
|
61
|
+
this.requirementId = options.requirementId || '';
|
|
62
|
+
this.section = options.section || '';
|
|
63
|
+
this.severity = options.severity || DefectSeverity.MINOR;
|
|
64
|
+
this.type = options.type || DefectType.AMBIGUOUS;
|
|
65
|
+
this.perspective = options.perspective || null;
|
|
66
|
+
this.title = options.title || '';
|
|
67
|
+
this.description = options.description || '';
|
|
68
|
+
this.evidence = options.evidence || '';
|
|
69
|
+
this.recommendation = options.recommendation || '';
|
|
70
|
+
this.status = options.status || 'open';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toJSON() {
|
|
74
|
+
return {
|
|
75
|
+
id: this.id,
|
|
76
|
+
requirementId: this.requirementId,
|
|
77
|
+
section: this.section,
|
|
78
|
+
severity: this.severity,
|
|
79
|
+
type: this.type,
|
|
80
|
+
perspective: this.perspective,
|
|
81
|
+
title: this.title,
|
|
82
|
+
description: this.description,
|
|
83
|
+
evidence: this.evidence,
|
|
84
|
+
recommendation: this.recommendation,
|
|
85
|
+
status: this.status,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* レビュー結果クラス
|
|
92
|
+
*/
|
|
93
|
+
class ReviewResult {
|
|
94
|
+
constructor() {
|
|
95
|
+
this.defects = [];
|
|
96
|
+
this.metrics = {
|
|
97
|
+
totalDefects: 0,
|
|
98
|
+
bySeverity: {},
|
|
99
|
+
byType: {},
|
|
100
|
+
byPerspective: {},
|
|
101
|
+
reviewCoverage: 0,
|
|
102
|
+
earsCompliance: 0,
|
|
103
|
+
testabilityScore: 0,
|
|
104
|
+
};
|
|
105
|
+
this.qualityGate = {
|
|
106
|
+
passed: false,
|
|
107
|
+
criteria: [],
|
|
108
|
+
};
|
|
109
|
+
this.timestamp = new Date();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
addDefect(defect) {
|
|
113
|
+
this.defects.push(defect);
|
|
114
|
+
this.updateMetrics();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
updateMetrics() {
|
|
118
|
+
this.metrics.totalDefects = this.defects.length;
|
|
119
|
+
|
|
120
|
+
// Severity別カウント
|
|
121
|
+
this.metrics.bySeverity = {};
|
|
122
|
+
Object.values(DefectSeverity).forEach(sev => {
|
|
123
|
+
this.metrics.bySeverity[sev] = this.defects.filter(d => d.severity === sev).length;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Type別カウント
|
|
127
|
+
this.metrics.byType = {};
|
|
128
|
+
Object.values(DefectType).forEach(type => {
|
|
129
|
+
this.metrics.byType[type] = this.defects.filter(d => d.type === type).length;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Perspective別カウント
|
|
133
|
+
this.metrics.byPerspective = {};
|
|
134
|
+
Object.values(ReviewPerspective).forEach(persp => {
|
|
135
|
+
this.metrics.byPerspective[persp] = this.defects.filter(d => d.perspective === persp).length;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
evaluateQualityGate(options = {}) {
|
|
140
|
+
const {
|
|
141
|
+
maxCritical = 0,
|
|
142
|
+
maxMajorPercent = 20,
|
|
143
|
+
minTestabilityScore = 0.7,
|
|
144
|
+
minEarsCompliance = 0.8,
|
|
145
|
+
} = options;
|
|
146
|
+
|
|
147
|
+
const criteria = [];
|
|
148
|
+
|
|
149
|
+
// Critical欠陥チェック
|
|
150
|
+
const criticalCount = this.metrics.bySeverity[DefectSeverity.CRITICAL] || 0;
|
|
151
|
+
criteria.push({
|
|
152
|
+
name: 'No Critical Defects',
|
|
153
|
+
passed: criticalCount <= maxCritical,
|
|
154
|
+
actual: criticalCount,
|
|
155
|
+
threshold: maxCritical,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Major欠陥率チェック
|
|
159
|
+
const majorCount = this.metrics.bySeverity[DefectSeverity.MAJOR] || 0;
|
|
160
|
+
const majorPercent =
|
|
161
|
+
this.metrics.totalDefects > 0 ? (majorCount / this.metrics.totalDefects) * 100 : 0;
|
|
162
|
+
criteria.push({
|
|
163
|
+
name: 'Major Defects Under Threshold',
|
|
164
|
+
passed: majorPercent <= maxMajorPercent,
|
|
165
|
+
actual: Math.round(majorPercent),
|
|
166
|
+
threshold: maxMajorPercent,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// テスト可能性スコアチェック
|
|
170
|
+
criteria.push({
|
|
171
|
+
name: 'Testability Score',
|
|
172
|
+
passed: this.metrics.testabilityScore >= minTestabilityScore,
|
|
173
|
+
actual: Math.round(this.metrics.testabilityScore * 100),
|
|
174
|
+
threshold: Math.round(minTestabilityScore * 100),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// EARS準拠率チェック
|
|
178
|
+
criteria.push({
|
|
179
|
+
name: 'EARS Compliance',
|
|
180
|
+
passed: this.metrics.earsCompliance >= minEarsCompliance,
|
|
181
|
+
actual: Math.round(this.metrics.earsCompliance * 100),
|
|
182
|
+
threshold: Math.round(minEarsCompliance * 100),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
this.qualityGate.criteria = criteria;
|
|
186
|
+
this.qualityGate.passed = criteria.every(c => c.passed);
|
|
187
|
+
|
|
188
|
+
return this.qualityGate;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
toMarkdown() {
|
|
192
|
+
let md = `# Requirements Review Report\n\n`;
|
|
193
|
+
md += `**Date**: ${this.timestamp.toISOString().split('T')[0]}\n\n`;
|
|
194
|
+
|
|
195
|
+
// Summary
|
|
196
|
+
md += `## Summary\n\n`;
|
|
197
|
+
md += `| Severity | Count |\n`;
|
|
198
|
+
md += `|----------|-------|\n`;
|
|
199
|
+
Object.entries(this.metrics.bySeverity).forEach(([severity, count]) => {
|
|
200
|
+
const emoji = {
|
|
201
|
+
critical: '🔴',
|
|
202
|
+
major: '🟠',
|
|
203
|
+
minor: '🟡',
|
|
204
|
+
suggestion: '🟢',
|
|
205
|
+
};
|
|
206
|
+
md += `| ${emoji[severity] || ''} ${severity} | ${count} |\n`;
|
|
207
|
+
});
|
|
208
|
+
md += `| **Total** | **${this.metrics.totalDefects}** |\n\n`;
|
|
209
|
+
|
|
210
|
+
// Quality Gate
|
|
211
|
+
md += `## Quality Gate\n\n`;
|
|
212
|
+
md += `**Status**: ${this.qualityGate.passed ? '✅ PASSED' : '❌ FAILED'}\n\n`;
|
|
213
|
+
md += `| Criterion | Status | Actual | Threshold |\n`;
|
|
214
|
+
md += `|-----------|--------|--------|-----------|\n`;
|
|
215
|
+
this.qualityGate.criteria.forEach(c => {
|
|
216
|
+
md += `| ${c.name} | ${c.passed ? '✅' : '❌'} | ${c.actual} | ${c.threshold} |\n`;
|
|
217
|
+
});
|
|
218
|
+
md += '\n';
|
|
219
|
+
|
|
220
|
+
// Detailed Defects
|
|
221
|
+
md += `## Defects\n\n`;
|
|
222
|
+
this.defects.forEach(defect => {
|
|
223
|
+
md += `### ${defect.id}: ${defect.title}\n\n`;
|
|
224
|
+
md += `- **Requirement**: ${defect.requirementId}\n`;
|
|
225
|
+
md += `- **Section**: ${defect.section}\n`;
|
|
226
|
+
md += `- **Severity**: ${defect.severity}\n`;
|
|
227
|
+
md += `- **Type**: ${defect.type}\n`;
|
|
228
|
+
if (defect.perspective) {
|
|
229
|
+
md += `- **Perspective**: ${defect.perspective}\n`;
|
|
230
|
+
}
|
|
231
|
+
md += `\n**Description**: ${defect.description}\n\n`;
|
|
232
|
+
if (defect.evidence) {
|
|
233
|
+
md += `**Evidence**: "${defect.evidence}"\n\n`;
|
|
234
|
+
}
|
|
235
|
+
md += `**Recommendation**: ${defect.recommendation}\n\n`;
|
|
236
|
+
md += `---\n\n`;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return md;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
toJSON() {
|
|
243
|
+
return {
|
|
244
|
+
defects: this.defects.map(d => d.toJSON()),
|
|
245
|
+
metrics: this.metrics,
|
|
246
|
+
qualityGate: this.qualityGate,
|
|
247
|
+
timestamp: this.timestamp.toISOString(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Requirements Reviewer クラス
|
|
254
|
+
*/
|
|
255
|
+
class RequirementsReviewer {
|
|
256
|
+
constructor(projectPath = process.cwd()) {
|
|
257
|
+
this.projectPath = projectPath;
|
|
258
|
+
this.earsPatterns = {
|
|
259
|
+
ubiquitous: /^The\s+\w+\s+shall\s+/i,
|
|
260
|
+
eventDriven: /^When\s+.+,\s+the\s+\w+\s+shall\s+/i,
|
|
261
|
+
stateDriven: /^While\s+.+,\s+the\s+\w+\s+shall\s+/i,
|
|
262
|
+
unwanted: /^If\s+.+,\s+then\s+the\s+\w+\s+shall\s+/i,
|
|
263
|
+
optional: /^Where\s+.+,\s+the\s+\w+\s+shall\s+/i,
|
|
264
|
+
};
|
|
265
|
+
this.ambiguousTerms = [
|
|
266
|
+
'quickly',
|
|
267
|
+
'fast',
|
|
268
|
+
'slow',
|
|
269
|
+
'many',
|
|
270
|
+
'few',
|
|
271
|
+
'often',
|
|
272
|
+
'sometimes',
|
|
273
|
+
'usually',
|
|
274
|
+
'normally',
|
|
275
|
+
'appropriate',
|
|
276
|
+
'suitable',
|
|
277
|
+
'adequate',
|
|
278
|
+
'reasonable',
|
|
279
|
+
'user-friendly',
|
|
280
|
+
'easy',
|
|
281
|
+
'simple',
|
|
282
|
+
'flexible',
|
|
283
|
+
'efficient',
|
|
284
|
+
'effective',
|
|
285
|
+
'robust',
|
|
286
|
+
'scalable',
|
|
287
|
+
'secure',
|
|
288
|
+
'reliable',
|
|
289
|
+
'etc',
|
|
290
|
+
'and so on',
|
|
291
|
+
'as needed',
|
|
292
|
+
'if necessary',
|
|
293
|
+
'when required',
|
|
294
|
+
'as appropriate',
|
|
295
|
+
// Japanese equivalents
|
|
296
|
+
'迅速',
|
|
297
|
+
'高速',
|
|
298
|
+
'適切',
|
|
299
|
+
'適当',
|
|
300
|
+
'十分',
|
|
301
|
+
'多く',
|
|
302
|
+
'少なく',
|
|
303
|
+
'必要に応じて',
|
|
304
|
+
'など',
|
|
305
|
+
'ユーザーフレンドリー',
|
|
306
|
+
'使いやすい',
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* ドキュメントを読み込む
|
|
312
|
+
*/
|
|
313
|
+
async loadDocument(documentPath) {
|
|
314
|
+
const fullPath = path.isAbsolute(documentPath)
|
|
315
|
+
? documentPath
|
|
316
|
+
: path.join(this.projectPath, documentPath);
|
|
317
|
+
|
|
318
|
+
if (!fs.existsSync(fullPath)) {
|
|
319
|
+
throw new Error(`Document not found: ${fullPath}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return fs.readFileSync(fullPath, 'utf-8');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 要件を抽出
|
|
327
|
+
*/
|
|
328
|
+
extractRequirements(content) {
|
|
329
|
+
const requirements = [];
|
|
330
|
+
const reqPattern = /(?:REQ-[A-Z0-9]+-\d+|FR-\d+|NFR-\d+|UC-\d+)[:\s]+([^\n]+)/gi;
|
|
331
|
+
let match;
|
|
332
|
+
|
|
333
|
+
while ((match = reqPattern.exec(content)) !== null) {
|
|
334
|
+
requirements.push({
|
|
335
|
+
id: match[0].split(/[:\s]/)[0],
|
|
336
|
+
text: match[1] || '',
|
|
337
|
+
fullText: match[0],
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return requirements;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* EARS形式準拠をチェック
|
|
346
|
+
*/
|
|
347
|
+
checkEarsCompliance(requirements) {
|
|
348
|
+
let compliantCount = 0;
|
|
349
|
+
|
|
350
|
+
requirements.forEach(req => {
|
|
351
|
+
const text = req.text;
|
|
352
|
+
const isCompliant = Object.values(this.earsPatterns).some(pattern => pattern.test(text));
|
|
353
|
+
if (isCompliant) {
|
|
354
|
+
compliantCount++;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return requirements.length > 0 ? compliantCount / requirements.length : 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 曖昧な用語をチェック
|
|
363
|
+
*/
|
|
364
|
+
findAmbiguousTerms(text) {
|
|
365
|
+
const found = [];
|
|
366
|
+
this.ambiguousTerms.forEach(term => {
|
|
367
|
+
const regex = new RegExp(`\\b${term}\\b`, 'gi');
|
|
368
|
+
if (regex.test(text)) {
|
|
369
|
+
found.push(term);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
return found;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* テスト可能性をチェック
|
|
377
|
+
*/
|
|
378
|
+
checkTestability(requirements) {
|
|
379
|
+
let testableCount = 0;
|
|
380
|
+
|
|
381
|
+
requirements.forEach(req => {
|
|
382
|
+
const text = req.text.toLowerCase();
|
|
383
|
+
|
|
384
|
+
// テスト可能な指標
|
|
385
|
+
const hasNumber = /\d+/.test(text);
|
|
386
|
+
const hasUnit = /\b(秒|分|時間|日|%|パーセント|seconds?|minutes?|hours?|days?|percent)/i.test(
|
|
387
|
+
text
|
|
388
|
+
);
|
|
389
|
+
const hasQuantifier = /\b(以下|以上|未満|超|within|at least|at most|maximum|minimum)/i.test(
|
|
390
|
+
text
|
|
391
|
+
);
|
|
392
|
+
const ambiguousTerms = this.findAmbiguousTerms(text);
|
|
393
|
+
|
|
394
|
+
const testabilityScore =
|
|
395
|
+
(hasNumber ? 0.3 : 0) +
|
|
396
|
+
(hasUnit ? 0.3 : 0) +
|
|
397
|
+
(hasQuantifier ? 0.2 : 0) +
|
|
398
|
+
(ambiguousTerms.length === 0 ? 0.2 : 0);
|
|
399
|
+
|
|
400
|
+
if (testabilityScore >= 0.5) {
|
|
401
|
+
testableCount++;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return requirements.length > 0 ? testableCount / requirements.length : 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Fagan Inspectionスタイルのレビュー
|
|
410
|
+
*/
|
|
411
|
+
async reviewFagan(content, _options = {}) {
|
|
412
|
+
const result = new ReviewResult();
|
|
413
|
+
const requirements = this.extractRequirements(content);
|
|
414
|
+
|
|
415
|
+
let defectCounter = 1;
|
|
416
|
+
|
|
417
|
+
// Phase 1: Completeness Check (完全性チェック)
|
|
418
|
+
const requiredSections = [
|
|
419
|
+
{ pattern: /## .*機能要件|## .*Functional/i, name: 'Functional Requirements' },
|
|
420
|
+
{ pattern: /## .*非機能要件|## .*Non-Functional/i, name: 'Non-Functional Requirements' },
|
|
421
|
+
{ pattern: /## .*制約|## .*Constraints/i, name: 'Constraints' },
|
|
422
|
+
{ pattern: /## .*用語|## .*Glossary|## .*Definitions/i, name: 'Glossary/Definitions' },
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
requiredSections.forEach(section => {
|
|
426
|
+
if (!section.pattern.test(content)) {
|
|
427
|
+
result.addDefect(
|
|
428
|
+
new Defect({
|
|
429
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
430
|
+
severity: DefectSeverity.MAJOR,
|
|
431
|
+
type: DefectType.MISSING,
|
|
432
|
+
title: `Missing Section: ${section.name}`,
|
|
433
|
+
description: `Required section "${section.name}" is not found in the document.`,
|
|
434
|
+
recommendation: `Add a section for "${section.name}" to ensure completeness.`,
|
|
435
|
+
})
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Phase 2: Requirements Quality Check
|
|
441
|
+
requirements.forEach(req => {
|
|
442
|
+
// Ambiguity check
|
|
443
|
+
const ambiguousTerms = this.findAmbiguousTerms(req.text);
|
|
444
|
+
if (ambiguousTerms.length > 0) {
|
|
445
|
+
result.addDefect(
|
|
446
|
+
new Defect({
|
|
447
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
448
|
+
requirementId: req.id,
|
|
449
|
+
severity: DefectSeverity.MAJOR,
|
|
450
|
+
type: DefectType.AMBIGUOUS,
|
|
451
|
+
title: `Ambiguous Terms in ${req.id}`,
|
|
452
|
+
description: `Requirement contains ambiguous terms: ${ambiguousTerms.join(', ')}`,
|
|
453
|
+
evidence: req.fullText,
|
|
454
|
+
recommendation: `Replace ambiguous terms with measurable criteria. Example: "quickly" → "within 2 seconds"`,
|
|
455
|
+
})
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// EARS format check
|
|
460
|
+
const isEarsCompliant = Object.values(this.earsPatterns).some(pattern =>
|
|
461
|
+
pattern.test(req.text)
|
|
462
|
+
);
|
|
463
|
+
if (!isEarsCompliant && req.text.length > 10) {
|
|
464
|
+
result.addDefect(
|
|
465
|
+
new Defect({
|
|
466
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
467
|
+
requirementId: req.id,
|
|
468
|
+
severity: DefectSeverity.MINOR,
|
|
469
|
+
type: DefectType.AMBIGUOUS,
|
|
470
|
+
title: `Non-EARS Format in ${req.id}`,
|
|
471
|
+
description: `Requirement does not follow EARS format patterns.`,
|
|
472
|
+
evidence: req.fullText,
|
|
473
|
+
recommendation: `Convert to EARS format. Example patterns: "The system shall...", "When X, the system shall...", "While X, the system shall..."`,
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Missing test criteria check
|
|
479
|
+
const hasTestCriteria =
|
|
480
|
+
/\d+/.test(req.text) ||
|
|
481
|
+
/shall (not )?be (able to )?/i.test(req.text) ||
|
|
482
|
+
/must (not )?/i.test(req.text);
|
|
483
|
+
if (!hasTestCriteria && req.text.length > 20) {
|
|
484
|
+
result.addDefect(
|
|
485
|
+
new Defect({
|
|
486
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
487
|
+
requirementId: req.id,
|
|
488
|
+
severity: DefectSeverity.MINOR,
|
|
489
|
+
type: DefectType.UNTESTABLE,
|
|
490
|
+
title: `Potentially Untestable: ${req.id}`,
|
|
491
|
+
description: `Requirement may lack measurable acceptance criteria.`,
|
|
492
|
+
evidence: req.fullText,
|
|
493
|
+
recommendation: `Add specific, measurable criteria. Include numbers, thresholds, or clear pass/fail conditions.`,
|
|
494
|
+
})
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Phase 3: Consistency Check (矛盾チェック)
|
|
500
|
+
// 同じ要件IDの重複をチェック
|
|
501
|
+
const idCounts = {};
|
|
502
|
+
requirements.forEach(req => {
|
|
503
|
+
idCounts[req.id] = (idCounts[req.id] || 0) + 1;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
Object.entries(idCounts).forEach(([id, count]) => {
|
|
507
|
+
if (count > 1) {
|
|
508
|
+
result.addDefect(
|
|
509
|
+
new Defect({
|
|
510
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
511
|
+
requirementId: id,
|
|
512
|
+
severity: DefectSeverity.MAJOR,
|
|
513
|
+
type: DefectType.REDUNDANT,
|
|
514
|
+
title: `Duplicate Requirement ID: ${id}`,
|
|
515
|
+
description: `Requirement ID "${id}" appears ${count} times in the document.`,
|
|
516
|
+
recommendation: `Review and consolidate duplicate requirements or assign unique IDs.`,
|
|
517
|
+
})
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Update metrics
|
|
523
|
+
result.metrics.earsCompliance = this.checkEarsCompliance(requirements);
|
|
524
|
+
result.metrics.testabilityScore = this.checkTestability(requirements);
|
|
525
|
+
result.metrics.reviewCoverage = 1.0;
|
|
526
|
+
|
|
527
|
+
result.evaluateQualityGate();
|
|
528
|
+
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Perspective-Based Reading レビュー
|
|
534
|
+
*/
|
|
535
|
+
async reviewPBR(content, options = {}) {
|
|
536
|
+
const result = new ReviewResult();
|
|
537
|
+
const requirements = this.extractRequirements(content);
|
|
538
|
+
const perspectives = options.perspectives || Object.values(ReviewPerspective);
|
|
539
|
+
|
|
540
|
+
let defectCounter = 1;
|
|
541
|
+
|
|
542
|
+
// User Perspective
|
|
543
|
+
if (perspectives.includes(ReviewPerspective.USER)) {
|
|
544
|
+
// ユーザーシナリオの有無チェック
|
|
545
|
+
if (!/ユーザー|user|利用者|actor/i.test(content)) {
|
|
546
|
+
result.addDefect(
|
|
547
|
+
new Defect({
|
|
548
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
549
|
+
perspective: ReviewPerspective.USER,
|
|
550
|
+
severity: DefectSeverity.MAJOR,
|
|
551
|
+
type: DefectType.MISSING,
|
|
552
|
+
title: 'Missing User Scenarios',
|
|
553
|
+
description: 'Document does not clearly identify users or user scenarios.',
|
|
554
|
+
recommendation:
|
|
555
|
+
'Add user personas and scenarios to clarify who will use the system and how.',
|
|
556
|
+
})
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// エラーメッセージ要件チェック
|
|
561
|
+
if (!/エラー.*メッセージ|error.*message/i.test(content)) {
|
|
562
|
+
result.addDefect(
|
|
563
|
+
new Defect({
|
|
564
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
565
|
+
perspective: ReviewPerspective.USER,
|
|
566
|
+
severity: DefectSeverity.MINOR,
|
|
567
|
+
type: DefectType.MISSING,
|
|
568
|
+
title: 'Missing Error Message Requirements',
|
|
569
|
+
description: 'No requirements found for error messages or user feedback.',
|
|
570
|
+
recommendation: 'Define how error conditions are communicated to users.',
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Developer Perspective
|
|
577
|
+
if (perspectives.includes(ReviewPerspective.DEVELOPER)) {
|
|
578
|
+
// データ型の定義チェック
|
|
579
|
+
requirements.forEach(req => {
|
|
580
|
+
if (/データ|data|入力|output|input|値|value/i.test(req.text)) {
|
|
581
|
+
if (!/型|type|format|形式|string|number|integer|boolean/i.test(req.text)) {
|
|
582
|
+
result.addDefect(
|
|
583
|
+
new Defect({
|
|
584
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
585
|
+
requirementId: req.id,
|
|
586
|
+
perspective: ReviewPerspective.DEVELOPER,
|
|
587
|
+
severity: DefectSeverity.MINOR,
|
|
588
|
+
type: DefectType.MISSING,
|
|
589
|
+
title: `Missing Data Type: ${req.id}`,
|
|
590
|
+
description: 'Requirement mentions data but does not specify data types.',
|
|
591
|
+
evidence: req.fullText,
|
|
592
|
+
recommendation: 'Specify data types, formats, and valid ranges.',
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// APIインターフェースチェック
|
|
600
|
+
if (/API|interface|インターフェース|連携/i.test(content)) {
|
|
601
|
+
if (!/endpoint|エンドポイント|request|response|JSON|XML/i.test(content)) {
|
|
602
|
+
result.addDefect(
|
|
603
|
+
new Defect({
|
|
604
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
605
|
+
perspective: ReviewPerspective.DEVELOPER,
|
|
606
|
+
severity: DefectSeverity.MAJOR,
|
|
607
|
+
type: DefectType.MISSING,
|
|
608
|
+
title: 'Incomplete API Specifications',
|
|
609
|
+
description: 'API/interfaces mentioned but details not specified.',
|
|
610
|
+
recommendation:
|
|
611
|
+
'Define API endpoints, request/response formats, authentication, and error codes.',
|
|
612
|
+
})
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Tester Perspective
|
|
619
|
+
if (perspectives.includes(ReviewPerspective.TESTER)) {
|
|
620
|
+
requirements.forEach(req => {
|
|
621
|
+
// 境界値チェック
|
|
622
|
+
if (/範囲|range|以上|以下|between|limit/i.test(req.text)) {
|
|
623
|
+
if (!/最小|最大|min|max|boundary|\d+/i.test(req.text)) {
|
|
624
|
+
result.addDefect(
|
|
625
|
+
new Defect({
|
|
626
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
627
|
+
requirementId: req.id,
|
|
628
|
+
perspective: ReviewPerspective.TESTER,
|
|
629
|
+
severity: DefectSeverity.MINOR,
|
|
630
|
+
type: DefectType.MISSING,
|
|
631
|
+
title: `Missing Boundary Values: ${req.id}`,
|
|
632
|
+
description: 'Requirement mentions ranges but boundary values are not specified.',
|
|
633
|
+
evidence: req.fullText,
|
|
634
|
+
recommendation: 'Specify exact minimum and maximum values for boundary testing.',
|
|
635
|
+
})
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// 受入基準チェック
|
|
642
|
+
if (!/受入基準|acceptance criteria|verification|検証/i.test(content)) {
|
|
643
|
+
result.addDefect(
|
|
644
|
+
new Defect({
|
|
645
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
646
|
+
perspective: ReviewPerspective.TESTER,
|
|
647
|
+
severity: DefectSeverity.MAJOR,
|
|
648
|
+
type: DefectType.MISSING,
|
|
649
|
+
title: 'Missing Acceptance Criteria',
|
|
650
|
+
description: 'Document does not include explicit acceptance criteria.',
|
|
651
|
+
recommendation:
|
|
652
|
+
'Add acceptance criteria section with testable conditions for each requirement.',
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Architect Perspective
|
|
659
|
+
if (perspectives.includes(ReviewPerspective.ARCHITECT)) {
|
|
660
|
+
// スケーラビリティ要件チェック
|
|
661
|
+
if (!/スケーラビリティ|scalability|拡張性|concurrent|同時/i.test(content)) {
|
|
662
|
+
result.addDefect(
|
|
663
|
+
new Defect({
|
|
664
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
665
|
+
perspective: ReviewPerspective.ARCHITECT,
|
|
666
|
+
severity: DefectSeverity.MINOR,
|
|
667
|
+
type: DefectType.MISSING,
|
|
668
|
+
title: 'Missing Scalability Requirements',
|
|
669
|
+
description: 'No requirements found for system scalability.',
|
|
670
|
+
recommendation: 'Define expected load, concurrent users, and scaling requirements.',
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 可用性要件チェック
|
|
676
|
+
if (!/可用性|availability|SLA|uptime|稼働率/i.test(content)) {
|
|
677
|
+
result.addDefect(
|
|
678
|
+
new Defect({
|
|
679
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
680
|
+
perspective: ReviewPerspective.ARCHITECT,
|
|
681
|
+
severity: DefectSeverity.MINOR,
|
|
682
|
+
type: DefectType.MISSING,
|
|
683
|
+
title: 'Missing Availability Requirements',
|
|
684
|
+
description: 'No availability/SLA requirements defined.',
|
|
685
|
+
recommendation:
|
|
686
|
+
'Specify target availability (e.g., 99.9% uptime) and recovery time objectives.',
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Security Perspective
|
|
693
|
+
if (perspectives.includes(ReviewPerspective.SECURITY)) {
|
|
694
|
+
// 認証要件チェック
|
|
695
|
+
if (
|
|
696
|
+
/ログイン|login|認証|authentication|ユーザー登録/i.test(content) &&
|
|
697
|
+
!/パスワード.*ポリシー|password.*policy|MFA|多要素|2FA/i.test(content)
|
|
698
|
+
) {
|
|
699
|
+
result.addDefect(
|
|
700
|
+
new Defect({
|
|
701
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
702
|
+
perspective: ReviewPerspective.SECURITY,
|
|
703
|
+
severity: DefectSeverity.MAJOR,
|
|
704
|
+
type: DefectType.MISSING,
|
|
705
|
+
title: 'Incomplete Authentication Requirements',
|
|
706
|
+
description: 'Authentication mentioned but security policies not defined.',
|
|
707
|
+
recommendation:
|
|
708
|
+
'Define password policy, MFA requirements, session management, and lockout policies.',
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// データ保護チェック
|
|
714
|
+
if (
|
|
715
|
+
/個人情報|personal|PII|機密|sensitive|プライバシー/i.test(content) &&
|
|
716
|
+
!/暗号化|encryption|GDPR|匿名化|anonymize/i.test(content)
|
|
717
|
+
) {
|
|
718
|
+
result.addDefect(
|
|
719
|
+
new Defect({
|
|
720
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
721
|
+
perspective: ReviewPerspective.SECURITY,
|
|
722
|
+
severity: DefectSeverity.CRITICAL,
|
|
723
|
+
type: DefectType.MISSING,
|
|
724
|
+
title: 'Missing Data Protection Requirements',
|
|
725
|
+
description: 'Sensitive data mentioned but protection requirements not specified.',
|
|
726
|
+
recommendation:
|
|
727
|
+
'Define data encryption, retention, anonymization, and compliance requirements (GDPR, etc.).',
|
|
728
|
+
})
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 監査ログチェック
|
|
733
|
+
if (!/監査|audit.*log|ログ記録|logging/i.test(content)) {
|
|
734
|
+
result.addDefect(
|
|
735
|
+
new Defect({
|
|
736
|
+
id: `DEF-${String(defectCounter++).padStart(3, '0')}`,
|
|
737
|
+
perspective: ReviewPerspective.SECURITY,
|
|
738
|
+
severity: DefectSeverity.MINOR,
|
|
739
|
+
type: DefectType.MISSING,
|
|
740
|
+
title: 'Missing Audit Requirements',
|
|
741
|
+
description: 'No audit logging requirements found.',
|
|
742
|
+
recommendation:
|
|
743
|
+
'Define what events should be logged for security and compliance purposes.',
|
|
744
|
+
})
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Update metrics
|
|
750
|
+
result.metrics.earsCompliance = this.checkEarsCompliance(requirements);
|
|
751
|
+
result.metrics.testabilityScore = this.checkTestability(requirements);
|
|
752
|
+
result.metrics.reviewCoverage = perspectives.length / Object.values(ReviewPerspective).length;
|
|
753
|
+
|
|
754
|
+
result.evaluateQualityGate();
|
|
755
|
+
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* 総合レビュー(Fagan + PBR)
|
|
761
|
+
*/
|
|
762
|
+
async review(documentPath, options = {}) {
|
|
763
|
+
const content = await this.loadDocument(documentPath);
|
|
764
|
+
const method = options.method || ReviewMethod.COMBINED;
|
|
765
|
+
|
|
766
|
+
switch (method) {
|
|
767
|
+
case ReviewMethod.FAGAN:
|
|
768
|
+
return this.reviewFagan(content, options);
|
|
769
|
+
case ReviewMethod.PBR:
|
|
770
|
+
return this.reviewPBR(content, options);
|
|
771
|
+
case ReviewMethod.COMBINED:
|
|
772
|
+
default: {
|
|
773
|
+
// 両方のレビューを実行してマージ
|
|
774
|
+
const faganResult = await this.reviewFagan(content, options);
|
|
775
|
+
const pbrResult = await this.reviewPBR(content, options);
|
|
776
|
+
|
|
777
|
+
// 結果をマージ
|
|
778
|
+
const combinedResult = new ReviewResult();
|
|
779
|
+
const allDefects = [...faganResult.defects, ...pbrResult.defects];
|
|
780
|
+
|
|
781
|
+
// 重複を除去(同じrequirementIdとtypeの組み合わせ)
|
|
782
|
+
const seen = new Set();
|
|
783
|
+
allDefects.forEach(defect => {
|
|
784
|
+
const key = `${defect.requirementId}-${defect.type}-${defect.title}`;
|
|
785
|
+
if (!seen.has(key)) {
|
|
786
|
+
seen.add(key);
|
|
787
|
+
combinedResult.addDefect(defect);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// メトリクス統合
|
|
792
|
+
combinedResult.metrics.earsCompliance =
|
|
793
|
+
(faganResult.metrics.earsCompliance + pbrResult.metrics.earsCompliance) / 2;
|
|
794
|
+
combinedResult.metrics.testabilityScore =
|
|
795
|
+
(faganResult.metrics.testabilityScore + pbrResult.metrics.testabilityScore) / 2;
|
|
796
|
+
combinedResult.metrics.reviewCoverage = 1.0;
|
|
797
|
+
|
|
798
|
+
combinedResult.evaluateQualityGate(options.qualityGateOptions);
|
|
799
|
+
|
|
800
|
+
return combinedResult;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* レビュー結果に基づいてドキュメントを修正
|
|
807
|
+
* @param {string} documentPath - 修正対象のドキュメントパス
|
|
808
|
+
* @param {Array} corrections - 修正指示の配列
|
|
809
|
+
* @param {Object} options - オプション
|
|
810
|
+
* @returns {Object} 修正結果
|
|
811
|
+
*/
|
|
812
|
+
async applyCorrections(documentPath, corrections, options = {}) {
|
|
813
|
+
const fullPath = path.isAbsolute(documentPath)
|
|
814
|
+
? documentPath
|
|
815
|
+
: path.join(this.projectPath, documentPath);
|
|
816
|
+
|
|
817
|
+
if (!fs.existsSync(fullPath)) {
|
|
818
|
+
throw new Error(`Document not found: ${fullPath}`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// バックアップ作成
|
|
822
|
+
if (options.createBackup !== false) {
|
|
823
|
+
const backupPath = `${fullPath}.backup`;
|
|
824
|
+
fs.copyFileSync(fullPath, backupPath);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
let content = fs.readFileSync(fullPath, 'utf-8');
|
|
828
|
+
const appliedChanges = [];
|
|
829
|
+
const rejectedFindings = [];
|
|
830
|
+
|
|
831
|
+
for (const correction of corrections) {
|
|
832
|
+
const { defectId, action, newText, reason } = correction;
|
|
833
|
+
|
|
834
|
+
switch (action) {
|
|
835
|
+
case 'accept': {
|
|
836
|
+
// 推奨を適用
|
|
837
|
+
const defect = this._findDefectInContent(content, defectId);
|
|
838
|
+
if (defect && defect.evidence && defect.recommendation) {
|
|
839
|
+
content = content.replace(defect.evidence, defect.recommendation);
|
|
840
|
+
appliedChanges.push({
|
|
841
|
+
defectId,
|
|
842
|
+
action: 'accepted',
|
|
843
|
+
original: defect.evidence,
|
|
844
|
+
corrected: defect.recommendation,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
case 'modify': {
|
|
851
|
+
// カスタム修正を適用
|
|
852
|
+
const modifyDefect = this._findDefectInContent(content, defectId);
|
|
853
|
+
if (modifyDefect && modifyDefect.evidence && newText) {
|
|
854
|
+
content = content.replace(modifyDefect.evidence, newText);
|
|
855
|
+
appliedChanges.push({
|
|
856
|
+
defectId,
|
|
857
|
+
action: 'modified',
|
|
858
|
+
original: modifyDefect.evidence,
|
|
859
|
+
corrected: newText,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
case 'reject':
|
|
866
|
+
rejectedFindings.push({
|
|
867
|
+
defectId,
|
|
868
|
+
reason: reason || 'No reason provided',
|
|
869
|
+
});
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// 変更履歴を追加
|
|
875
|
+
const changeHistoryEntry = this._generateChangeHistoryEntry(appliedChanges);
|
|
876
|
+
if (changeHistoryEntry && !content.includes('## Change History')) {
|
|
877
|
+
content += `\n\n## Change History\n\n${changeHistoryEntry}`;
|
|
878
|
+
} else if (changeHistoryEntry) {
|
|
879
|
+
content = content.replace(
|
|
880
|
+
/## Change History\n/,
|
|
881
|
+
`## Change History\n\n${changeHistoryEntry}`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ファイルを保存
|
|
886
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
887
|
+
|
|
888
|
+
// 日本語版も更新
|
|
889
|
+
if (options.updateJapanese !== false) {
|
|
890
|
+
const jaPath = fullPath.replace(/\.md$/, '.ja.md');
|
|
891
|
+
if (fs.existsSync(jaPath)) {
|
|
892
|
+
// 簡易的な日本語版更新(実際にはより高度な翻訳ロジックが必要)
|
|
893
|
+
let jaContent = fs.readFileSync(jaPath, 'utf-8');
|
|
894
|
+
for (const change of appliedChanges) {
|
|
895
|
+
if (jaContent.includes(change.original)) {
|
|
896
|
+
jaContent = jaContent.replace(change.original, change.corrected);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
fs.writeFileSync(jaPath, jaContent, 'utf-8');
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// 再レビューして品質ゲートを更新
|
|
904
|
+
const updatedResult = await this.review(documentPath, options.reviewOptions || {});
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
success: true,
|
|
908
|
+
changesApplied: appliedChanges,
|
|
909
|
+
rejectedFindings,
|
|
910
|
+
updatedQualityGate: updatedResult.qualityGate,
|
|
911
|
+
updatedMetrics: updatedResult.metrics,
|
|
912
|
+
filesModified: [
|
|
913
|
+
fullPath,
|
|
914
|
+
options.createBackup !== false ? `${fullPath}.backup` : null,
|
|
915
|
+
options.updateJapanese !== false && fs.existsSync(fullPath.replace(/\.md$/, '.ja.md'))
|
|
916
|
+
? fullPath.replace(/\.md$/, '.ja.md')
|
|
917
|
+
: null,
|
|
918
|
+
].filter(Boolean),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* 修正レポートを生成
|
|
924
|
+
*/
|
|
925
|
+
generateCorrectionReport(correctionResult) {
|
|
926
|
+
const { changesApplied, rejectedFindings, updatedQualityGate, filesModified } =
|
|
927
|
+
correctionResult;
|
|
928
|
+
|
|
929
|
+
let report = `## 📝 Correction Report\n\n`;
|
|
930
|
+
report += `**Correction Date**: ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
931
|
+
|
|
932
|
+
// Changes Applied
|
|
933
|
+
report += `### Changes Applied\n\n`;
|
|
934
|
+
if (changesApplied.length > 0) {
|
|
935
|
+
report += `| Defect ID | Action | Original | Corrected |\n`;
|
|
936
|
+
report += `|-----------|--------|----------|----------|\n`;
|
|
937
|
+
changesApplied.forEach(change => {
|
|
938
|
+
const original = change.original.substring(0, 30) + '...';
|
|
939
|
+
const corrected = change.corrected.substring(0, 30) + '...';
|
|
940
|
+
report += `| ${change.defectId} | ${change.action} | "${original}" | "${corrected}" |\n`;
|
|
941
|
+
});
|
|
942
|
+
} else {
|
|
943
|
+
report += `No changes applied.\n`;
|
|
944
|
+
}
|
|
945
|
+
report += `\n`;
|
|
946
|
+
|
|
947
|
+
// Rejected Findings
|
|
948
|
+
report += `### Rejected Findings\n\n`;
|
|
949
|
+
if (rejectedFindings.length > 0) {
|
|
950
|
+
report += `| Defect ID | Reason |\n`;
|
|
951
|
+
report += `|-----------|--------|\n`;
|
|
952
|
+
rejectedFindings.forEach(finding => {
|
|
953
|
+
report += `| ${finding.defectId} | ${finding.reason} |\n`;
|
|
954
|
+
});
|
|
955
|
+
} else {
|
|
956
|
+
report += `No findings rejected.\n`;
|
|
957
|
+
}
|
|
958
|
+
report += `\n`;
|
|
959
|
+
|
|
960
|
+
// Quality Gate
|
|
961
|
+
report += `### Updated Quality Gate\n\n`;
|
|
962
|
+
report += `**Status**: ${updatedQualityGate.passed ? '✅ PASSED' : '❌ FAILED'}\n\n`;
|
|
963
|
+
report += `| Criterion | Status |\n`;
|
|
964
|
+
report += `|-----------|--------|\n`;
|
|
965
|
+
updatedQualityGate.criteria.forEach(c => {
|
|
966
|
+
report += `| ${c.name} | ${c.passed ? '✅' : '❌'} (${c.actual}/${c.threshold}) |\n`;
|
|
967
|
+
});
|
|
968
|
+
report += `\n`;
|
|
969
|
+
|
|
970
|
+
// Files Modified
|
|
971
|
+
report += `### Files Modified\n\n`;
|
|
972
|
+
filesModified.forEach((file, index) => {
|
|
973
|
+
report += `${index + 1}. \`${file}\`\n`;
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
return report;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* 内部: 欠陥情報を取得(簡易実装)
|
|
981
|
+
*/
|
|
982
|
+
_findDefectInContent(_content, defectId) {
|
|
983
|
+
// 実際の実装ではレビュー結果から欠陥を検索
|
|
984
|
+
// ここでは簡易的なプレースホルダーを返す
|
|
985
|
+
return {
|
|
986
|
+
id: defectId,
|
|
987
|
+
evidence: '',
|
|
988
|
+
recommendation: '',
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* 内部: 変更履歴エントリを生成
|
|
994
|
+
*/
|
|
995
|
+
_generateChangeHistoryEntry(appliedChanges) {
|
|
996
|
+
if (appliedChanges.length === 0) return null;
|
|
997
|
+
|
|
998
|
+
const date = new Date().toISOString().split('T')[0];
|
|
999
|
+
let entry = `### ${date} - Requirements Review Corrections\n\n`;
|
|
1000
|
+
entry += `| Defect ID | Change Type |\n`;
|
|
1001
|
+
entry += `|-----------|-------------|\n`;
|
|
1002
|
+
appliedChanges.forEach(change => {
|
|
1003
|
+
entry += `| ${change.defectId} | ${change.action} |\n`;
|
|
1004
|
+
});
|
|
1005
|
+
entry += `\n`;
|
|
1006
|
+
|
|
1007
|
+
return entry;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
module.exports = {
|
|
1012
|
+
RequirementsReviewer,
|
|
1013
|
+
ReviewResult,
|
|
1014
|
+
Defect,
|
|
1015
|
+
DefectSeverity,
|
|
1016
|
+
DefectType,
|
|
1017
|
+
ReviewPerspective,
|
|
1018
|
+
ReviewMethod,
|
|
1019
|
+
};
|