musubi-sdd 6.1.1 → 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.
@@ -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
+ };