gitinstall 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. gitinstall/__init__.py +61 -0
  2. gitinstall/_sdk.py +541 -0
  3. gitinstall/academic.py +831 -0
  4. gitinstall/admin.html +327 -0
  5. gitinstall/auto_update.py +384 -0
  6. gitinstall/autopilot.py +349 -0
  7. gitinstall/badge.py +476 -0
  8. gitinstall/checkpoint.py +330 -0
  9. gitinstall/cicd.py +499 -0
  10. gitinstall/clawhub.html +718 -0
  11. gitinstall/config_schema.py +353 -0
  12. gitinstall/db.py +984 -0
  13. gitinstall/db_backend.py +445 -0
  14. gitinstall/dep_chain.py +337 -0
  15. gitinstall/dependency_audit.py +1153 -0
  16. gitinstall/detector.py +542 -0
  17. gitinstall/doctor.py +493 -0
  18. gitinstall/education.py +869 -0
  19. gitinstall/enterprise.py +802 -0
  20. gitinstall/error_fixer.py +953 -0
  21. gitinstall/event_bus.py +251 -0
  22. gitinstall/executor.py +577 -0
  23. gitinstall/feature_flags.py +138 -0
  24. gitinstall/fetcher.py +921 -0
  25. gitinstall/huggingface.py +922 -0
  26. gitinstall/hw_detect.py +988 -0
  27. gitinstall/i18n.py +664 -0
  28. gitinstall/installer_registry.py +362 -0
  29. gitinstall/knowledge_base.py +379 -0
  30. gitinstall/license_check.py +605 -0
  31. gitinstall/llm.py +569 -0
  32. gitinstall/log.py +236 -0
  33. gitinstall/main.py +1408 -0
  34. gitinstall/mcp_agent.py +841 -0
  35. gitinstall/mcp_server.py +386 -0
  36. gitinstall/monorepo.py +810 -0
  37. gitinstall/multi_source.py +425 -0
  38. gitinstall/onboard.py +276 -0
  39. gitinstall/planner.py +222 -0
  40. gitinstall/planner_helpers.py +323 -0
  41. gitinstall/planner_known_projects.py +1010 -0
  42. gitinstall/planner_templates.py +996 -0
  43. gitinstall/remote_gpu.py +633 -0
  44. gitinstall/resilience.py +608 -0
  45. gitinstall/run_tests.py +572 -0
  46. gitinstall/skills.py +476 -0
  47. gitinstall/tool_schemas.py +324 -0
  48. gitinstall/trending.py +279 -0
  49. gitinstall/uninstaller.py +415 -0
  50. gitinstall/validate_top100.py +607 -0
  51. gitinstall/watchdog.py +180 -0
  52. gitinstall/web.py +1277 -0
  53. gitinstall/web_ui.html +2277 -0
  54. gitinstall-1.1.0.dist-info/METADATA +275 -0
  55. gitinstall-1.1.0.dist-info/RECORD +59 -0
  56. gitinstall-1.1.0.dist-info/WHEEL +5 -0
  57. gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
  58. gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
  59. gitinstall-1.1.0.dist-info/top_level.txt +1 -0
