gabriel / muse public
annotate.py python
137 lines 4.9 KB
6acddccb fix: restore structured --help output for all CLI commands Gabriel Cardona <gabriel@tellurstori.com> 5d ago
1 """``muse annotate`` — attach CRDT-backed metadata to an existing commit.
2
3 Annotations use real CRDT semantics so that multiple agents can annotate the
4 same commit concurrently without conflicts:
5
6 - ``--reviewed-by`` merges into the commit's ``reviewed_by`` field using
7 **ORSet** semantics (set union — once added, a reviewer is never lost).
8 - ``--test-run`` increments the commit's ``test_runs`` field using
9 **GCounter** semantics (monotonically increasing).
10
11 These annotations are persisted directly in the commit JSON on disk. If two
12 agents race to annotate the same commit, the last writer wins for the raw
13 JSON, but the semantics are CRDT-safe: ORSet entries are unioned and GCounter
14 values are taken as the max.
15
16 Usage::
17
18 muse annotate abc1234 --reviewed-by agent-x
19 muse annotate abc1234 --reviewed-by human-bob --reviewed-by claude-v4
20 muse annotate abc1234 --test-run
21 muse annotate abc1234 --reviewed-by ci-bot --test-run
22 muse annotate # annotate HEAD
23 """
24
25 from __future__ import annotations
26
27 import argparse
28 import logging
29 import pathlib
30 import sys
31
32 from muse.core.crdts.or_set import ORSet
33 from muse.core.repo import require_repo
34 from muse.core.store import get_head_commit_id, overwrite_commit, read_commit, read_current_branch
35
36 logger = logging.getLogger(__name__)
37
38
39 def _resolve_commit_id(root: pathlib.Path, commit_arg: str | None) -> str | None:
40 """Return the resolved commit ID (HEAD branch if *commit_arg* is None)."""
41 if commit_arg is None:
42 branch = read_current_branch(root)
43 return get_head_commit_id(root, branch)
44 return commit_arg
45
46
47 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
48 """Register the annotate subcommand."""
49 parser = subparsers.add_parser(
50 "annotate",
51 help="Attach CRDT-backed annotations to an existing commit.",
52 description=__doc__,
53 formatter_class=argparse.RawDescriptionHelpFormatter,
54 )
55 parser.add_argument(
56 "commit_arg", nargs="?", default=None,
57 help="Commit ID to annotate (default: HEAD).",
58 )
59 parser.add_argument(
60 "--reviewed-by", default=None, dest="reviewed_by",
61 help="Add a reviewer (comma-separated for multiple: --reviewed-by 'alice,bob').",
62 )
63 parser.add_argument(
64 "--test-run", action="store_true", dest="test_run",
65 help="Increment the GCounter test-run count for this commit.",
66 )
67 parser.set_defaults(func=run)
68
69
70 def run(args: argparse.Namespace) -> None:
71 """Attach CRDT-backed annotations to an existing commit.
72
73 ``--reviewed-by`` uses ORSet semantics — a reviewer once added is never
74 removed. Pass multiple reviewers as a comma-separated
75 string: ``--reviewed-by 'alice,claude-v4'``.
76 ``--test-run`` uses GCounter semantics — the count is monotonically
77 increasing and concurrent increments are additive.
78 """
79 commit_arg: str | None = args.commit_arg
80 reviewed_by: str | None = args.reviewed_by
81 test_run: bool = args.test_run
82
83 root = require_repo()
84
85 commit_id = _resolve_commit_id(root, commit_arg)
86 if commit_id is None:
87 print("❌ No commit found.")
88 raise SystemExit(1)
89
90 record = read_commit(root, commit_id)
91 if record is None:
92 print(f"❌ Commit {commit_id!r} not found.")
93 raise SystemExit(1)
94
95 # Parse comma-separated reviewers into a list.
96 reviewers: list[str] = (
97 [r.strip() for r in reviewed_by.split(",") if r.strip()]
98 if reviewed_by else []
99 )
100
101 if not reviewers and not test_run:
102 print(f"ℹ️ commit {commit_id[:8]}")
103 if record.reviewed_by:
104 print(f" reviewed-by: {', '.join(sorted(record.reviewed_by))}")
105 else:
106 print(" reviewed-by: (none)")
107 print(f" test-runs: {record.test_runs}")
108 return
109
110 changed = False
111
112 if reviewers:
113 # ORSet merge: current set ∪ new reviewers.
114 current_set: ORSet = ORSet()
115 for r in record.reviewed_by:
116 current_set, _tok = current_set.add(r)
117 for r in reviewers:
118 current_set, _tok = current_set.add(r)
119 new_list = sorted(current_set.elements())
120 if new_list != sorted(record.reviewed_by):
121 record.reviewed_by = new_list
122 changed = True
123 for r in reviewers:
124 print(f"✅ Added reviewer: {r}")
125
126 if test_run:
127 # GCounter semantics: the value is monotonically non-decreasing.
128 # Each call increments by 1 (the GCounter join of two replicas
129 # would take max(a, b) per agent key; here we model the single-writer
130 # common case as a simple increment).
131 record.test_runs += 1
132 changed = True
133 print(f"✅ Test run recorded (total: {record.test_runs})")
134
135 if changed:
136 overwrite_commit(root, record)
137 print(f"[{commit_id[:8]}] annotation updated")