"""Context Manager — 관련 파일 선별 + 토큰 예산 제어. Gemini CLI Context Rot 해결의 핵심. 태스크에 필요한 파일만 골라 토큰 예산 내에서 컨텍스트를 구성. """ from pathlib import Path from core.project_indexer import ProjectIndex # 대략적 토큰 추정: 1 토큰 ≈ 4 bytes (영문), 2 bytes (한글) BYTES_PER_TOKEN = 3 class ContextManager: """태스크별 컨텍스트를 생성합니다.""" def __init__(self, index: ProjectIndex, token_budget: int = 50_000): self.index = index self.token_budget = token_budget def gather(self, task: str, max_files: int = 15) -> str: """태스크에 필요한 파일만 선별하여 컨텍스트 생성.""" # 1. 태스크에서 관련 파일 찾기 relevant = self.index.find_relevant(task) # 2. import 관계로 확장 if relevant: expanded = self.index.expand_dependencies(relevant[:5], depth=1) else: expanded = [] # 3. 관련 파일을 우선순위 순으로 정렬 (원래 순서 유지) ordered = [] seen = set() for f in relevant + expanded: if f not in seen: ordered.append(f) seen.add(f) # 4. 토큰 예산 내에서 파일 포함 context_parts = [] total_tokens = 0 files_included = 0 # 프로젝트 구조 요약 항상 포함 structure = self.index.get_structure_summary() structure_tokens = len(structure.encode("utf-8")) // BYTES_PER_TOKEN context_parts.append(f"=== PROJECT STRUCTURE ===\n{structure}") total_tokens += structure_tokens for fpath in ordered[:max_files]: info = self.index.files.get(fpath) if not info: continue file_tokens = info.size // BYTES_PER_TOKEN if total_tokens + file_tokens > self.token_budget: context_parts.append( f"\n=== SKIPPED: {fpath} ({info.line_count}L, budget exceeded) ===" ) continue try: abs_path = self.index.project_path / fpath content = abs_path.read_text(encoding="utf-8", errors="ignore") except Exception: continue context_parts.append( f"\n=== FILE: {fpath} ({info.line_count}L) ===\n{content}" ) total_tokens += file_tokens files_included += 1 context_parts.append( f"\n=== CONTEXT SUMMARY: {files_included} files, ~{total_tokens} tokens ===" ) return "\n".join(context_parts) def gather_for_review(self, original: str, modified: str, task: str) -> str: """리뷰용 컨텍스트: 원본 + 수정본 + 관련 타입.""" parts = [ f"=== TASK: {task} ===", f"\n=== ORIGINAL ===\n{original}", f"\n=== MODIFIED ===\n{modified}", ] return "\n".join(parts)