gabriel / muse public
verify.py python
112 lines 4.1 KB
00373ad0 feat: migrate CLI from typer to argparse (POSIX-compliant, order-independent) Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """``muse verify`` — whole-repository integrity check.
2
3 Walks every reachable commit from every branch ref and performs a three-tier
4 integrity check:
5
6 1. Every branch ref points to an existing commit.
7 2. Every commit's snapshot exists.
8 3. Every object referenced by every snapshot exists, and (unless
9 ``--no-objects``) its SHA-256 is recomputed to detect silent corruption.
10
11 This is Muse's equivalent of ``git fsck``. Run it periodically on long-lived
12 agent repositories or after recovering from a storage failure.
13
14 Usage::
15
16 muse verify # full check — re-hashes all objects
17 muse verify --no-objects # existence check only (faster)
18 muse verify --quiet # no output — exit code only
19 muse verify --format json # machine-readable report
20
21 Exit codes::
22
23 0 — all checks passed
24 1 — one or more integrity failures detected
25 3 — I/O error reading repository files
26 """
27
28 from __future__ import annotations
29
30 import argparse
31 import json
32 import logging
33 import sys
34
35 from muse.core.errors import ExitCode
36 from muse.core.repo import require_repo
37 from muse.core.validation import sanitize_display
38 from muse.core.verify import run_verify
39
40 logger = logging.getLogger(__name__)
41
42
43 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
44 """Register the verify subcommand."""
45 parser = subparsers.add_parser(
46 "verify",
47 help="Check repository integrity — commits, snapshots, and objects.",
48 description=__doc__,
49 )
50 parser.add_argument("--quiet", "-q", action="store_true",
51 help="No output — exit code only.")
52 parser.add_argument("--no-objects", action="store_true", dest="no_objects",
53 help="Existence check only (faster).")
54 parser.add_argument("--format", "-f", default="text", dest="fmt",
55 help="Output format: text or json.")
56 parser.set_defaults(func=run)
57
58
59 def run(args: argparse.Namespace) -> None:
60 """Check repository integrity — commits, snapshots, and objects.
61
62 Walks every reachable commit from every branch ref. For each commit,
63 verifies that the snapshot exists. For each snapshot, verifies that every
64 object file exists and (by default) re-hashes it to detect bit-rot.
65
66 The exit code is 0 when all checks pass, 1 when any failure is found.
67 Use ``--quiet`` in scripts that only care about the exit code.
68
69 Examples::
70
71 muse verify # full integrity check
72 muse verify --no-objects # fast existence-only check
73 muse verify --quiet && echo "healthy"
74 muse verify --format json | jq '.failures'
75 """
76 quiet: bool = args.quiet
77 no_objects: bool = args.no_objects
78 fmt: str = args.fmt
79
80 if fmt not in {"text", "json"}:
81 print(f"❌ Unknown --format '{sanitize_display(fmt)}'. Choose text or json.", file=sys.stderr)
82 raise SystemExit(ExitCode.USER_ERROR)
83
84 root = require_repo()
85
86 try:
87 result = run_verify(root, check_objects=not no_objects)
88 except OSError as exc:
89 if not quiet:
90 print(f"❌ I/O error during verify: {exc}", file=sys.stderr)
91 raise SystemExit(ExitCode.INTERNAL_ERROR) from exc
92
93 if quiet:
94 raise SystemExit(0 if result["all_ok"] else ExitCode.USER_ERROR)
95
96 if fmt == "json":
97 print(json.dumps(dict(result), indent=2))
98 else:
99 print(f"Checking refs... {result['refs_checked']} ref(s)")
100 print(f"Checking commits... {result['commits_checked']} commit(s)")
101 print(f"Checking snapshots... {result['snapshots_checked']} snapshot(s)")
102 action = "checked" if not no_objects else "verified (existence only)"
103 print(f"Checking objects... {result['objects_checked']} object(s) {action}")
104
105 if result["all_ok"]:
106 print("✅ Repository is healthy.")
107 else:
108 print(f"\n❌ {len(result['failures'])} integrity failure(s):")
109 for f in result["failures"]:
110 print(f" {f['kind']:<10} {f['id'][:24]} {f['error']}")
111
112 raise SystemExit(0 if result["all_ok"] else ExitCode.USER_ERROR)