@nahisaho/musubix-core 1.0.2 → 1.0.4
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/dist/cli/commands/design.d.ts.map +1 -1
- package/dist/cli/commands/design.js +330 -0
- package/dist/cli/commands/design.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +170 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/requirements.d.ts.map +1 -1
- package/dist/cli/commands/requirements.js +744 -2
- package/dist/cli/commands/requirements.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* @see DES-MUSUBIX-001 Section 16-C.1 - requirementsコマンド設計
|
|
13
13
|
* @see TSK-062〜065 - Requirements CLI実装
|
|
14
14
|
*/
|
|
15
|
-
import { readFile, writeFile, access } from 'fs/promises';
|
|
16
|
-
import { resolve } from 'path';
|
|
15
|
+
import { readFile, writeFile, access, mkdir } from 'fs/promises';
|
|
16
|
+
import { resolve, join } from 'path';
|
|
17
|
+
import { createInterface } from 'readline';
|
|
17
18
|
import { ExitCode, getGlobalOptions, outputResult } from '../base.js';
|
|
18
19
|
import { createEARSValidator, DEFAULT_EARS_OPTIONS, } from '../../validators/ears-validator.js';
|
|
20
|
+
import { VERSION } from '../../version.js';
|
|
19
21
|
/**
|
|
20
22
|
* Parse EARS requirements from text
|
|
21
23
|
*/
|
|
@@ -83,6 +85,33 @@ export function registerRequirementsCommand(program) {
|
|
|
83
85
|
const requirements = program
|
|
84
86
|
.command('requirements')
|
|
85
87
|
.description('Requirements analysis and validation');
|
|
88
|
+
// requirements new (interactive)
|
|
89
|
+
requirements
|
|
90
|
+
.command('new <feature>')
|
|
91
|
+
.description('Create new requirements through interactive dialogue')
|
|
92
|
+
.option('-i, --interactive', 'Enable interactive mode (default)', true)
|
|
93
|
+
.option('-o, --output <file>', 'Output file path')
|
|
94
|
+
.option('--no-interactive', 'Skip interactive mode')
|
|
95
|
+
.action(async (feature, options) => {
|
|
96
|
+
const globalOpts = getGlobalOptions(program);
|
|
97
|
+
try {
|
|
98
|
+
if (options.interactive !== false) {
|
|
99
|
+
await runInteractiveHearing(feature, options.output, globalOpts);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Generate minimal template without interaction
|
|
103
|
+
const outputPath = options.output ?? `storage/specs/REQ-${feature.toUpperCase()}-001.md`;
|
|
104
|
+
await generateMinimalTemplate(feature, outputPath, globalOpts);
|
|
105
|
+
}
|
|
106
|
+
process.exit(ExitCode.SUCCESS);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (!globalOpts.quiet) {
|
|
110
|
+
console.error(`❌ Failed: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
86
115
|
// requirements analyze
|
|
87
116
|
requirements
|
|
88
117
|
.command('analyze <input>')
|
|
@@ -399,5 +428,718 @@ function generateMarkdownOutput(result) {
|
|
|
399
428
|
}
|
|
400
429
|
return output;
|
|
401
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Predefined hearing questions (1問1答)
|
|
433
|
+
*/
|
|
434
|
+
const HEARING_QUESTIONS = [
|
|
435
|
+
{
|
|
436
|
+
id: 'purpose',
|
|
437
|
+
questionJa: 'この機能で解決したい「本当の課題」は何ですか?',
|
|
438
|
+
questionEn: 'What is the TRUE problem you want to solve with this feature?',
|
|
439
|
+
required: true,
|
|
440
|
+
followUp: '具体的なシナリオがあれば教えてください。',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
id: 'targetUser',
|
|
444
|
+
questionJa: 'この機能を最も必要としているのは誰ですか?(ユーザー種別)',
|
|
445
|
+
questionEn: 'Who needs this feature the most? (user type)',
|
|
446
|
+
required: true,
|
|
447
|
+
followUp: 'そのユーザーの技術レベルは?',
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
id: 'successState',
|
|
451
|
+
questionJa: 'もしこの機能が完璧に動作したら、何が変わりますか?',
|
|
452
|
+
questionEn: 'If this feature works perfectly, what changes?',
|
|
453
|
+
required: true,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: 'constraints',
|
|
457
|
+
questionJa: 'この機能で「絶対にやってはいけないこと」はありますか?',
|
|
458
|
+
questionEn: 'Are there any things this feature must NEVER do?',
|
|
459
|
+
required: true,
|
|
460
|
+
followUp: 'セキュリティやパフォーマンスの制約は?',
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'successCriteria',
|
|
464
|
+
questionJa: 'この機能が「成功した」と言えるのはどんな状態ですか?',
|
|
465
|
+
questionEn: 'What state indicates this feature is "successful"?',
|
|
466
|
+
required: true,
|
|
467
|
+
followUp: '測定可能な指標はありますか?',
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
/**
|
|
471
|
+
* Ask a single question and wait for response
|
|
472
|
+
*/
|
|
473
|
+
async function askQuestion(rl, question) {
|
|
474
|
+
return new Promise((resolve) => {
|
|
475
|
+
rl.question(question, (answer) => {
|
|
476
|
+
resolve(answer.trim());
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Read all input lines from pipe (non-TTY mode)
|
|
482
|
+
*/
|
|
483
|
+
async function readPipeInput() {
|
|
484
|
+
return new Promise((resolve) => {
|
|
485
|
+
const lines = [];
|
|
486
|
+
const rl = createInterface({
|
|
487
|
+
input: process.stdin,
|
|
488
|
+
output: process.stdout,
|
|
489
|
+
terminal: false,
|
|
490
|
+
});
|
|
491
|
+
rl.on('line', (line) => {
|
|
492
|
+
lines.push(line.trim());
|
|
493
|
+
});
|
|
494
|
+
rl.on('close', () => {
|
|
495
|
+
resolve(lines);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Run interactive hearing session
|
|
501
|
+
* Supports both TTY (interactive) and pipe (batch) input
|
|
502
|
+
*/
|
|
503
|
+
async function runInteractiveHearing(feature, outputPath, globalOpts) {
|
|
504
|
+
const isTTY = process.stdin.isTTY;
|
|
505
|
+
const context = {
|
|
506
|
+
feature,
|
|
507
|
+
purpose: '',
|
|
508
|
+
targetUser: '',
|
|
509
|
+
successState: '',
|
|
510
|
+
constraints: '',
|
|
511
|
+
successCriteria: '',
|
|
512
|
+
additionalContext: [],
|
|
513
|
+
};
|
|
514
|
+
if (!isTTY) {
|
|
515
|
+
// Pipe input mode - read all lines at once
|
|
516
|
+
if (!globalOpts.quiet) {
|
|
517
|
+
console.log('📋 MUSUBIX Requirements - Batch Input Mode');
|
|
518
|
+
console.log(`🎯 Feature: ${feature}\n`);
|
|
519
|
+
}
|
|
520
|
+
const lines = await readPipeInput();
|
|
521
|
+
// Map lines to questions (5 main questions + additional)
|
|
522
|
+
HEARING_QUESTIONS.forEach((q, i) => {
|
|
523
|
+
if (lines[i]) {
|
|
524
|
+
context[q.id] = lines[i];
|
|
525
|
+
if (!globalOpts.quiet) {
|
|
526
|
+
console.log(`✓ ${q.id}: ${lines[i]}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
// Remaining lines are additional context
|
|
531
|
+
for (let i = HEARING_QUESTIONS.length; i < lines.length; i++) {
|
|
532
|
+
if (lines[i]) {
|
|
533
|
+
context.additionalContext.push(lines[i]);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
// TTY mode - interactive questioning
|
|
539
|
+
const rl = createInterface({
|
|
540
|
+
input: process.stdin,
|
|
541
|
+
output: process.stdout,
|
|
542
|
+
});
|
|
543
|
+
console.log('\n' + '═'.repeat(60));
|
|
544
|
+
console.log('📋 MUSUBIX Requirements - Interactive Hearing');
|
|
545
|
+
console.log('═'.repeat(60));
|
|
546
|
+
console.log(`\n🎯 Feature: ${feature}\n`);
|
|
547
|
+
console.log('1問1答で要件を明確化します。');
|
|
548
|
+
console.log('空欄でEnterを押すとスキップします。\n');
|
|
549
|
+
console.log('─'.repeat(60) + '\n');
|
|
550
|
+
try {
|
|
551
|
+
// Ask each question one by one
|
|
552
|
+
for (let i = 0; i < HEARING_QUESTIONS.length; i++) {
|
|
553
|
+
const q = HEARING_QUESTIONS[i];
|
|
554
|
+
const questionNum = `[${i + 1}/${HEARING_QUESTIONS.length}]`;
|
|
555
|
+
console.log(`${questionNum} ${q.questionJa}`);
|
|
556
|
+
console.log(` (${q.questionEn})`);
|
|
557
|
+
const answer = await askQuestion(rl, '\n→ あなたの回答: ');
|
|
558
|
+
if (answer) {
|
|
559
|
+
context[q.id] = answer;
|
|
560
|
+
console.log(`✓ 記録しました\n`);
|
|
561
|
+
// Ask follow-up if provided and answer was given
|
|
562
|
+
if (q.followUp) {
|
|
563
|
+
console.log(` 補足質問: ${q.followUp}`);
|
|
564
|
+
const followUpAnswer = await askQuestion(rl, ' → (任意): ');
|
|
565
|
+
if (followUpAnswer) {
|
|
566
|
+
context.additionalContext.push(`${q.id}: ${followUpAnswer}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else if (q.required) {
|
|
571
|
+
console.log('⚠️ この質問は重要ですが、後で追記できます。\n');
|
|
572
|
+
}
|
|
573
|
+
console.log('');
|
|
574
|
+
}
|
|
575
|
+
// Ask for additional requirements
|
|
576
|
+
console.log('─'.repeat(60));
|
|
577
|
+
console.log('\n追加の要件や考慮事項はありますか?(完了するには空欄でEnter)\n');
|
|
578
|
+
let additionalCount = 1;
|
|
579
|
+
while (true) {
|
|
580
|
+
const additional = await askQuestion(rl, `追加要件 ${additionalCount}: `);
|
|
581
|
+
if (!additional)
|
|
582
|
+
break;
|
|
583
|
+
context.additionalContext.push(additional);
|
|
584
|
+
additionalCount++;
|
|
585
|
+
}
|
|
586
|
+
rl.close();
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
rl.close();
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// Generate EARS requirements from context
|
|
594
|
+
if (!globalOpts.quiet) {
|
|
595
|
+
console.log('\n' + '═'.repeat(60));
|
|
596
|
+
console.log('📝 EARS要件を生成中...');
|
|
597
|
+
console.log('═'.repeat(60) + '\n');
|
|
598
|
+
}
|
|
599
|
+
const requirements = generateEARSFromContext(context);
|
|
600
|
+
const document = generateRequirementsDocument(context, requirements);
|
|
601
|
+
// Determine output path
|
|
602
|
+
const finalOutputPath = outputPath ?? join('storage', 'specs', `REQ-${feature.toUpperCase()}-001.md`);
|
|
603
|
+
const fullPath = resolve(process.cwd(), finalOutputPath);
|
|
604
|
+
// Ensure directory exists
|
|
605
|
+
const dir = resolve(fullPath, '..');
|
|
606
|
+
await mkdir(dir, { recursive: true });
|
|
607
|
+
// Write document
|
|
608
|
+
await writeFile(fullPath, document, 'utf-8');
|
|
609
|
+
console.log(`✅ 要件ドキュメントを生成しました: ${finalOutputPath}`);
|
|
610
|
+
console.log(`\n生成された要件数: ${requirements.length}\n`);
|
|
611
|
+
// Show preview
|
|
612
|
+
if (!globalOpts.quiet) {
|
|
613
|
+
console.log('─'.repeat(60));
|
|
614
|
+
console.log('📄 プレビュー:\n');
|
|
615
|
+
console.log(requirements.map((r, i) => ` ${i + 1}. ${r.text.substring(0, 70)}...`).join('\n'));
|
|
616
|
+
console.log('\n' + '─'.repeat(60));
|
|
617
|
+
}
|
|
618
|
+
// Automatic requirements review (Article IV & IX compliance)
|
|
619
|
+
console.log('\n' + '═'.repeat(60));
|
|
620
|
+
console.log('🔍 要件レビューを実行中...');
|
|
621
|
+
console.log('═'.repeat(60) + '\n');
|
|
622
|
+
const reviewResult = await reviewRequirements(requirements, context);
|
|
623
|
+
displayReviewResult(reviewResult, globalOpts.quiet ?? false);
|
|
624
|
+
// Save review result
|
|
625
|
+
const reviewPath = finalOutputPath.replace('.md', '-REVIEW.md');
|
|
626
|
+
await writeFile(resolve(process.cwd(), reviewPath), generateReviewDocument(reviewResult), 'utf-8');
|
|
627
|
+
console.log(`📋 レビュー結果を保存しました: ${reviewPath}`);
|
|
628
|
+
if (globalOpts.json) {
|
|
629
|
+
outputResult({
|
|
630
|
+
success: true,
|
|
631
|
+
outputPath: finalOutputPath,
|
|
632
|
+
requirements: requirements.length,
|
|
633
|
+
context,
|
|
634
|
+
review: reviewResult,
|
|
635
|
+
}, globalOpts);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Generate EARS requirements from hearing context
|
|
640
|
+
*/
|
|
641
|
+
function generateEARSFromContext(context) {
|
|
642
|
+
const requirements = [];
|
|
643
|
+
const feature = context.feature.toLowerCase();
|
|
644
|
+
let reqIndex = 1;
|
|
645
|
+
const genId = () => `REQ-${context.feature.toUpperCase()}-${String(reqIndex++).padStart(3, '0')}`;
|
|
646
|
+
// Generate from purpose (WHY) - Core functionality
|
|
647
|
+
if (context.purpose) {
|
|
648
|
+
requirements.push({
|
|
649
|
+
id: genId(),
|
|
650
|
+
pattern: 'ubiquitous',
|
|
651
|
+
text: `The system SHALL provide ${feature} functionality to ${context.purpose}`,
|
|
652
|
+
priority: 'P0',
|
|
653
|
+
rationale: `Core purpose: ${context.purpose}`,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
// Generate from target user (WHO) - User-centric requirements
|
|
657
|
+
if (context.targetUser) {
|
|
658
|
+
requirements.push({
|
|
659
|
+
id: genId(),
|
|
660
|
+
pattern: 'event-driven',
|
|
661
|
+
text: `WHEN ${context.targetUser} accesses the ${feature}, the system SHALL respond within acceptable time`,
|
|
662
|
+
priority: 'P1',
|
|
663
|
+
rationale: `Target user: ${context.targetUser}`,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
// Generate from success state (WHAT-IF) - Expected outcomes
|
|
667
|
+
if (context.successState) {
|
|
668
|
+
requirements.push({
|
|
669
|
+
id: genId(),
|
|
670
|
+
pattern: 'state-driven',
|
|
671
|
+
text: `WHILE the ${feature} is active, the system SHALL maintain ${context.successState}`,
|
|
672
|
+
priority: 'P1',
|
|
673
|
+
rationale: `Success state: ${context.successState}`,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
// Generate from constraints - Unwanted behaviors
|
|
677
|
+
if (context.constraints) {
|
|
678
|
+
const constraints = context.constraints.split(/[,、。]/);
|
|
679
|
+
for (const constraint of constraints) {
|
|
680
|
+
if (constraint.trim()) {
|
|
681
|
+
requirements.push({
|
|
682
|
+
id: genId(),
|
|
683
|
+
pattern: 'unwanted',
|
|
684
|
+
text: `IF ${constraint.trim()}, THEN the system SHALL prevent this behavior and notify the user`,
|
|
685
|
+
priority: 'P0',
|
|
686
|
+
rationale: `Constraint: ${constraint.trim()}`,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Generate from success criteria - Validation requirements
|
|
692
|
+
if (context.successCriteria) {
|
|
693
|
+
requirements.push({
|
|
694
|
+
id: genId(),
|
|
695
|
+
pattern: 'ubiquitous',
|
|
696
|
+
text: `The system SHALL meet the following success criteria for ${feature}: ${context.successCriteria}`,
|
|
697
|
+
priority: 'P1',
|
|
698
|
+
rationale: `Success criteria: ${context.successCriteria}`,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
// Generate from additional context
|
|
702
|
+
for (const additional of context.additionalContext) {
|
|
703
|
+
if (additional && !additional.includes(':')) {
|
|
704
|
+
requirements.push({
|
|
705
|
+
id: genId(),
|
|
706
|
+
pattern: 'optional',
|
|
707
|
+
text: `WHERE applicable, the system SHALL ${additional}`,
|
|
708
|
+
priority: 'P2',
|
|
709
|
+
rationale: 'Additional requirement from hearing',
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return requirements;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Generate requirements document in Markdown format
|
|
717
|
+
*/
|
|
718
|
+
function generateRequirementsDocument(context, requirements) {
|
|
719
|
+
const now = new Date().toISOString().split('T')[0];
|
|
720
|
+
let doc = `# REQ-${context.feature.toUpperCase()}-001: ${context.feature} Requirements
|
|
721
|
+
|
|
722
|
+
> Generated by MUSUBIX Interactive Hearing v${VERSION}
|
|
723
|
+
> Date: ${now}
|
|
724
|
+
|
|
725
|
+
## 1. Overview
|
|
726
|
+
|
|
727
|
+
### 1.1 Feature
|
|
728
|
+
**${context.feature}**
|
|
729
|
+
|
|
730
|
+
### 1.2 Purpose (WHY)
|
|
731
|
+
${context.purpose || '_Not specified_'}
|
|
732
|
+
|
|
733
|
+
### 1.3 Target User (WHO)
|
|
734
|
+
${context.targetUser || '_Not specified_'}
|
|
735
|
+
|
|
736
|
+
### 1.4 Success State (WHAT-IF)
|
|
737
|
+
${context.successState || '_Not specified_'}
|
|
738
|
+
|
|
739
|
+
### 1.5 Constraints
|
|
740
|
+
${context.constraints || '_Not specified_'}
|
|
741
|
+
|
|
742
|
+
### 1.6 Success Criteria
|
|
743
|
+
${context.successCriteria || '_Not specified_'}
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## 2. EARS Requirements
|
|
748
|
+
|
|
749
|
+
| ID | Pattern | Priority | Requirement |
|
|
750
|
+
|----|---------|----------|-------------|
|
|
751
|
+
${requirements.map(r => `| ${r.id} | ${r.pattern} | ${r.priority} | ${r.text.substring(0, 60)}... |`).join('\n')}
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
## 3. Detailed Requirements
|
|
756
|
+
|
|
757
|
+
`;
|
|
758
|
+
for (const req of requirements) {
|
|
759
|
+
doc += `### ${req.id}
|
|
760
|
+
|
|
761
|
+
**Pattern**: ${req.pattern}
|
|
762
|
+
**Priority**: ${req.priority}
|
|
763
|
+
|
|
764
|
+
> ${req.text}
|
|
765
|
+
|
|
766
|
+
*Rationale*: ${req.rationale}
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
`;
|
|
771
|
+
}
|
|
772
|
+
doc += `## 4. Additional Context
|
|
773
|
+
|
|
774
|
+
${context.additionalContext.length > 0 ? context.additionalContext.map(c => `- ${c}`).join('\n') : '_No additional context_'}
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
## 5. Traceability
|
|
779
|
+
|
|
780
|
+
| Requirement | Design | Test |
|
|
781
|
+
|-------------|--------|------|
|
|
782
|
+
${requirements.map(r => `| ${r.id} | DES-${context.feature.toUpperCase()}-??? | TST-${context.feature.toUpperCase()}-??? |`).join('\n')}
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
**Generated by**: MUSUBIX v${VERSION}
|
|
787
|
+
**Hearing Date**: ${now}
|
|
788
|
+
`;
|
|
789
|
+
return doc;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Generate minimal template without interaction
|
|
793
|
+
*/
|
|
794
|
+
async function generateMinimalTemplate(feature, outputPath, globalOpts) {
|
|
795
|
+
const now = new Date().toISOString().split('T')[0];
|
|
796
|
+
const fullPath = resolve(process.cwd(), outputPath);
|
|
797
|
+
const template = `# REQ-${feature.toUpperCase()}-001: ${feature} Requirements
|
|
798
|
+
|
|
799
|
+
> Generated by MUSUBIX v${VERSION}
|
|
800
|
+
> Date: ${now}
|
|
801
|
+
|
|
802
|
+
## 1. Overview
|
|
803
|
+
|
|
804
|
+
### 1.1 Feature
|
|
805
|
+
**${feature}**
|
|
806
|
+
|
|
807
|
+
### 1.2 Purpose (WHY)
|
|
808
|
+
_TODO: Define the purpose_
|
|
809
|
+
|
|
810
|
+
### 1.3 Target User (WHO)
|
|
811
|
+
_TODO: Define target users_
|
|
812
|
+
|
|
813
|
+
### 1.4 Success Criteria
|
|
814
|
+
_TODO: Define success criteria_
|
|
815
|
+
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
## 2. EARS Requirements
|
|
819
|
+
|
|
820
|
+
### REQ-${feature.toUpperCase()}-001 (P0)
|
|
821
|
+
|
|
822
|
+
**Pattern**: ubiquitous
|
|
823
|
+
|
|
824
|
+
> The system SHALL [TODO: define core functionality]
|
|
825
|
+
|
|
826
|
+
---
|
|
827
|
+
|
|
828
|
+
### REQ-${feature.toUpperCase()}-002 (P1)
|
|
829
|
+
|
|
830
|
+
**Pattern**: event-driven
|
|
831
|
+
|
|
832
|
+
> WHEN [TODO: trigger event], the system SHALL [TODO: define response]
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
### REQ-${feature.toUpperCase()}-003 (P1)
|
|
837
|
+
|
|
838
|
+
**Pattern**: state-driven
|
|
839
|
+
|
|
840
|
+
> WHILE [TODO: state condition], the system SHALL [TODO: define behavior]
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## 3. Traceability
|
|
845
|
+
|
|
846
|
+
| Requirement | Design | Test |
|
|
847
|
+
|-------------|--------|------|
|
|
848
|
+
| REQ-${feature.toUpperCase()}-001 | DES-${feature.toUpperCase()}-??? | TST-${feature.toUpperCase()}-??? |
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
**Generated by**: MUSUBIX v${VERSION}
|
|
853
|
+
`;
|
|
854
|
+
const dir = resolve(fullPath, '..');
|
|
855
|
+
await mkdir(dir, { recursive: true });
|
|
856
|
+
await writeFile(fullPath, template, 'utf-8');
|
|
857
|
+
if (!globalOpts.quiet) {
|
|
858
|
+
console.log(`✅ Template created: ${outputPath}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* EARS pattern keywords for validation
|
|
863
|
+
*/
|
|
864
|
+
const EARS_KEYWORDS = {
|
|
865
|
+
ubiquitous: ['SHALL', 'system'],
|
|
866
|
+
'event-driven': ['WHEN', 'SHALL'],
|
|
867
|
+
'state-driven': ['WHILE', 'SHALL'],
|
|
868
|
+
unwanted: ['SHALL NOT', 'IF', 'THEN'],
|
|
869
|
+
optional: ['IF', 'THEN', 'SHALL'],
|
|
870
|
+
};
|
|
871
|
+
/**
|
|
872
|
+
* Review requirements for quality and completeness
|
|
873
|
+
*/
|
|
874
|
+
async function reviewRequirements(requirements, context) {
|
|
875
|
+
const findings = [];
|
|
876
|
+
const recommendations = [];
|
|
877
|
+
let passedChecks = 0;
|
|
878
|
+
let totalChecks = 0;
|
|
879
|
+
// 1. Check EARS format compliance (Article IV)
|
|
880
|
+
totalChecks++;
|
|
881
|
+
let earsCompliant = true;
|
|
882
|
+
for (const req of requirements) {
|
|
883
|
+
const keywords = EARS_KEYWORDS[req.pattern];
|
|
884
|
+
if (keywords) {
|
|
885
|
+
const hasKeywords = keywords.every(kw => req.text.toUpperCase().includes(kw.toUpperCase()));
|
|
886
|
+
if (!hasKeywords) {
|
|
887
|
+
earsCompliant = false;
|
|
888
|
+
findings.push({
|
|
889
|
+
severity: 'error',
|
|
890
|
+
category: 'ears',
|
|
891
|
+
requirement: req.id,
|
|
892
|
+
message: `EARS pattern '${req.pattern}' requires keywords: ${keywords.join(', ')}`,
|
|
893
|
+
suggestion: `Rewrite requirement to include: ${keywords.join(', ')}`,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (earsCompliant)
|
|
899
|
+
passedChecks++;
|
|
900
|
+
// 2. Check completeness (all 5Ws covered)
|
|
901
|
+
totalChecks++;
|
|
902
|
+
const completenessIssues = [];
|
|
903
|
+
if (!context.purpose)
|
|
904
|
+
completenessIssues.push('WHY (purpose)');
|
|
905
|
+
if (!context.targetUser)
|
|
906
|
+
completenessIssues.push('WHO (target user)');
|
|
907
|
+
if (!context.successState)
|
|
908
|
+
completenessIssues.push('WHAT-IF (success state)');
|
|
909
|
+
if (!context.constraints)
|
|
910
|
+
completenessIssues.push('CONSTRAINTS');
|
|
911
|
+
if (!context.successCriteria)
|
|
912
|
+
completenessIssues.push('SUCCESS CRITERIA');
|
|
913
|
+
if (completenessIssues.length > 0) {
|
|
914
|
+
findings.push({
|
|
915
|
+
severity: completenessIssues.length > 2 ? 'error' : 'warning',
|
|
916
|
+
category: 'completeness',
|
|
917
|
+
message: `Missing context: ${completenessIssues.join(', ')}`,
|
|
918
|
+
suggestion: 'Run interactive hearing again to fill missing information',
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
passedChecks++;
|
|
923
|
+
}
|
|
924
|
+
// 3. Check testability
|
|
925
|
+
totalChecks++;
|
|
926
|
+
let allTestable = true;
|
|
927
|
+
for (const req of requirements) {
|
|
928
|
+
const hasVagueTerms = /always|never|all|any|best|optimal|fast|slow|many|few/i.test(req.text);
|
|
929
|
+
if (hasVagueTerms) {
|
|
930
|
+
allTestable = false;
|
|
931
|
+
findings.push({
|
|
932
|
+
severity: 'warning',
|
|
933
|
+
category: 'testability',
|
|
934
|
+
requirement: req.id,
|
|
935
|
+
message: 'Contains vague terms that may not be testable',
|
|
936
|
+
suggestion: 'Replace vague terms with specific, measurable criteria',
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (allTestable)
|
|
941
|
+
passedChecks++;
|
|
942
|
+
// 4. Check priority distribution
|
|
943
|
+
totalChecks++;
|
|
944
|
+
const priorityCounts = {
|
|
945
|
+
P0: requirements.filter(r => r.priority === 'P0').length,
|
|
946
|
+
P1: requirements.filter(r => r.priority === 'P1').length,
|
|
947
|
+
P2: requirements.filter(r => r.priority === 'P2').length,
|
|
948
|
+
};
|
|
949
|
+
if (priorityCounts.P0 === 0) {
|
|
950
|
+
findings.push({
|
|
951
|
+
severity: 'error',
|
|
952
|
+
category: 'completeness',
|
|
953
|
+
message: 'No P0 (must-have) requirements defined',
|
|
954
|
+
suggestion: 'Ensure at least one core requirement is marked as P0',
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
else if (priorityCounts.P0 > requirements.length * 0.5) {
|
|
958
|
+
findings.push({
|
|
959
|
+
severity: 'warning',
|
|
960
|
+
category: 'consistency',
|
|
961
|
+
message: `Too many P0 requirements (${priorityCounts.P0}/${requirements.length})`,
|
|
962
|
+
suggestion: 'Review priorities - not everything can be must-have',
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
passedChecks++;
|
|
967
|
+
}
|
|
968
|
+
// 5. Check minimum requirements count
|
|
969
|
+
totalChecks++;
|
|
970
|
+
if (requirements.length < 3) {
|
|
971
|
+
findings.push({
|
|
972
|
+
severity: 'warning',
|
|
973
|
+
category: 'completeness',
|
|
974
|
+
message: `Only ${requirements.length} requirements defined`,
|
|
975
|
+
suggestion: 'Consider if more requirements are needed for complete coverage',
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
passedChecks++;
|
|
980
|
+
}
|
|
981
|
+
// 6. Check for unique IDs
|
|
982
|
+
totalChecks++;
|
|
983
|
+
const ids = requirements.map(r => r.id);
|
|
984
|
+
const duplicateIds = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
985
|
+
if (duplicateIds.length > 0) {
|
|
986
|
+
findings.push({
|
|
987
|
+
severity: 'error',
|
|
988
|
+
category: 'consistency',
|
|
989
|
+
requirement: duplicateIds[0],
|
|
990
|
+
message: `Duplicate requirement ID: ${duplicateIds[0]}`,
|
|
991
|
+
suggestion: 'Ensure all requirement IDs are unique',
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
passedChecks++;
|
|
996
|
+
}
|
|
997
|
+
// Generate recommendations
|
|
998
|
+
if (findings.filter(f => f.severity === 'error').length > 0) {
|
|
999
|
+
recommendations.push('⚠️ Address error-level findings before proceeding to design');
|
|
1000
|
+
}
|
|
1001
|
+
if (findings.filter(f => f.category === 'testability').length > 0) {
|
|
1002
|
+
recommendations.push('📝 Make requirements more specific and measurable');
|
|
1003
|
+
}
|
|
1004
|
+
if (completenessIssues.length > 0) {
|
|
1005
|
+
recommendations.push('🔄 Re-run interactive hearing to complete missing context');
|
|
1006
|
+
}
|
|
1007
|
+
if (requirements.length >= 3 && findings.filter(f => f.severity === 'error').length === 0) {
|
|
1008
|
+
recommendations.push('✅ Requirements are ready for design phase');
|
|
1009
|
+
}
|
|
1010
|
+
const score = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0;
|
|
1011
|
+
return {
|
|
1012
|
+
passed: findings.filter(f => f.severity === 'error').length === 0,
|
|
1013
|
+
score,
|
|
1014
|
+
totalChecks,
|
|
1015
|
+
passedChecks,
|
|
1016
|
+
findings,
|
|
1017
|
+
recommendations,
|
|
1018
|
+
constitutionCompliance: {
|
|
1019
|
+
articleIV: earsCompliant,
|
|
1020
|
+
articleV: true, // Traceability section is generated
|
|
1021
|
+
articleIX: score >= 60,
|
|
1022
|
+
},
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Display review result
|
|
1027
|
+
*/
|
|
1028
|
+
function displayReviewResult(result, quiet) {
|
|
1029
|
+
if (quiet)
|
|
1030
|
+
return;
|
|
1031
|
+
const statusIcon = result.passed ? '✅' : '❌';
|
|
1032
|
+
console.log(`${statusIcon} レビュー結果: ${result.score}% (${result.passedChecks}/${result.totalChecks} checks)`);
|
|
1033
|
+
console.log('');
|
|
1034
|
+
// Constitution compliance
|
|
1035
|
+
console.log('📜 憲法準拠状況:');
|
|
1036
|
+
console.log(` Article IV (EARS形式): ${result.constitutionCompliance.articleIV ? '✓' : '✗'}`);
|
|
1037
|
+
console.log(` Article V (トレーサビリティ): ${result.constitutionCompliance.articleV ? '✓' : '✗'}`);
|
|
1038
|
+
console.log(` Article IX (品質ゲート): ${result.constitutionCompliance.articleIX ? '✓' : '✗'}`);
|
|
1039
|
+
console.log('');
|
|
1040
|
+
// Findings
|
|
1041
|
+
if (result.findings.length > 0) {
|
|
1042
|
+
console.log('📋 発見事項:');
|
|
1043
|
+
for (const finding of result.findings) {
|
|
1044
|
+
const icon = finding.severity === 'error' ? '🔴' : finding.severity === 'warning' ? '🟡' : '🔵';
|
|
1045
|
+
console.log(` ${icon} [${finding.category}] ${finding.message}`);
|
|
1046
|
+
if (finding.requirement) {
|
|
1047
|
+
console.log(` 対象: ${finding.requirement}`);
|
|
1048
|
+
}
|
|
1049
|
+
if (finding.suggestion) {
|
|
1050
|
+
console.log(` 💡 ${finding.suggestion}`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
console.log('');
|
|
1054
|
+
}
|
|
1055
|
+
// Recommendations
|
|
1056
|
+
if (result.recommendations.length > 0) {
|
|
1057
|
+
console.log('💡 推奨事項:');
|
|
1058
|
+
for (const rec of result.recommendations) {
|
|
1059
|
+
console.log(` ${rec}`);
|
|
1060
|
+
}
|
|
1061
|
+
console.log('');
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Generate review document
|
|
1066
|
+
*/
|
|
1067
|
+
function generateReviewDocument(result) {
|
|
1068
|
+
const now = new Date().toISOString().split('T')[0];
|
|
1069
|
+
let doc = `# Requirements Review Report
|
|
1070
|
+
|
|
1071
|
+
> Generated by MUSUBIX v${VERSION}
|
|
1072
|
+
> Date: ${now}
|
|
1073
|
+
|
|
1074
|
+
## Summary
|
|
1075
|
+
|
|
1076
|
+
| Metric | Value |
|
|
1077
|
+
|--------|-------|
|
|
1078
|
+
| **Status** | ${result.passed ? '✅ PASSED' : '❌ FAILED'} |
|
|
1079
|
+
| **Score** | ${result.score}% |
|
|
1080
|
+
| **Checks Passed** | ${result.passedChecks}/${result.totalChecks} |
|
|
1081
|
+
|
|
1082
|
+
## Constitution Compliance
|
|
1083
|
+
|
|
1084
|
+
| Article | Name | Status |
|
|
1085
|
+
|---------|------|--------|
|
|
1086
|
+
| IV | EARS Format | ${result.constitutionCompliance.articleIV ? '✅ Compliant' : '❌ Non-compliant'} |
|
|
1087
|
+
| V | Traceability | ${result.constitutionCompliance.articleV ? '✅ Compliant' : '❌ Non-compliant'} |
|
|
1088
|
+
| IX | Quality Gates | ${result.constitutionCompliance.articleIX ? '✅ Compliant' : '❌ Non-compliant'} |
|
|
1089
|
+
|
|
1090
|
+
## Findings
|
|
1091
|
+
|
|
1092
|
+
`;
|
|
1093
|
+
if (result.findings.length === 0) {
|
|
1094
|
+
doc += '_No issues found._\n\n';
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
const errors = result.findings.filter(f => f.severity === 'error');
|
|
1098
|
+
const warnings = result.findings.filter(f => f.severity === 'warning');
|
|
1099
|
+
const infos = result.findings.filter(f => f.severity === 'info');
|
|
1100
|
+
if (errors.length > 0) {
|
|
1101
|
+
doc += '### 🔴 Errors\n\n';
|
|
1102
|
+
for (const f of errors) {
|
|
1103
|
+
doc += `- **[${f.category}]** ${f.message}\n`;
|
|
1104
|
+
if (f.requirement)
|
|
1105
|
+
doc += ` - Requirement: ${f.requirement}\n`;
|
|
1106
|
+
if (f.suggestion)
|
|
1107
|
+
doc += ` - 💡 ${f.suggestion}\n`;
|
|
1108
|
+
}
|
|
1109
|
+
doc += '\n';
|
|
1110
|
+
}
|
|
1111
|
+
if (warnings.length > 0) {
|
|
1112
|
+
doc += '### 🟡 Warnings\n\n';
|
|
1113
|
+
for (const f of warnings) {
|
|
1114
|
+
doc += `- **[${f.category}]** ${f.message}\n`;
|
|
1115
|
+
if (f.requirement)
|
|
1116
|
+
doc += ` - Requirement: ${f.requirement}\n`;
|
|
1117
|
+
if (f.suggestion)
|
|
1118
|
+
doc += ` - 💡 ${f.suggestion}\n`;
|
|
1119
|
+
}
|
|
1120
|
+
doc += '\n';
|
|
1121
|
+
}
|
|
1122
|
+
if (infos.length > 0) {
|
|
1123
|
+
doc += '### 🔵 Info\n\n';
|
|
1124
|
+
for (const f of infos) {
|
|
1125
|
+
doc += `- **[${f.category}]** ${f.message}\n`;
|
|
1126
|
+
}
|
|
1127
|
+
doc += '\n';
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
doc += `## Recommendations
|
|
1131
|
+
|
|
1132
|
+
`;
|
|
1133
|
+
for (const rec of result.recommendations) {
|
|
1134
|
+
doc += `- ${rec}\n`;
|
|
1135
|
+
}
|
|
1136
|
+
doc += `
|
|
1137
|
+
---
|
|
1138
|
+
|
|
1139
|
+
**Reviewed by**: MUSUBIX Automated Review System
|
|
1140
|
+
**Review Date**: ${now}
|
|
1141
|
+
`;
|
|
1142
|
+
return doc;
|
|
1143
|
+
}
|
|
402
1144
|
export { parseEARSRequirements };
|
|
403
1145
|
//# sourceMappingURL=requirements.js.map
|