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

1"""Claude Code Headless-based Merge Analyzer 

2 

3Analyzes template merge differences using Claude Code headless mode 

4for intelligent backup vs new template comparison and recommendations. 

5""" 

6 

7import json 

8import subprocess 

9from difflib import unified_diff 

10from pathlib import Path 

11from typing import Any 

12 

13import click 

14from rich.console import Console 

15from rich.live import Live 

16from rich.spinner import Spinner 

17from rich.table import Table 

18 

19console = Console() 

20 

21 

22class MergeAnalyzer: 

23 """분석기: Claude Code를 사용한 지능형 병합 분석 

24 

25 백업된 사용자 설정과 새 템플릿을 비교하여 Claude AI가 분석하고 

26 병합 권장사항을 제시합니다. 

27 """ 

28 

29 # 분석할 주요 파일 목록 

30 ANALYZED_FILES = [ 

31 "CLAUDE.md", 

32 ".claude/settings.json", 

33 ".moai/config/config.json", 

34 ".gitignore", 

35 ] 

36 

37 # Claude headless 실행 설정 

38 CLAUDE_TIMEOUT = 120 # 최대 2분 

39 CLAUDE_MODEL = "claude-haiku-4-5-20251001" # 최신 Haiku (비용 최적화) 

40 CLAUDE_TOOLS = ["Read", "Glob", "Grep"] # 읽기 전용 

41 

42 def __init__(self, project_path: Path): 

43 """Initialize analyzer with project path.""" 

44 self.project_path = project_path 

45 

46 def analyze_merge( 

47 self, backup_path: Path, template_path: Path 

48 ) -> dict[str, Any]: 

49 """Claude Code headless로 병합 분석 수행 

50 

51 Args: 

52 backup_path: 백업된 설정 디렉토리 경로 

53 template_path: 새 템플릿 디렉토리 경로 

54 

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) 

65 

66 # 2. Claude headless 프롬프트 작성 

67 prompt = self._create_analysis_prompt( 

68 backup_path, template_path, diff_files 

69 ) 

70 

71 # 3. Claude Code headless 실행 (스피너 표시) 

72 spinner = Spinner("dots", text="[cyan]Claude Code 분석 진행 중...[/cyan]") 

73 

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 ) 

83 

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 ) 

103 

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 ) 

121 

122 def ask_user_confirmation(self, analysis: dict[str, Any]) -> bool: 

123 """분석 결과를 표시하고 사용자 승인 요청 

124 

125 Args: 

126 analysis: analyze_merge() 결과 

127 

128 Returns: 

129 True: 진행, False: 취소 

130 """ 

131 # 1. 분석 결과 표시 

132 self._display_analysis(analysis) 

133 

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 ) 

145 

146 # 3. 확인 프롬프트 

147 proceed = click.confirm( 

148 "\n병합을 진행하시겠습니까?", 

149 default=analysis.get("safe_to_auto_merge", False), 

150 ) 

151 

152 return proceed 

153 

154 def _collect_diff_files( 

155 self, backup_path: Path, template_path: Path 

156 ) -> dict[str, dict[str, Any]]: 

157 """백업과 템플릿 간 차이 파일 수집 

158 

159 Returns: 

160 파일별 diff 정보 딕셔너리 

161 """ 

162 diff_files = {} 

163 

164 for file_name in self.ANALYZED_FILES: 

165 backup_file = backup_path / file_name 

166 template_file = template_path / file_name 

167 

168 if not backup_file.exists() and not template_file.exists(): 

169 continue 

170 

171 diff_info = { 

172 "backup_exists": backup_file.exists(), 

173 "template_exists": template_file.exists(), 

174 "has_diff": False, 

175 "diff_lines": 0, 

176 } 

177 

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") 

181 

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) 

192 

193 diff_files[file_name] = diff_info 

194 

195 return diff_files 

196 

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 분석 프롬프트 생성 

204 

205 Returns: 

206 Claude에게 전달할 분석 프롬프트 

207 """ 

208 return f"""당신은 MoAI-ADK 설정 파일 병합 전문가입니다. 

209 

210## 컨텍스트 

211- 백업된 사용자 설정: {backup_path} 

212- 새 템플릿: {template_path} 

213- 분석할 파일: {', '.join(self.ANALYZED_FILES)} 

214 

215## 분석 대상 파일 

216{self._format_diff_summary(diff_files)} 

217 

218## 분석 작업 

219다음 항목을 분석하고 JSON 응답을 제공하세요: 

220 

2211. 각 파일별 변경사항 식별 

2222. 충돌 위험도 평가 (low/medium/high) 

2233. 병합 권장사항 (use_template/keep_existing/smart_merge) 

2244. 전반적 안전성 평가 

225 

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}} 

242 

243## 병합 규칙 참고 

244- CLAUDE.md: Project Information 섹션 보존 

245- settings.json: env 변수는 병합, permissions.deny는 템플릿 우선 

246- config.json: 사용자 메타데이터 보존, 스키마 업데이트 

247- .gitignore: 추가만 (기존 항목 보존) 

248 

249## 추가 고려사항 

250- 사용자 커스터마이징이 손실될 위험 평가 

251- Alfred 인프라 파일의 강제 덮어쓰기 여부 

252- 롤백 가능성 검토 

253""" 

254 

255 def _display_analysis(self, analysis: dict[str, Any]) -> None: 

256 """분석 결과를 Rich 형식으로 표시""" 

257 # 제목 

258 console.print("\n📊 병합 분석 결과 (Claude Code 분석)", style="bold") 

259 

260 # 요약 

261 summary = analysis.get("summary", "분석 결과 없음") 

262 console.print(f"\n📝 {summary}") 

263 

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) 

269 

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") 

277 

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") 

284 

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 ) 

292 

293 console.print(table) 

294 

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 ) 

302 

303 def _build_claude_command(self) -> list[str]: 

304 """Claude Code headless 명령어 구축 (공식 v4.0+ 기반) 

305 

306 Claude Code CLI 공식 옵션: 

307 - -p: Non-interactive headless mode 

308 - --model: 명시적 모델 선택 (Haiku 사용) 

309 - --output-format: JSON 응답 형식 

310 - --tools: 읽기 전용 도구만 허용 (공백 구분 - POSIX 표준) 

311 - --permission-mode: 자동 승인 (백그라운드 작업) 

312 

313 Returns: 

314 Claude CLI 명령 인자 리스트 

315 """ 

316 # 도구 목록을 공백으로 구분 (POSIX 표준, 공식 권장) 

317 tools_str = " ".join(self.CLAUDE_TOOLS) 

318 

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 ] 

331 

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 = "✨ 새 파일 (템플릿)" 

348 

349 summary.append(f"- {file_name}: {status}") 

350 

351 return "\n".join(summary) 

352 

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 기반) 

360 

361 Claude를 사용할 수 없을 때 기본적인 분석 결과 반환 

362 """ 

363 console.print( 

364 "⚠️ Claude Code를 사용할 수 없습니다. 기본 분석을 사용합니다.", 

365 style="yellow", 

366 ) 

367 

368 files_analysis = [] 

369 has_high_risk = False 

370 

371 for file_name, info in diff_files.items(): 

372 if not info["has_diff"]: 

373 continue 

374 

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" 

379 

380 files_analysis.append({ 

381 "filename": file_name, 

382 "changes": f"{info['diff_lines']} 줄 변경됨", 

383 "recommendation": "smart_merge", 

384 "conflict_severity": severity, 

385 }) 

386 

387 if severity == "high": 

388 has_high_risk = True 

389 

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 }