# markdown-parsing > 마크다운 계획서, 로드맵, 커맨드 파일의 파싱 및 섹션 교체 로직을 구현합니다. 파서 작성, 정규식 파싱, 마크다운 처리, Fallback 체인, 섹션 교체 요청 시 사용합니다. - Author: yachaboom - Repository: viilab/vibe-commander - Version: 20260207190114 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-07 - Source: https://github.com/viilab/vibe-commander - Web: https://mule.run/skillshub/@@viilab/vibe-commander~markdown-parsing:20260207190114 --- --- name: markdown-parsing description: 마크다운 계획서, 로드맵, 커맨드 파일의 파싱 및 섹션 교체 로직을 구현합니다. 파서 작성, 정규식 파싱, 마크다운 처리, Fallback 체인, 섹션 교체 요청 시 사용합니다. metadata: version: 1.0.0 category: core tags: [markdown, parsing, regex, fallback, template] --- # Markdown Parsing Strategy vibe-commander는 반정형 마크다운 문서를 **정규식 + 라인 기반**으로 파싱합니다. AST 파서(remark/unified)는 사용하지 않습니다 — 구조가 예측 가능하므로 과잉입니다. ## Instructions ### Step 1: 파싱 대상 식별 | 대상 | 파일 | 추출 항목 | |------|------|----------| | 계획서 | `unit-plans/*.md` | 제목, 의존성, 페어링 질문 | | 로드맵 | `roadmap.md` | 유닛 목록, 상태, 의존 관계 | | 커맨드 파일 | `commands.md` | 섹션 범위, 기존 내용 | ### Step 2: 설정 기반 파싱 전략 선택 파싱 방법은 `planParsing` 설정에 따라 결정됩니다. ```typescript // 제목 추출 전략 switch (schema.titleSource) { case 'h1': return extractH1Title(content); case 'frontmatter:title': return extractFrontmatterField(content, 'title'); } // 의존성 추출 전략 switch (schema.dependsSource) { case 'section': return extractFromSection(content, schema.dependsSectionName); case 'frontmatter': return extractFromFrontmatter(content, 'depends'); case 'metadata-table': return extractFromTable(content, schema.metadataTable); } ``` ### Step 3: Fallback 체인 구현 설정된 전략으로 찾지 못하면 다음 순서로 시도합니다. ``` 1. 설정된 소스 (planParsing.dependsSource) 2. 메타데이터 테이블 스캔 3. 본문 정규식 전체 스캔 4. 부분 결과 반환 (찾은 것만) ``` CRITICAL: 파싱 실패 시 전체 에러가 아닌 **부분 결과 + 실패 필드 명시**로 반환합니다. ### Step 4: 커맨드 파일 섹션 교체 ``` 1. commandSection 헤더(예: "# 유닛 구현")로 섹션 시작점 탐색 2. 다음 separator("---...---") 또는 다음 commandSection 헤더까지가 섹션 범위 3. 범위 내 콘텐츠만 교체, 나머지 보존 ``` ## Rules ### 정규식 패턴 작성 원칙 **Do ✅**: ```typescript // 명명된 캡처 그룹 사용 — 가독성 확보 const BACKLOG_ENTRY = /ID=\[(?[^\]]+)\]\((?[^)]+)\)\s*\|\s*(?[^|]+)\|\s*Depends=(?<deps>[^|]*)\|\s*(?<status>.+)/; const match = line.match(BACKLOG_ENTRY); if (match?.groups) { const { id, title, deps, status } = match.groups; } ``` **Don't ❌**: ```typescript // 인덱스 기반 캡처 — 유지보수 불가 const match = line.match(/\[([^\]]+)\]\(([^)]+)\)\s*\|\s*([^|]+)\|\s*([^|]*)\|\s*(.+)/); const id = match?.[1]; // ❌ 매직 넘버 const title = match?.[3]; // ❌ 순서 변경 시 깨짐 ``` ### 라인 기반 파싱 패턴 **Do ✅**: ```typescript // 라인 단위 순회 — 상태 머신 패턴 function extractSection(content: string, sectionName: string): string[] { const lines = content.split('\n'); let inSection = false; const result: string[] = []; for (const line of lines) { if (line.startsWith(`## ${sectionName}`)) { inSection = true; continue; } if (inSection && line.startsWith('## ')) { break; // 다음 섹션 시작 → 종료 } if (inSection) { result.push(line); } } return result; } ``` **Don't ❌**: ```typescript // 전체 문서 정규식 매칭 — 복잡하고 디버깅 어려움 const section = content.match( /## 이전 작업에서 가져올 것\n([\s\S]*?)(?=\n## |\n---|\z)/ )?.[1]; // ❌ 복잡한 lookahead, 엣지 케이스 다수 ``` ### 부분 결과 반환 (Graceful Degradation) **Do ✅**: ```typescript // 찾은 것만 반환, 실패 필드 명시 function parseUnitPlan(content: string, schema: PlanParsingConfig): ParseResult { const title = extractTitle(content, schema); const depends = extractDeps(content, schema); const questions = extractPairingQuestions(content, schema); return { success: true, data: { title, depends, questions }, warnings: [ ...(!title ? ['제목 추출 실패: titleSource 설정 확인'] : []), ...(depends.length === 0 ? ['의존성 미발견: dependsSource 설정 확인'] : []), ], }; } ``` **Don't ❌**: ```typescript // 하나라도 실패하면 전체 에러 const title = extractTitle(content, schema); if (!title) throw new Error('제목 없음'); // ❌ 나머지 데이터도 버려짐 ``` ### 섹션 교체 안전 규칙 **Do ✅**: ```typescript // 대상 섹션만 교체, 나머지 보존 function replaceSection( content: string, sectionHeader: string, newContent: string, separator: string ): string { const lines = content.split('\n'); const startIdx = lines.findIndex(l => l.trim() === sectionHeader); if (startIdx === -1) return content; // 섹션 없으면 원본 반환 const endIdx = findSectionEnd(lines, startIdx + 1, separator); const before = lines.slice(0, startIdx); const after = lines.slice(endIdx); return [...before, sectionHeader, newContent, ...after].join('\n'); } ``` **Don't ❌**: ```typescript // 전체 파일 덮어쓰기 — 다른 섹션 손실 writeFileSync(commandsPath, newSection); // ❌ 전체 교체 ``` ### 템플릿 변수 보간 **Do ✅**: ```typescript // 안전한 변수 보간 — 미정의 변수는 빈 문자열 function interpolate(template: string, vars: Record<string, string>): string { return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? ''); } ``` **Don't ❌**: ```typescript // eval 또는 Function 생성자 사용 금지 const result = eval(`\`${template}\``); // ❌ 보안 위험 ``` ## Examples ### Example 1: 계획서에서 의존성 추출 User says: "계획서 파싱해서 의존 유닛 가져와" Input (계획서): ```markdown # U-118[Mmp]: 마크다운 이미지 링크 삽입 ## 이전 작업에서 가져올 것 - **U-117[Mmp]**: 클립보드 이미지 저장 기능의 파일 경로 생성 로직 - 결과물: `save_clipboard_image()` 함수의 반환값 구조 ``` Result: ```json { "depends": [ { "unitId": "U-117[Mmp]", "description": "클립보드 이미지 저장 기능의 파일 경로 생성 로직", "artifacts": "save_clipboard_image() 함수의 반환값 구조" } ] } ``` ### Example 2: 커맨드 파일 섹션 교체 User says: "유닛 구현 섹션만 업데이트해줘" Actions: 1. "# 유닛 구현" 헤더로 섹션 시작점 탐색 2. 다음 "---...---" separator까지 범위 결정 3. 새 콘텐츠로 범위 내 교체 4. "# 리펙토링 유닛 제안" 등 다른 섹션은 그대로 보존 ## Troubleshooting ### Error: 의존성 파싱 결과가 비어있음 Cause: `dependsSectionName`이 계획서의 실제 섹션 이름과 불일치 Solution: 계획서의 `##` 헤더를 확인하고 `planParsing.dependsSectionName` 수정 ### Error: 섹션 교체 후 다른 섹션이 사라짐 Cause: separator 패턴이 잘못되어 범위가 파일 끝까지 확장 Solution: `commandSections.separator` 설정이 커맨드 파일의 실제 구분선과 일치하는지 확인 ### Error: 템플릿 변수가 그대로 출력 ({{unitId}}) Cause: 변수 맵에 해당 키가 없음 Solution: `interpolate()` 호출 시 모든 필수 변수가 vars 객체에 포함되었는지 확인