UNPKG

aiwf

Version:

AI Workflow Framework for Claude Code with multi-language support (Korean/English)

557 lines (481 loc) 15.5 kB
import { TokenCounter } from './token-counter.js'; /** * 압축 전략의 기본 인터페이스 */ export class CompressionStrategy { constructor(name) { this.name = name; this.tokenCounter = new TokenCounter(); } /** * 콘텐츠를 압축합니다 * @param {string} content - 압축할 콘텐츠 * @param {Object} options - 압축 옵션 * @returns {Object} 압축 결과 */ compress(content, options = {}) { throw new Error('compress method must be implemented'); } /** * 압축된 콘텐츠를 복원합니다 * @param {string} compressed - 압축된 콘텐츠 * @param {Object} metadata - 복원 메타데이터 * @returns {string} 복원된 콘텐츠 */ decompress(compressed, metadata) { throw new Error('decompress method must be implemented'); } /** * 압축률을 계산합니다 * @param {string} original - 원본 콘텐츠 * @param {string} compressed - 압축된 콘텐츠 * @returns {number} 압축률 (백분율) */ calculateCompressionRatio(original, compressed) { const originalTokens = this.tokenCounter.countTokens(original); const compressedTokens = this.tokenCounter.countTokens(compressed); if (originalTokens === 0) return 0; return ((originalTokens - compressedTokens) / originalTokens) * 100; } /** * 마크다운 콘텐츠를 섹션으로 파싱합니다 * @param {string} content - 마크다운 콘텐츠 * @returns {Array<Object>} 섹션 배열 */ parseMarkdownSections(content) { const sections = []; const lines = content.split('\n'); let currentSection = null; let currentContent = []; let currentLevel = 0; for (const line of lines) { const headerMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headerMatch) { // 이전 섹션 저장 if (currentSection) { sections.push({ name: currentSection, level: currentLevel, content: currentContent.join('\n').trim(), tokens: this.tokenCounter.countTokens(currentContent.join('\n').trim()) }); } // 새 섹션 시작 currentLevel = headerMatch[1].length; currentSection = headerMatch[2]; currentContent = [line]; } else { if (currentSection) { currentContent.push(line); } } } // 마지막 섹션 저장 if (currentSection) { sections.push({ name: currentSection, level: currentLevel, content: currentContent.join('\n').trim(), tokens: this.tokenCounter.countTokens(currentContent.join('\n').trim()) }); } return sections; } /** * 중요도를 기반으로 섹션을 분류합니다 * @param {Array<Object>} sections - 섹션 배열 * @returns {Array<Object>} 중요도가 추가된 섹션 배열 */ classifyImportance(sections) { const importanceKeywords = { critical: ['urgent', 'critical', 'blocking', 'error', 'failed', 'in_progress', 'active'], high: ['important', 'priority', 'deadline', 'milestone', 'requirements', 'goal'], medium: ['enhancement', 'improvement', 'optimize', 'planned', 'task'], low: ['note', 'example', 'history', 'log', 'template', 'completed'] }; const importanceSections = { critical: ['subtasks', 'acceptance criteria', 'goal', 'objectives', 'current sprint'], high: ['description', 'requirements', 'architecture', 'specifications'], medium: ['implementation', 'testing', 'documentation', 'notes'], low: ['output log', 'history', 'examples', 'templates'] }; return sections.map(section => { let importance = 'low'; let score = 0; // 키워드 기반 점수 계산 for (const [level, keywords] of Object.entries(importanceKeywords)) { const matches = keywords.filter(keyword => section.content.toLowerCase().includes(keyword.toLowerCase()) ).length; if (matches > 0) { switch (level) { case 'critical': score += matches * 4; break; case 'high': score += matches * 3; break; case 'medium': score += matches * 2; break; case 'low': score += matches * 1; break; } } } // 섹션 타입 기반 점수 계산 for (const [level, sectionTypes] of Object.entries(importanceSections)) { const isImportantSection = sectionTypes.some(type => section.name.toLowerCase().includes(type.toLowerCase()) ); if (isImportantSection) { switch (level) { case 'critical': score += 5; break; case 'high': score += 4; break; case 'medium': score += 3; break; case 'low': score += 1; break; } } } // 점수를 카테고리로 변환 if (score >= 8) importance = 'critical'; else if (score >= 5) importance = 'high'; else if (score >= 3) importance = 'medium'; else importance = 'low'; return { ...section, importance, score }; }); } /** * 반복적인 콘텐츠를 식별합니다 * @param {string} content - 콘텐츠 * @returns {Array<Object>} 반복 패턴 배열 */ identifyRepetitiveContent(content) { const repetitivePatterns = [ /---\n[\s\S]*?\n---/g, // Front matter /\*\*.*?\*\*:/g, // Bold labels /\[.*?\]\(.*?\)/g, // Markdown links /```[\s\S]*?```/g, // Code blocks /^\s*[-*+]\s+/gm, // List items /^\s*\d+\.\s+/gm, // Numbered lists /#{1,6}\s+/g, // Headers ]; const patterns = []; for (const pattern of repetitivePatterns) { const matches = content.match(pattern); if (matches && matches.length > 3) { patterns.push({ pattern: pattern.source, occurrences: matches.length, examples: matches.slice(0, 3) }); } } return patterns; } /** * 오래된 로그를 식별합니다 * @param {string} content - 콘텐츠 * @returns {Array<string>} 오래된 로그 라인 */ identifyOldLogs(content) { const lines = content.split('\n'); const oldLogs = []; const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - 7); // 7일 전 for (const line of lines) { // 날짜 패턴 매칭 const dateMatch = line.match(/\[(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]/); if (dateMatch) { const logDate = new Date(dateMatch[1]); if (logDate < cutoffDate) { oldLogs.push(line); } } } return oldLogs; } /** * 리소스를 정리합니다 */ cleanup() { if (this.tokenCounter) { this.tokenCounter.cleanup(); } } } /** * 적극적 압축 전략 (50-70% 압축) */ export class AggressiveCompressionStrategy extends CompressionStrategy { constructor() { super('aggressive'); } compress(content, options = {}) { let compressed = content; const metadata = { strategy: 'aggressive', removedSections: [], compressionLevel: 'high', preservedSections: [] }; // 1. 섹션 분석 const sections = this.parseMarkdownSections(content); const classifiedSections = this.classifyImportance(sections); // 2. 중요도 기반 필터링 (high, critical만 유지) const filteredSections = classifiedSections.filter(section => { if (section.importance === 'critical' || section.importance === 'high') { metadata.preservedSections.push(section.name); return true; } else { metadata.removedSections.push(section.name); return false; } }); // 3. 반복 콘텐츠 제거 compressed = this.removeRepetitiveContent(compressed); // 4. 오래된 로그 제거 const oldLogs = this.identifyOldLogs(compressed); for (const log of oldLogs) { compressed = compressed.replace(log + '\n', ''); } // 5. 상세 설명 요약 compressed = this.summarizeDescriptions(compressed); // 6. 필터링된 섹션으로 재구성 compressed = filteredSections.map(section => section.content).join('\n\n'); return { content: compressed, metadata }; } /** * 반복 콘텐츠를 제거합니다 */ removeRepetitiveContent(content) { // 중복된 헤더 제거 const lines = content.split('\n'); const uniqueLines = []; const seenHeaders = new Set(); for (const line of lines) { const headerMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headerMatch) { const headerText = headerMatch[2]; if (!seenHeaders.has(headerText)) { seenHeaders.add(headerText); uniqueLines.push(line); } } else { uniqueLines.push(line); } } return uniqueLines.join('\n'); } /** * 오래된 로그를 제거합니다 */ removeOldLogs(content) { const oldLogs = this.identifyOldLogs(content); let filtered = content; for (const log of oldLogs) { filtered = filtered.replace(log + '\n', ''); } return filtered; } /** * 상세 설명을 요약합니다 */ summarizeDescriptions(content) { // 긴 단락을 요약 const paragraphs = content.split('\n\n'); const summarized = paragraphs.map(paragraph => { if (paragraph.length > 500) { // 첫 번째와 마지막 문장만 유지 const sentences = paragraph.split('. '); if (sentences.length > 2) { return sentences[0] + '. ... ' + sentences[sentences.length - 1]; } } return paragraph; }); return summarized.join('\n\n'); } /** * 필터링된 섹션으로 콘텐츠를 재구성합니다 */ reconstructContent(sections) { return sections.map(section => section.content).join('\n\n'); } } /** * 균형 압축 전략 (30-50% 압축) */ export class BalancedCompressionStrategy extends CompressionStrategy { constructor() { super('balanced'); } compress(content, options = {}) { let compressed = content; const metadata = { strategy: 'balanced', removedSections: [], compressionLevel: 'medium', preservedSections: [] }; // 1. 섹션 분석 const sections = this.parseMarkdownSections(content); const classifiedSections = this.classifyImportance(sections); // 2. 중요도 기반 필터링 (medium 이상 유지) const filteredSections = classifiedSections.filter(section => { if (section.importance !== 'low') { metadata.preservedSections.push(section.name); return true; } else { metadata.removedSections.push(section.name); return false; } }); // 3. 반복 콘텐츠 축약 compressed = this.condenseRepetitiveContent(compressed); // 4. 오래된 로그 제거 const oldLogs = this.identifyOldLogs(compressed); for (const log of oldLogs) { compressed = compressed.replace(log + '\n', ''); } // 5. 장문 설명 압축 compressed = this.compressLongDescriptions(compressed); // 6. 코드 블록 축소 compressed = this.compressCodeBlocks(compressed); // 7. 필터링된 섹션으로 재구성 compressed = filteredSections.map(section => { // 각 섹션 내용도 압축 let sectionContent = section.content; sectionContent = this.condenseRepetitiveContent(sectionContent); sectionContent = this.compressLongDescriptions(sectionContent); return sectionContent; }).join('\n\n'); return { content: compressed, metadata }; } /** * 반복 콘텐츠를 축약합니다 */ condenseRepetitiveContent(content) { // 연속된 빈 줄 제거 content = content.replace(/\n{3,}/g, '\n\n'); // 중복된 목록 항목 제거 const lines = content.split('\n'); const condensed = []; const seenListItems = new Set(); for (const line of lines) { const listMatch = line.match(/^\s*[-*+]\s+(.+)$/); if (listMatch) { const listText = listMatch[1]; if (!seenListItems.has(listText)) { seenListItems.add(listText); condensed.push(line); } } else { condensed.push(line); } } return condensed.join('\n'); } /** * 장문 설명을 압축합니다 */ compressLongDescriptions(content) { const paragraphs = content.split('\n\n'); const compressed = paragraphs.map(paragraph => { if (paragraph.length > 300) { // 핵심 문장만 유지 const sentences = paragraph.split('. '); if (sentences.length > 3) { return sentences.slice(0, 2).join('. ') + '. [압축됨]'; } } return paragraph; }); return compressed.join('\n\n'); } /** * 코드 블록을 압축합니다 */ compressCodeBlocks(content) { // 긴 코드 블록을 축소 const codeBlockPattern = /```[\s\S]*?```/g; return content.replace(codeBlockPattern, (match) => { const lines = match.split('\n'); if (lines.length > 20) { // 처음 5줄과 마지막 3줄만 유지 const language = lines[0]; const firstLines = lines.slice(1, 6).join('\n'); const lastLines = lines.slice(-4, -1).join('\n'); return `${language}\n${firstLines}\n... [${lines.length - 10}줄 생략] ...\n${lastLines}\n\`\`\``; } return match; }); } /** * 필터링된 섹션으로 콘텐츠를 재구성합니다 */ reconstructContent(sections) { return sections.map(section => section.content).join('\n\n'); } } /** * 최소 압축 전략 (10-30% 압축) */ export class MinimalCompressionStrategy extends CompressionStrategy { constructor() { super('minimal'); } compress(content, options = {}) { let compressed = content; const metadata = { strategy: 'minimal', removedSections: [], compressionLevel: 'low', preservedSections: [] }; // 1. 포맷팅 최적화 compressed = this.optimizeFormatting(compressed); // 2. 오래된 로그만 제거 const oldLogs = this.identifyOldLogs(compressed); for (const log of oldLogs) { compressed = compressed.replace(log + '\n', ''); } // 3. 기본 정리 compressed = this.basicCleanup(compressed); return { content: compressed, metadata }; } /** * 포맷팅을 최적화합니다 */ optimizeFormatting(content) { // 불필요한 공백 제거 content = content.replace(/[ \t]+$/gm, ''); // 줄 끝 공백 content = content.replace(/\n{4,}/g, '\n\n\n'); // 과도한 빈 줄 content = content.replace(/^\n+/, ''); // 시작 빈 줄 content = content.replace(/\n+$/, '\n'); // 끝 빈 줄 return content; } /** * 기본 정리를 수행합니다 */ basicCleanup(content) { // 중복된 구분선 제거 content = content.replace(/---+/g, '---'); // 불필요한 마크다운 태그 정리 content = content.replace(/\*\*\s*\*\*/g, ''); // 빈 bold 태그 content = content.replace(/\*\s*\*/g, ''); // 빈 italic 태그 return content; } } export default { CompressionStrategy, AggressiveCompressionStrategy, BalancedCompressionStrategy, MinimalCompressionStrategy };