@nahisaho/musubix-core 1.0.3 → 1.0.5

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.
@@ -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