#!/usr/bin/env node /** * ドキュメント作成後のチェックフック * * このフックは /create-docs コマンド実行後に自動的に実行され、 * 作成されたドキュメントの完全性と品質をチェックします。 */ const fs = require('fs'); const path = require('path'); // ドキュメントの必須セクション const REQUIRED_SECTIONS = [ '概要', '機能要件', '技術仕様', '非機能要件', 'テスト', ]; // ドキュメントファイルを探す function findDocFiles() { const possiblePaths = [ 'docs/features', 'docs/specs', 'docs', '.', ]; const docFiles = []; for (const dir of possiblePaths) { if (!fs.existsSync(dir)) continue; const files = fs.readdirSync(dir) .filter(f => f.endsWith('.md') && !f.startsWith('README')) .map(f => path.join(dir, f)); docFiles.push(...files); } return docFiles; } // ドキュメントの内容をチェック function checkDocument(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); const issues = []; // 必須セクションのチェック const missingSection = REQUIRED_SECTIONS.filter(section => { const regex = new RegExp(`##?\\s+${section}`, 'i'); return !regex.test(content); }); if (missingSections.length > 0) { issues.push({ type: 'missing_sections', message: `必須セクションが不足しています: ${missingSections.join(', ')}`, }); } // 最小文字数チェック(簡易的な品質チェック) if (content.length < 500) { issues.push({ type: 'too_short', message: 'ドキュメントが短すぎます。より詳細な説明が必要です。', }); } // TODOやFIXMEが残っていないかチェック const todoMatches = content.match(/TODO|FIXME|XXX/g); if (todoMatches && todoMatches.length > 0) { issues.push({ type: 'incomplete', message: `未完了の項目が ${todoMatches.length} 件あります(TODO/FIXME)`, }); } // コードブロックがあるかチェック(技術仕様には必要) if (!content.includes('```')) { issues.push({ type: 'no_code_examples', message: 'コード例やデータ構造の記載がありません', severity: 'warning', }); } return { filePath, issues, passed: issues.filter(i => i.severity !== 'warning').length === 0, }; } // メイン処理 function main() { console.log('📄 ドキュメントの品質チェックを実行中...\n'); const docFiles = findDocFiles(); if (docFiles.length === 0) { console.error('❌ ドキュメントファイルが見つかりません'); console.error(' 以下のディレクトリにMarkdownファイルを作成してください:'); console.error(' - docs/features/'); console.error(' - docs/specs/'); process.exit(1); } console.log(`検出されたドキュメント: ${docFiles.length}件\n`); const results = docFiles.map(checkDocument); const failedDocs = results.filter(r => !r.passed); // 結果の表示 results.forEach(result => { if (result.passed) { console.log(`✅ ${result.filePath}`); } else { console.log(`❌ ${result.filePath}`); result.issues.forEach(issue => { const icon = issue.severity === 'warning' ? '⚠️' : '❌'; console.log(` ${icon} ${issue.message}`); }); } console.log(); }); // サマリー console.log('─'.repeat(50)); console.log(`合計: ${results.length}件`); console.log(`成功: ${results.length - failedDocs.length}件`); console.log(`失敗: ${failedDocs.length}件`); if (failedDocs.length > 0) { console.log('\n⚠️ ドキュメントに問題があります。修正してから次のステップに進んでください。'); process.exit(1); } console.log('\n✅ ドキュメントの品質チェックが完了しました!'); console.log(' 次のステップ: /create-tests を実行してテストケースを作成'); // タスク状態の自動更新 updateTaskStatus(2, 'completed'); } // タスク状態を更新する関数 function updateTaskStatus(taskId, status) { const tasksFile = '.tasks.json'; if (!fs.existsSync(tasksFile)) { console.log('\n⚠️ タスクファイルが見つかりません。タスク状態は更新されませんでした。'); return; } try { const data = JSON.parse(fs.readFileSync(tasksFile, 'utf-8')); const task = data.tasks.find(t => t.id === taskId); if (!task) { console.log(`\n⚠️ タスク #${taskId} が見つかりません。`); return; } task.status = status; task.completedAt = new Date().toISOString(); data.updatedAt = new Date().toISOString(); fs.writeFileSync(tasksFile, JSON.stringify(data, null, 2)); console.log(`\n📋 タスク #${taskId} (${task.name}) を完了としてマークしました`); } catch (error) { console.log(`\n⚠️ タスク状態の更新に失敗しました: ${error.message}`); } } // エラーハンドリング try { main(); } catch (error) { console.error('❌ チェック中にエラーが発生しました:', error.message); process.exit(1); }