@jmkim85/dev-flow-mcp
Version:
MCP-based Dev Flow - AI-powered development workflow management with 13 essential tools for TDD and context management
145 lines (137 loc) • 5.55 kB
JavaScript
/**
* Safety Guards Module
* Provides path validation, file header management, and missing file feedback
*/
import { promises as fs } from 'fs';
import { join } from 'path';
// Find outputs created for a specific stage with new task-based structure
export async function findStageOutputs(stage, taskId, projectRoot) {
const outputs = [];
if (!taskId) {
console.error('❌ No task ID provided for stage output check');
return outputs;
}
try {
// NEW: Task-based folder structure
const taskDir = join(projectRoot, '.devflow', 'tasks', taskId);
// Define expected outputs for each stage with new structure
const stageOutputMap = {
'analyze': [`analysis/analysis.md`, `analysis/requirements.md`],
'design': [`design/architecture.md`, `design/api_design.md`],
'write_tests': [`tests/test_plan.md`, `tests/test_cases.md`],
'implement': [`implementation/implementation.md`, `implementation/code_review.md`],
'refactor': [`refactor/refactor_plan.md`, `refactor/improvements.md`]
};
const expectedOutputs = stageOutputMap[stage] || [];
// Check if each expected output exists in task directory
for (const output of expectedOutputs) {
try {
const outputPath = join(taskDir, output);
await fs.access(outputPath);
outputs.push(output);
console.error(`✅ Found stage output: ${output} in task ${taskId}`);
}
catch {
console.error(`❌ Missing stage output: ${output} in task ${taskId}`);
}
}
// Also check for actual test files if stage is write_tests
if (stage === 'write_tests') {
try {
const testDirs = ['test', 'tests', '__tests__', 'spec'];
for (const testDir of testDirs) {
try {
const testDirPath = join(projectRoot, testDir);
const stats = await fs.stat(testDirPath);
if (stats.isDirectory()) {
const testFiles = await fs.readdir(testDirPath);
if (testFiles.length > 0) {
outputs.push('test files');
console.error(`✅ Found test files in ${testDir}/`);
break;
}
}
}
catch {
// Continue checking other test directories
}
}
}
catch {
// No test files found
}
}
}
catch (error) {
console.error(`Warning: Could not check stage outputs: ${error}`);
}
return outputs;
}
// 안전장치 3-2: 경로 화이트리스트 검증
export function validateTaskPath(filePath, taskId) {
const allowedPaths = [
`.devflow/tasks/${taskId}/**`,
`.devflow/project/**`,
`.devflow/workflows/**`
];
// Normalize path separators
const normalizedPath = filePath.replace(/\\/g, '/');
// Check if path matches any allowed pattern
for (const pattern of allowedPaths) {
const regexPattern = pattern
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\./g, '\\.');
const regex = new RegExp(`^${regexPattern}$`);
if (regex.test(normalizedPath)) {
return { valid: true };
}
}
return {
valid: false,
reason: `Path '${filePath}' is not allowed. Use task-specific paths: .devflow/tasks/${taskId}/`
};
}
// 안전장치 3-3: 헤더 자동 보정
export function ensureFileHeader(content, filePath) {
const headerPattern = /^#\s+file:\s+/;
if (!headerPattern.test(content)) {
return `# file: ${filePath}\n\n${content}`;
}
return content;
}
// 안전장치 3-4: 누락 파일 피드백 생성
export async function generateMissingFilesFeedback(stage, taskId, projectRoot) {
const outputsCreated = await findStageOutputs(stage, taskId, projectRoot);
const stageOutputMap = {
'analyze': [`analysis/analysis.md`, `analysis/requirements.md`],
'design': [`design/architecture.md`, `design/api_design.md`],
'write_tests': [`tests/test_plan.md`, `tests/test_cases.md`],
'implement': [`implementation/implementation.md`, `implementation/code_review.md`],
'refactor': [`refactor/refactor_plan.md`, `refactor/improvements.md`]
};
const expectedOutputs = stageOutputMap[stage] || [];
const missingFiles = expectedOutputs.filter(file => !outputsCreated.includes(file));
if (missingFiles.length === 0) {
return '';
}
const taskDir = `.devflow/tasks/${taskId}`;
const missingPaths = missingFiles.map(file => `${taskDir}/${file}`);
return `
📝 **Missing Required Files**
Please create the following files before advancing to the next stage:
${missingPaths.map(path => `- \`${path}\``).join('\n')}
**Example for analysis stage:**
\`\`\`markdown
# file: .devflow/tasks/${taskId}/analysis/analysis.md
# Analysis for Task ${taskId}
## Problem Statement
[Describe the problem to solve]
## Requirements
[List functional and non-functional requirements]
## Approach
[Outline your solution approach]
\`\`\`
Create these files and try advancing again.`;
}
//# sourceMappingURL=safety-guards.js.map