Coverage for src / moai_adk / core / merge / analyzer.py: 21.19%
118 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""Claude Code Headless-based Merge Analyzer
3Analyzes template merge differences using Claude Code headless mode
4for intelligent backup vs new template comparison and recommendations.
5"""
7import json
8import subprocess
9from difflib import unified_diff
10from pathlib import Path
11from typing import Any
13import click
14from rich.console import Console
15from rich.live import Live
16from rich.spinner import Spinner
17from rich.table import Table
19console = Console()
22class MergeAnalyzer:
23 """분석기: Claude Code를 사용한 지능형 병합 분석
25 백업된 사용자 설정과 새 템플릿을 비교하여 Claude AI가 분석하고
26 병합 권장사항을 제시합니다.
27 """
29 # 분석할 주요 파일 목록
30 ANALYZED_FILES = [
31 "CLAUDE.md",
32 ".claude/settings.json",
33 ".moai/config/config.json",
34 ".gitignore",
35 ]
37 # Claude headless 실행 설정
38 CLAUDE_TIMEOUT = 120 # 최대 2분
39 CLAUDE_MODEL = "claude-haiku-4-5-20251001" # 최신 Haiku (비용 최적화)
40 CLAUDE_TOOLS = ["Read", "Glob", "Grep"] # 읽기 전용
42 def __init__(self, project_path: Path):
43 """Initialize analyzer with project path."""
44 self.project_path = project_path
46 def analyze_merge(
47 self, backup_path: Path, template_path: Path
48 ) -> dict[str, Any]:
49 """Claude Code headless로 병합 분석 수행
51 Args:
52 backup_path: 백업된 설정 디렉토리 경로
53 template_path: 새 템플릿 디렉토리 경로
55 Returns:
56 분석 결과를 담은 딕셔너리
57 - files: 파일별 변경사항 리스트
58 - safe_to_auto_merge: 자동 병합 안전 여부
59 - user_action_required: 사용자 개입 필요 여부
60 - summary: 종합 요약
61 - error: 오류 메시지 (있는 경우)
62 """
63 # 1. 비교할 파일 수집
64 diff_files = self._collect_diff_files(backup_path, template_path)
66 # 2. Claude headless 프롬프트 작성
67 prompt = self._create_analysis_prompt(
68 backup_path, template_path, diff_files
69 )
71 # 3. Claude Code headless 실행 (스피너 표시)
72 spinner = Spinner("dots", text="[cyan]Claude Code 분석 진행 중...[/cyan]")
74 try:
75 with Live(spinner, refresh_per_second=12):
76 result = subprocess.run(
77 self._build_claude_command(),
78 input=prompt,
79 capture_output=True,
80 text=True,
81 timeout=self.CLAUDE_TIMEOUT,
82 )
84 if result.returncode == 0:
85 try:
86 analysis = json.loads(result.stdout)
87 console.print("[green]✅ 분석 완료[/green]")
88 return analysis
89 except json.JSONDecodeError as e:
90 console.print(
91 f"[yellow]⚠️ Claude 응답 파싱 오류: {e}[/yellow]"
92 )
93 return self._fallback_analysis(
94 backup_path, template_path, diff_files
95 )
96 else:
97 console.print(
98 f"[yellow]⚠️ Claude 실행 오류: {result.stderr[:200]}[/yellow]"
99 )
100 return self._fallback_analysis(
101 backup_path, template_path, diff_files
102 )
104 except subprocess.TimeoutExpired:
105 console.print(
106 "[yellow]⚠️ Claude 분석 타임아웃 (120초 초과)[/yellow]"
107 )
108 return self._fallback_analysis(
109 backup_path, template_path, diff_files
110 )
111 except FileNotFoundError:
112 console.print(
113 "[red]❌ Claude Code를 찾을 수 없습니다.[/red]"
114 )
115 console.print(
116 "[cyan] Claude Code 설치: https://claude.com/claude-code[/cyan]"
117 )
118 return self._fallback_analysis(
119 backup_path, template_path, diff_files
120 )
122 def ask_user_confirmation(self, analysis: dict[str, Any]) -> bool:
123 """분석 결과를 표시하고 사용자 승인 요청
125 Args:
126 analysis: analyze_merge() 결과
128 Returns:
129 True: 진행, False: 취소
130 """
131 # 1. 분석 결과 표시
132 self._display_analysis(analysis)
134 # 2. 사용자 확인
135 if analysis.get("user_action_required", False):
136 console.print(
137 "\n⚠️ 사용자 개입이 필요합니다. 아래 사항을 검토하세요:",
138 style="warning",
139 )
140 for file_info in analysis.get("files", []):
141 if file_info.get("conflict_severity") in ["medium", "high"]:
142 console.print(
143 f" • {file_info['filename']}: {file_info.get('note', '')}",
144 )
146 # 3. 확인 프롬프트
147 proceed = click.confirm(
148 "\n병합을 진행하시겠습니까?",
149 default=analysis.get("safe_to_auto_merge", False),
150 )
152 return proceed
154 def _collect_diff_files(
155 self, backup_path: Path, template_path: Path
156 ) -> dict[str, dict[str, Any]]:
157 """백업과 템플릿 간 차이 파일 수집
159 Returns:
160 파일별 diff 정보 딕셔너리
161 """
162 diff_files = {}
164 for file_name in self.ANALYZED_FILES:
165 backup_file = backup_path / file_name
166 template_file = template_path / file_name
168 if not backup_file.exists() and not template_file.exists():
169 continue
171 diff_info = {
172 "backup_exists": backup_file.exists(),
173 "template_exists": template_file.exists(),
174 "has_diff": False,
175 "diff_lines": 0,
176 }
178 if backup_file.exists() and template_file.exists():
179 backup_content = backup_file.read_text(encoding="utf-8")
180 template_content = template_file.read_text(encoding="utf-8")
182 if backup_content != template_content:
183 diff = list(
184 unified_diff(
185 backup_content.splitlines(),
186 template_content.splitlines(),
187 lineterm="",
188 )
189 )
190 diff_info["has_diff"] = True
191 diff_info["diff_lines"] = len(diff)
193 diff_files[file_name] = diff_info
195 return diff_files
197 def _create_analysis_prompt(
198 self,
199 backup_path: Path,
200 template_path: Path,
201 diff_files: dict[str, dict[str, Any]],
202 ) -> str:
203 """Claude headless 분석 프롬프트 생성
205 Returns:
206 Claude에게 전달할 분석 프롬프트
207 """
208 return f"""당신은 MoAI-ADK 설정 파일 병합 전문가입니다.
210## 컨텍스트
211- 백업된 사용자 설정: {backup_path}
212- 새 템플릿: {template_path}
213- 분석할 파일: {', '.join(self.ANALYZED_FILES)}
215## 분석 대상 파일
216{self._format_diff_summary(diff_files)}
218## 분석 작업
219다음 항목을 분석하고 JSON 응답을 제공하세요:
2211. 각 파일별 변경사항 식별
2222. 충돌 위험도 평가 (low/medium/high)
2233. 병합 권장사항 (use_template/keep_existing/smart_merge)
2244. 전반적 안전성 평가
226## 응답 형식 (JSON)
227{{
228 "files": [
229 {{
230 "filename": "CLAUDE.md",
231 "changes": "변경사항 설명",
232 "recommendation": "use_template|keep_existing|smart_merge",
233 "conflict_severity": "low|medium|high",
234 "note": "추가 설명 (선택사항)"
235 }}
236 ],
237 "safe_to_auto_merge": true/false,
238 "user_action_required": true/false,
239 "summary": "병합 가능 여부와 이유",
240 "risk_assessment": "위험도 평가"
241}}
243## 병합 규칙 참고
244- CLAUDE.md: Project Information 섹션 보존
245- settings.json: env 변수는 병합, permissions.deny는 템플릿 우선
246- config.json: 사용자 메타데이터 보존, 스키마 업데이트
247- .gitignore: 추가만 (기존 항목 보존)
249## 추가 고려사항
250- 사용자 커스터마이징이 손실될 위험 평가
251- Alfred 인프라 파일의 강제 덮어쓰기 여부
252- 롤백 가능성 검토
253"""
255 def _display_analysis(self, analysis: dict[str, Any]) -> None:
256 """분석 결과를 Rich 형식으로 표시"""
257 # 제목
258 console.print("\n📊 병합 분석 결과 (Claude Code 분석)", style="bold")
260 # 요약
261 summary = analysis.get("summary", "분석 결과 없음")
262 console.print(f"\n📝 {summary}")
264 # 위험도 평가
265 risk_assessment = analysis.get("risk_assessment", "")
266 if risk_assessment:
267 risk_style = "green" if "safe" in risk_assessment.lower() else "yellow"
268 console.print(f"⚠️ 위험도: {risk_assessment}", style=risk_style)
270 # 파일별 변경사항 테이블
271 if analysis.get("files"):
272 table = Table(title="파일별 변경사항")
273 table.add_column("파일", style="cyan")
274 table.add_column("변경사항", style="white")
275 table.add_column("권장", style="yellow")
276 table.add_column("위험도", style="red")
278 for file_info in analysis["files"]:
279 severity_style = {
280 "low": "green",
281 "medium": "yellow",
282 "high": "red",
283 }.get(file_info.get("conflict_severity", "low"), "white")
285 table.add_row(
286 file_info.get("filename", "?"),
287 file_info.get("changes", "")[:30],
288 file_info.get("recommendation", "?"),
289 file_info.get("conflict_severity", "?"),
290 style=severity_style,
291 )
293 console.print(table)
295 # 추가 설명
296 for file_info in analysis["files"]:
297 if file_info.get("note"):
298 console.print(
299 f"\n💡 {file_info['filename']}: {file_info['note']}",
300 style="dim",
301 )
303 def _build_claude_command(self) -> list[str]:
304 """Claude Code headless 명령어 구축 (공식 v4.0+ 기반)
306 Claude Code CLI 공식 옵션:
307 - -p: Non-interactive headless mode
308 - --model: 명시적 모델 선택 (Haiku 사용)
309 - --output-format: JSON 응답 형식
310 - --tools: 읽기 전용 도구만 허용 (공백 구분 - POSIX 표준)
311 - --permission-mode: 자동 승인 (백그라운드 작업)
313 Returns:
314 Claude CLI 명령 인자 리스트
315 """
316 # 도구 목록을 공백으로 구분 (POSIX 표준, 공식 권장)
317 tools_str = " ".join(self.CLAUDE_TOOLS)
319 return [
320 "claude",
321 "-p", # Non-interactive headless mode
322 "--model",
323 self.CLAUDE_MODEL, # 명시적 모델 지정 (Haiku)
324 "--output-format",
325 "json", # JSON 단일 응답
326 "--tools",
327 tools_str, # 공백 구분 (Read Glob Grep)
328 "--permission-mode",
329 "dontAsk", # 자동 승인 (읽기만 가능하므로 안전)
330 ]
332 def _format_diff_summary(
333 self, diff_files: dict[str, dict[str, Any]]
334 ) -> str:
335 """diff_files를 프롬프트 형식으로 정렬"""
336 summary = []
337 for file_name, info in diff_files.items():
338 if info["backup_exists"] and info["template_exists"]:
339 status = (
340 f"✏️ 변경됨 ({info['diff_lines']} 줄)"
341 if info["has_diff"]
342 else "✓ 동일"
343 )
344 elif info["backup_exists"]:
345 status = "❌ 템플릿에서 삭제됨"
346 else:
347 status = "✨ 새 파일 (템플릿)"
349 summary.append(f"- {file_name}: {status}")
351 return "\n".join(summary)
353 def _fallback_analysis(
354 self,
355 backup_path: Path,
356 template_path: Path,
357 diff_files: dict[str, dict[str, Any]],
358 ) -> dict[str, Any]:
359 """Claude 호출 실패 시 기본 분석 (difflib 기반)
361 Claude를 사용할 수 없을 때 기본적인 분석 결과 반환
362 """
363 console.print(
364 "⚠️ Claude Code를 사용할 수 없습니다. 기본 분석을 사용합니다.",
365 style="yellow",
366 )
368 files_analysis = []
369 has_high_risk = False
371 for file_name, info in diff_files.items():
372 if not info["has_diff"]:
373 continue
375 # 간단한 위험도 평가
376 severity = "low"
377 if file_name in [".claude/settings.json", ".moai/config/config.json"]:
378 severity = "medium" if info["diff_lines"] > 10 else "low"
380 files_analysis.append({
381 "filename": file_name,
382 "changes": f"{info['diff_lines']} 줄 변경됨",
383 "recommendation": "smart_merge",
384 "conflict_severity": severity,
385 })
387 if severity == "high":
388 has_high_risk = True
390 return {
391 "files": files_analysis,
392 "safe_to_auto_merge": not has_high_risk,
393 "user_action_required": has_high_risk,
394 "summary": f"{len(files_analysis)}개 파일 변경 감지 (기본 분석)",
395 "risk_assessment": "높음 - Claude 분석 불가, 수동 검토 권장" if has_high_risk else "낮음",
396 "fallback": True,
397 }