gitinstall/doctor.py ADDED
@@ -0,0 +1,493 @@
1
+ """
2
+ doctor.py - gitinstall 诊断系统
3
+ ================================
4
+
5
+ 灵感来源:OpenClaw `openclaw doctor` 诊断工具
6
+
7
+ 功能:
8
+ 1. 系统环境全面检查(OS、Python、Git、包管理器)
9
+ 2. GitHub API 连通性 + 配额检测
10
+ 3. LLM API Key 可用性验证
11
+ 4. 缓存健康度检查(大小、过期条目)
12
+ 5. 数据库完整性校验
13
+ 6. GPU / AI 硬件就绪度
14
+ 7. 安全配置审计
15
+ 8. 已知问题自动修复建议
16
+
17
+ 零外部依赖,纯 Python 标准库。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import platform
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ import time
29
+ import urllib.error
30
+ import urllib.request
31
+ from dataclasses import dataclass, field
32
+ from pathlib import Path
33
+ from typing import Optional
34
+
35
+
36
+ # ── 诊断结果等级 ──
37
+ LEVEL_OK = "ok" # ✅ 正常
38
+ LEVEL_WARN = "warn" # ⚠️ 警告(不阻塞,但建议修复)
39
+ LEVEL_ERROR = "error" # ❌ 错误(会影响功能)
40
+ LEVEL_INFO = "info" # ℹ️ 纯信息
41
+
42
+
43
+ @dataclass
44
+ class CheckResult:
45
+ """单项检查结果"""
46
+ name: str
47
+ level: str
48
+ message: str
49
+ detail: str = ""
50
+ fix_hint: str = ""
51
+
52
+
53
+ @dataclass
54
+ class DoctorReport:
55
+ """完整诊断报告"""
56
+ checks: list[CheckResult] = field(default_factory=list)
57
+ timestamp: float = 0.0
58
+ duration_ms: float = 0.0
59
+
60
+ @property
61
+ def ok_count(self) -> int:
62
+ return sum(1 for c in self.checks if c.level == LEVEL_OK)
63
+
64
+ @property
65
+ def warn_count(self) -> int:
66
+ return sum(1 for c in self.checks if c.level == LEVEL_WARN)
67
+
68
+ @property
69
+ def error_count(self) -> int:
70
+ return sum(1 for c in self.checks if c.level == LEVEL_ERROR)
71
+
72
+ @property
73
+ def all_ok(self) -> bool:
74
+ return self.error_count == 0
75
+
76
+
77
+ # ─────────────────────────────────────────────
78
+ # 各项检查
79
+ # ─────────────────────────────────────────────
80
+
81
+ def _check_python() -> CheckResult:
82
+ """检查 Python 版本"""
83
+ ver = sys.version_info
84
+ ver_str = f"{ver.major}.{ver.minor}.{ver.micro}"
85
+ if ver >= (3, 10):
86
+ return CheckResult("Python 版本", LEVEL_OK, f"Python {ver_str}")
87
+ elif ver >= (3, 8):
88
+ return CheckResult("Python 版本", LEVEL_WARN, f"Python {ver_str}(推荐 3.10+)",
89
+ fix_hint="升级 Python: brew install python3 / apt install python3.12")
90
+ else:
91
+ return CheckResult("Python 版本", LEVEL_ERROR, f"Python {ver_str} 版本过低",
92
+ fix_hint="需要 Python 3.8+,推荐 3.10+")
93
+
94
+
95
+ def _check_git() -> CheckResult:
96
+ """检查 Git 可用性"""
97
+ git = shutil.which("git")
98
+ if not git:
99
+ return CheckResult("Git", LEVEL_ERROR, "未安装 Git",
100
+ fix_hint="安装: brew install git / apt install git / winget install Git.Git")
101
+ try:
102
+ result = subprocess.run([git, "--version"], capture_output=True, text=True, timeout=5)
103
+ ver = result.stdout.strip()
104
+ return CheckResult("Git", LEVEL_OK, ver)
105
+ except Exception as e:
106
+ return CheckResult("Git", LEVEL_WARN, f"Git 存在但无法运行: {e}")
107
+
108
+
109
+ def _check_package_managers() -> list[CheckResult]:
110
+ """检查包管理器"""
111
+ results = []
112
+ managers = {
113
+ "brew": ("Homebrew", "macOS/Linux 包管理器"),
114
+ "apt": ("APT", "Debian/Ubuntu 包管理器"),
115
+ "yum": ("YUM", "RHEL/CentOS 包管理器"),
116
+ "dnf": ("DNF", "Fedora 包管理器"),
117
+ "pacman": ("Pacman", "Arch Linux 包管理器"),
118
+ "pip": ("pip", "Python 包管理器"),
119
+ "npm": ("npm", "Node.js 包管理器"),
120
+ "cargo": ("Cargo", "Rust 包管理器"),
121
+ "go": ("Go", "Go 语言工具链"),
122
+ "docker": ("Docker", "容器运行时"),
123
+ "conda": ("Conda", "科学计算环境管理器"),
124
+ }
125
+ found_any = False
126
+ for cmd, (display_name, desc) in managers.items():
127
+ path = shutil.which(cmd)
128
+ if path:
129
+ found_any = True
130
+ results.append(CheckResult(display_name, LEVEL_OK, f"已安装 ({path})"))
131
+
132
+ if not found_any:
133
+ results.append(CheckResult("包管理器", LEVEL_ERROR,
134
+ "未检测到任何包管理器",
135
+ fix_hint="至少需要一个包管理器才能安装项目依赖"))
136
+ return results
137
+
138
+
139
+ def _check_github_api() -> CheckResult:
140
+ """检查 GitHub API 连通性和配额"""
141
+ token = os.getenv("GITHUB_TOKEN", "").strip()
142
+ headers = {"User-Agent": "gitinstall-doctor/1.0"}
143
+ if token:
144
+ headers["Authorization"] = f"token {token}"
145
+
146
+ try:
147
+ req = urllib.request.Request("https://api.github.com/rate_limit", headers=headers)
148
+ with urllib.request.urlopen(req, timeout=10) as resp:
149
+ data = json.loads(resp.read().decode())
150
+ core = data.get("resources", {}).get("core", {})
151
+ remaining = core.get("remaining", 0)
152
+ limit = core.get("limit", 0)
153
+ reset_ts = core.get("reset", 0)
154
+
155
+ if token:
156
+ auth_info = f"已认证(GITHUB_TOKEN),配额 {remaining}/{limit}"
157
+ else:
158
+ auth_info = f"未认证,配额 {remaining}/{limit}"
159
+
160
+ if remaining < 5:
161
+ reset_time = time.strftime("%H:%M:%S", time.localtime(reset_ts))
162
+ return CheckResult("GitHub API", LEVEL_WARN,
163
+ f"{auth_info},配额即将耗尽,{reset_time} 重置",
164
+ fix_hint="设置 GITHUB_TOKEN 环境变量提升到 5000 次/小时,或使用 --local 模式")
165
+ if not token:
166
+ return CheckResult("GitHub API", LEVEL_WARN,
167
+ f"{auth_info}(限制 60 次/小时)",
168
+ fix_hint="设置 GITHUB_TOKEN=ghp_xxx 提升到 5000 次/小时")
169
+ return CheckResult("GitHub API", LEVEL_OK, auth_info)
170
+ except urllib.error.URLError as e:
171
+ return CheckResult("GitHub API", LEVEL_ERROR, f"无法连接 GitHub API: {e}",
172
+ fix_hint="检查网络连接,或设置 HTTP_PROXY 环境变量")
173
+ except Exception as e:
174
+ return CheckResult("GitHub API", LEVEL_ERROR, f"检查失败: {e}")
175
+
176
+
177
+ def _check_llm_keys() -> list[CheckResult]:
178
+ """检查 LLM API Key 配置"""
179
+ results = []
180
+ keys = {
181
+ "ANTHROPIC_API_KEY": ("Anthropic Claude", "sk-ant-"),
182
+ "OPENAI_API_KEY": ("OpenAI GPT", "sk-"),
183
+ "OPENROUTER_API_KEY": ("OpenRouter", "sk-or-"),
184
+ "GEMINI_API_KEY": ("Google Gemini", "AI"),
185
+ "GROQ_API_KEY": ("Groq Llama", "gsk_"),
186
+ "DEEPSEEK_API_KEY": ("DeepSeek", "sk-"),
187
+ }
188
+ configured = []
189
+ for env_var, (display, prefix) in keys.items():
190
+ val = os.getenv(env_var, "").strip()
191
+ if val:
192
+ # 简单格式校验(不发送请求)
193
+ if prefix and not val.startswith(prefix):
194
+ results.append(CheckResult(display, LEVEL_WARN,
195
+ f"{env_var} 已设置但格式可能有误(期望 {prefix}... 开头)"))
196
+ else:
197
+ configured.append(display)
198
+ results.append(CheckResult(display, LEVEL_OK, f"{env_var} 已配置"))
199
+
200
+ # 检查本地 LLM
201
+ local_llms = [
202
+ ("localhost:11434", "Ollama"),
203
+ ("localhost:1234", "LM Studio"),
204
+ ]
205
+ for addr, name in local_llms:
206
+ try:
207
+ req = urllib.request.Request(f"http://{addr}/")
208
+ with urllib.request.urlopen(req, timeout=3):
209
+ configured.append(name)
210
+ results.append(CheckResult(name, LEVEL_OK, f"{name} 本地服务运行中"))
211
+ except Exception:
212
+ pass # 不报错,本地 LLM 是可选的
213
+
214
+ if not configured:
215
+ results.append(CheckResult("LLM 配置", LEVEL_INFO,
216
+ "未配置任何 LLM API Key(gitinstall 无需 AI 也能工作)",
217
+ detail="SmartPlanner 内置 80+ 已知项目 + 50+ 语言模板,覆盖大部分场景",
218
+ fix_hint="如需 AI 增强,设置任一 API Key: ANTHROPIC_API_KEY, OPENAI_API_KEY, GROQ_API_KEY 等"))
219
+
220
+ return results
221
+
222
+
223
+ def _check_cache() -> CheckResult:
224
+ """检查缓存健康度"""
225
+ cache_dir = Path.home() / ".cache" / "gitinstall"
226
+ if not cache_dir.exists():
227
+ return CheckResult("缓存", LEVEL_OK, "缓存目录尚未创建(首次使用时自动创建)")
228
+
229
+ total_size = 0
230
+ file_count = 0
231
+ expired_count = 0
232
+ now = time.time()
233
+ ttl = int(os.getenv("GITINSTALL_CACHE_TTL", str(24 * 3600)))
234
+
235
+ for f in cache_dir.rglob("*"):
236
+ if f.is_file():
237
+ file_count += 1
238
+ total_size += f.stat().st_size
239
+ if now - f.stat().st_mtime > ttl:
240
+ expired_count += 1
241
+
242
+ size_mb = total_size / (1024 * 1024)
243
+
244
+ if size_mb > 100:
245
+ return CheckResult("缓存", LEVEL_WARN,
246
+ f"缓存较大: {size_mb:.1f}MB ({file_count} 文件, {expired_count} 已过期)",
247
+ fix_hint=f"清理缓存: rm -rf {cache_dir}")
248
+ return CheckResult("缓存", LEVEL_OK,
249
+ f"{size_mb:.1f}MB, {file_count} 文件" +
250
+ (f" ({expired_count} 已过期)" if expired_count else ""))
251
+
252
+
253
+ def _check_database() -> CheckResult:
254
+ """检查数据库完整性"""
255
+ try:
256
+ from db_backend import get_backend
257
+ backend = get_backend()
258
+ except Exception:
259
+ return CheckResult("数据库", LEVEL_OK, "尚未创建(首次安装时自动初始化)")
260
+
261
+ db_path = Path.home() / ".gitinstall" / "data.db"
262
+ if backend.backend_type == "sqlite" and not db_path.exists():
263
+ return CheckResult("数据库", LEVEL_OK, "尚未创建(首次安装时自动初始化)")
264
+
265
+ try:
266
+ # 完整性检查
267
+ integrity = backend.integrity_check()
268
+ if integrity != "ok":
269
+ return CheckResult("数据库", LEVEL_ERROR, f"数据库损坏: {integrity}",
270
+ fix_hint=f"备份后删除: mv {db_path} {db_path}.bak")
271
+
272
+ # 统计数据
273
+ tables = {}
274
+ for table in ["events", "install_telemetry", "plans_history", "users"]:
275
+ try:
276
+ tables[table] = backend.table_row_count(table)
277
+ except Exception:
278
+ pass
279
+
280
+ # 文件权限检查(仅 SQLite)
281
+ perm_ok = True
282
+ if backend.backend_type == "sqlite" and db_path.exists():
283
+ mode = oct(db_path.stat().st_mode)[-3:]
284
+ perm_ok = mode in ("600", "644", "700")
285
+
286
+ stats = ", ".join(f"{k}: {v}" for k, v in tables.items() if v > 0)
287
+ msg = f"正常 ({stats})" if stats else "正常(空数据库)"
288
+ if not perm_ok:
289
+ return CheckResult("数据库", LEVEL_WARN, f"{msg},权限 {mode} 不安全",
290
+ fix_hint=f"修复: chmod 600 {db_path}")
291
+ return CheckResult("数据库", LEVEL_OK, msg)
292
+
293
+ except Exception as e:
294
+ return CheckResult("数据库", LEVEL_ERROR, f"无法打开: {e}")
295
+
296
+
297
+ def _check_gpu() -> CheckResult:
298
+ """检查 GPU / AI 硬件"""
299
+ try:
300
+ from hw_detect import get_gpu_info
301
+ gpu = get_gpu_info()
302
+ gpu_type = gpu.get("type", "cpu_only")
303
+ gpu_name = gpu.get("name", "")
304
+ vram = gpu.get("vram_gb")
305
+
306
+ if gpu_type == "apple_mps":
307
+ return CheckResult("GPU / AI 硬件", LEVEL_OK,
308
+ f"Apple Silicon: {gpu_name} (MPS 已就绪)")
309
+ elif gpu_type == "nvidia_cuda":
310
+ cuda = gpu.get("cuda_version", "?")
311
+ vram_str = f", {vram}GB VRAM" if vram else ""
312
+ return CheckResult("GPU / AI 硬件", LEVEL_OK,
313
+ f"NVIDIA: {gpu_name} (CUDA {cuda}{vram_str})")
314
+ elif gpu_type == "amd_rocm":
315
+ return CheckResult("GPU / AI 硬件", LEVEL_OK,
316
+ f"AMD: {gpu_name} (ROCm)")
317
+ else:
318
+ return CheckResult("GPU / AI 硬件", LEVEL_INFO,
319
+ "仅 CPU(AI/ML 项目可能较慢)",
320
+ detail="大部分项目不需要 GPU,AI/ML 项目推荐 GPU 加速")
321
+ except Exception:
322
+ return CheckResult("GPU / AI 硬件", LEVEL_INFO, "检测跳过")
323
+
324
+
325
+ def _check_disk_space() -> CheckResult:
326
+ """检查磁盘空间"""
327
+ try:
328
+ total, used, free = shutil.disk_usage(str(Path.home()))
329
+ free_gb = free / (1024 ** 3)
330
+ total_gb = total / (1024 ** 3)
331
+ pct = (used / total) * 100
332
+
333
+ if free_gb < 1:
334
+ return CheckResult("磁盘空间", LEVEL_ERROR,
335
+ f"仅剩 {free_gb:.1f}GB 可用空间",
336
+ fix_hint="磁盘空间不足,安装大项目可能失败")
337
+ elif free_gb < 5:
338
+ return CheckResult("磁盘空间", LEVEL_WARN,
339
+ f"剩余 {free_gb:.1f}GB / {total_gb:.0f}GB ({pct:.0f}% 已用)",
340
+ fix_hint="建议保留至少 5GB 空间用于项目安装")
341
+ return CheckResult("磁盘空间", LEVEL_OK,
342
+ f"剩余 {free_gb:.1f}GB / {total_gb:.0f}GB ({pct:.0f}% 已用)")
343
+ except Exception:
344
+ return CheckResult("磁盘空间", LEVEL_INFO, "检测跳过")
345
+
346
+
347
+ def _check_security() -> CheckResult:
348
+ """安全配置审计"""
349
+ issues = []
350
+ # 检查 .gitinstall 目录权限
351
+ gi_dir = Path.home() / ".gitinstall"
352
+ if gi_dir.exists():
353
+ mode = oct(gi_dir.stat().st_mode)[-3:]
354
+ if mode not in ("700", "755"):
355
+ issues.append(f"~/.gitinstall 目录权限 {mode},建议 700")
356
+
357
+ # 检查是否存在不安全的 API key 存储
358
+ shell_files = [".bashrc", ".zshrc", ".bash_profile", ".profile"]
359
+ for sf in shell_files:
360
+ p = Path.home() / sf
361
+ if p.exists():
362
+ try:
363
+ content = p.read_text(errors="ignore")
364
+ for key_name in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GITHUB_TOKEN"]:
365
+ if f"export {key_name}=" in content:
366
+ # 只是信息提示,不算错误
367
+ pass
368
+ except Exception:
369
+ pass
370
+
371
+ if issues:
372
+ return CheckResult("安全", LEVEL_WARN, f"发现 {len(issues)} 项安全建议",
373
+ detail="; ".join(issues))
374
+ return CheckResult("安全", LEVEL_OK, "配置安全")
375
+
376
+
377
+ def _check_skills() -> CheckResult:
378
+ """检查 Skills 插件目录"""
379
+ skills_dir = Path.home() / ".gitinstall" / "skills"
380
+ if not skills_dir.exists():
381
+ return CheckResult("Skills 插件", LEVEL_INFO,
382
+ "未安装任何 Skill(使用 gitinstall skills install 安装)")
383
+
384
+ skills = [d.name for d in skills_dir.iterdir() if d.is_dir() and (d / "skill.json").exists()]
385
+ if skills:
386
+ return CheckResult("Skills 插件", LEVEL_OK, f"已安装 {len(skills)} 个: {', '.join(skills[:5])}")
387
+ return CheckResult("Skills 插件", LEVEL_INFO, "Skills 目录存在但无已安装插件")
388
+
389
+
390
+ # ─────────────────────────────────────────────
391
+ # 主诊断入口
392
+ # ─────────────────────────────────────────────
393
+
394
+ def run_doctor(verbose: bool = False) -> DoctorReport:
395
+ """运行完整诊断,返回报告"""
396
+ start = time.time()
397
+ report = DoctorReport(timestamp=start)
398
+
399
+ # 基础环境
400
+ report.checks.append(_check_python())
401
+ report.checks.append(_check_git())
402
+ report.checks.append(_check_disk_space())
403
+
404
+ # 包管理器
405
+ report.checks.extend(_check_package_managers())
406
+
407
+ # GPU
408
+ report.checks.append(_check_gpu())
409
+
410
+ # 网络与 API
411
+ report.checks.append(_check_github_api())
412
+
413
+ # LLM
414
+ report.checks.extend(_check_llm_keys())
415
+
416
+ # 数据存储
417
+ report.checks.append(_check_cache())
418
+ report.checks.append(_check_database())
419
+
420
+ # 安全
421
+ report.checks.append(_check_security())
422
+
423
+ # Skills
424
+ report.checks.append(_check_skills())
425
+
426
+ report.duration_ms = (time.time() - start) * 1000
427
+ return report
428
+
429
+
430
+ def format_doctor_report(report: DoctorReport) -> str:
431
+ """格式化诊断报告为终端可读字符串"""
432
+ lines = []
433
+ lines.append("")
434
+ lines.append("🩺 gitinstall doctor — 系统诊断报告")
435
+ lines.append("═" * 55)
436
+
437
+ icon_map = {
438
+ LEVEL_OK: "✅",
439
+ LEVEL_WARN: "⚠️ ",
440
+ LEVEL_ERROR: "❌",
441
+ LEVEL_INFO: "ℹ️ ",
442
+ }
443
+
444
+ # 按类型分组显示
445
+ for check in report.checks:
446
+ icon = icon_map.get(check.level, "?")
447
+ lines.append(f" {icon} {check.name}: {check.message}")
448
+ if check.detail:
449
+ lines.append(f" {check.detail}")
450
+ if check.fix_hint and check.level in (LEVEL_WARN, LEVEL_ERROR):
451
+ lines.append(f" 💡 {check.fix_hint}")
452
+
453
+ # 汇总
454
+ lines.append("")
455
+ lines.append("─" * 55)
456
+ total = len(report.checks)
457
+ summary_parts = [f"{report.ok_count} 通过"]
458
+ if report.warn_count:
459
+ summary_parts.append(f"{report.warn_count} 警告")
460
+ if report.error_count:
461
+ summary_parts.append(f"{report.error_count} 错误")
462
+
463
+ status = "✅ 系统就绪" if report.all_ok else "⚠️ 存在需要关注的问题"
464
+ lines.append(f" {status} — {total} 项检查: {', '.join(summary_parts)}")
465
+ lines.append(f" 诊断耗时: {report.duration_ms:.0f}ms")
466
+ lines.append("")
467
+
468
+ return "\n".join(lines)
469
+
470
+
471
+ def doctor_to_dict(report: DoctorReport) -> dict:
472
+ """将诊断报告转为 JSON 可序列化的 dict"""
473
+ return {
474
+ "status": "ok" if report.all_ok else "warning" if report.error_count == 0 else "error",
475
+ "timestamp": report.timestamp,
476
+ "duration_ms": report.duration_ms,
477
+ "summary": {
478
+ "total": len(report.checks),
479
+ "ok": report.ok_count,
480
+ "warn": report.warn_count,
481
+ "error": report.error_count,
482
+ },
483
+ "checks": [
484
+ {
485
+ "name": c.name,
486
+ "level": c.level,
487
+ "message": c.message,
488
+ "detail": c.detail,
489
+ "fix_hint": c.fix_hint,
490
+ }
491
+ for c in report.checks
492
+ ],
493
+ }