aiwf
Version:
AI Workflow Framework for Claude Code with multi-language support (Korean/English)
328 lines (286 loc) • 9.37 kB
JavaScript
/**
* AIWF Task Scanner
* 프로젝트 디렉토리에서 태스크 파일들을 스캔하고 파싱
*/
import fs from 'fs/promises';
import path from 'path';
import { glob } from 'glob';
export class TaskScanner {
constructor(aiwfPath) {
this.aiwfPath = aiwfPath;
}
/**
* 스프린트 디렉토리 스캔
*/
async scanSprintDirectories() {
try {
const sprintDirs = await glob('03_SPRINTS/S*/', { cwd: this.aiwfPath });
const sprintData = {};
for (const sprintDir of sprintDirs) {
const sprintId = path.basename(sprintDir);
const fullSprintPath = path.join(this.aiwfPath, sprintDir);
// 스프린트 메타 파일 찾기
const metaFiles = await glob('*_meta.md', { cwd: fullSprintPath });
let sprintMeta = {};
if (metaFiles.length > 0) {
const metaPath = path.join(fullSprintPath, metaFiles[0]);
sprintMeta = await this.parseSprintMeta(metaPath);
}
// 태스크 파일들 스캔
const taskFiles = await glob('T*.md', { cwd: fullSprintPath });
const tasks = {};
for (const taskFile of taskFiles) {
const taskPath = path.join(fullSprintPath, taskFile);
const taskData = await this.parseTaskFile(taskPath, sprintId);
if (taskData) {
tasks[taskData.id] = taskData;
}
}
sprintData[sprintId] = {
meta: sprintMeta,
tasks: tasks,
task_count: taskFiles.length,
path: sprintDir
};
}
return sprintData;
} catch (error) {
throw new Error(`Failed to scan sprint directories: ${error.message}`);
}
}
/**
* 일반 태스크 디렉토리 스캔
*/
async scanGeneralTasks() {
try {
const generalTasksPath = path.join(this.aiwfPath, '04_GENERAL_TASKS');
const taskFiles = await glob('*.md', { cwd: generalTasksPath });
const tasks = {};
for (const taskFile of taskFiles) {
const taskPath = path.join(generalTasksPath, taskFile);
const taskData = await this.parseTaskFile(taskPath, 'general');
if (taskData) {
tasks[taskData.id] = taskData;
}
}
return tasks;
} catch (error) {
// 일반 태스크 디렉토리가 없으면 빈 객체 반환
return {};
}
}
/**
* 스프린트 메타 파일 파싱
*/
async parseSprintMeta(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
const meta = {
status: 'planned',
progress: 0,
start_date: null,
end_date: null,
goals: [],
deliverables: []
};
// YAML frontmatter 파싱
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
const frontmatter = frontmatterMatch[1];
// 간단한 YAML 파싱 (key: value 형태)
const lines = frontmatter.split('\n');
for (const line of lines) {
const [key, ...valueParts] = line.split(':');
if (key && valueParts.length > 0) {
const value = valueParts.join(':').trim();
meta[key.trim()] = value;
}
}
}
// 본문에서 목표와 결과물 추출
const bodyContent = content.replace(/^---\n[\s\S]*?\n---\n/, '');
// 목표 섹션 찾기
const goalsMatch = bodyContent.match(/## 목표[^#]*?((?:- .*\n?)*)/i);
if (goalsMatch) {
meta.goals = goalsMatch[1]
.split('\n')
.filter(line => line.trim().startsWith('-'))
.map(line => line.replace(/^-\s*/, '').trim());
}
// 결과물 섹션 찾기
const deliverablesMatch = bodyContent.match(/## 결과물[^#]*?((?:- .*\n?)*)/i);
if (deliverablesMatch) {
meta.deliverables = deliverablesMatch[1]
.split('\n')
.filter(line => line.trim().startsWith('-'))
.map(line => line.replace(/^-\s*/, '').trim());
}
return meta;
} catch (error) {
console.warn(`Failed to parse sprint meta ${filePath}: ${error.message}`);
return {};
}
}
/**
* 태스크 파일 파싱
*/
async parseTaskFile(filePath, sprintId) {
try {
const content = await fs.readFile(filePath, 'utf8');
const fileName = path.basename(filePath, '.md');
const task = {
id: fileName,
sprint_id: sprintId,
title: this.extractTitle(content),
status: this.determineStatus(content),
description: this.extractDescription(content),
tags: this.extractTags(content),
dependencies: this.extractDependencies(content),
assignee: this.extractAssignee(content),
created_date: this.extractCreatedDate(filePath),
updated_date: this.extractUpdatedDate(filePath),
estimated_hours: this.extractEstimatedHours(content),
complexity: this.extractComplexity(content),
type: this.extractTaskType(content),
milestone_id: this.extractMilestoneId(sprintId),
file_path: filePath,
progress_notes: this.extractProgressNotes(content)
};
return task;
} catch (error) {
console.warn(`Failed to parse task file ${filePath}: ${error.message}`);
return null;
}
}
/**
* 태스크 제목 추출
*/
extractTitle(content) {
const titleMatch = content.match(/^#\s+(.+)$/m);
return titleMatch ? titleMatch[1].trim() : 'Untitled Task';
}
/**
* 태스크 상태 결정
*/
determineStatus(content) {
// OUTPUT LOG에서 완료 표시 확인
if (content.includes('## OUTPUT LOG') || content.includes('✅ 완료')) {
const outputSection = content.match(/## OUTPUT LOG([\s\S]*?)(?=##|$)/i);
if (outputSection && outputSection[1].trim().length > 50) {
return 'completed';
}
}
// 진행 중 표시 확인
if (content.includes('🔄') || content.includes('진행 중') || content.includes('in progress')) {
return 'in_progress';
}
// 차단됨 표시 확인
if (content.includes('❌') || content.includes('차단') || content.includes('blocked')) {
return 'blocked';
}
return 'pending';
}
/**
* 태스크 설명 추출
*/
extractDescription(content) {
// 첫 번째 헤더 다음부터 두 번째 헤더 전까지
const descMatch = content.match(/^#[^#\n]*\n([\s\S]*?)(?=\n##|\n#|$)/);
return descMatch ? descMatch[1].trim().substring(0, 200) : '';
}
/**
* 태그 추출
*/
extractTags(content) {
const tagMatches = content.match(/#(\w+)/g);
return tagMatches ? tagMatches.map(tag => tag.substring(1)) : [];
}
/**
* 종속성 추출
*/
extractDependencies(content) {
const depMatches = content.match(/depends on:?\s*([T\d\s,]+)/i);
if (depMatches) {
return depMatches[1]
.split(/[,\s]+/)
.filter(dep => dep.match(/^T\d+$/))
.map(dep => dep.trim());
}
return [];
}
/**
* 담당자 추출
*/
extractAssignee(content) {
const assigneeMatch = content.match(/assignee:?\s*([^\n]+)/i);
return assigneeMatch ? assigneeMatch[1].trim() : null;
}
/**
* 생성일 추출 (파일 생성 시간 기반)
*/
async extractCreatedDate(filePath) {
try {
const stats = await fs.stat(filePath);
return stats.birthtime.toISOString();
} catch {
return new Date().toISOString();
}
}
/**
* 수정일 추출 (파일 수정 시간 기반)
*/
async extractUpdatedDate(filePath) {
try {
const stats = await fs.stat(filePath);
return stats.mtime.toISOString();
} catch {
return new Date().toISOString();
}
}
/**
* 예상 시간 추출
*/
extractEstimatedHours(content) {
const hourMatch = content.match(/estimated:?\s*(\d+\.?\d*)\s*hours?/i);
return hourMatch ? parseFloat(hourMatch[1]) : null;
}
/**
* 복잡도 추출
*/
extractComplexity(content) {
const complexityMatch = content.match(/complexity:?\s*(low|medium|high|very-high)/i);
return complexityMatch ? complexityMatch[1].toLowerCase() : null;
}
/**
* 태스크 타입 추출
*/
extractTaskType(content) {
const typeMatch = content.match(/type:?\s*([^\n]+)/i);
if (typeMatch) {
return typeMatch[1].trim().toLowerCase();
}
// 제목이나 내용에서 타입 추정
const title = this.extractTitle(content).toLowerCase();
if (title.includes('bug') || title.includes('fix')) return 'bug-fix';
if (title.includes('test')) return 'testing';
if (title.includes('doc')) return 'documentation';
if (title.includes('refactor')) return 'refactoring';
if (title.includes('feature')) return 'core-feature';
return 'enhancement';
}
/**
* 마일스톤 ID 추출 (스프린트 ID에서)
*/
extractMilestoneId(sprintId) {
if (sprintId === 'general') return null;
const milestoneMatch = sprintId.match(/S\d+_M(\d+)/);
return milestoneMatch ? `M${milestoneMatch[1].padStart(2, '0')}` : null;
}
/**
* 진행 노트 추출
*/
extractProgressNotes(content) {
const notesMatch = content.match(/## 진행 상황([\s\S]*?)(?=##|$)/i);
return notesMatch ? notesMatch[1].trim() : '';
}
}