reconcile.py python
359 lines 12.8 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 32 days ago
1 """``muse coord reconcile`` — recommend merge ordering and integration strategy.
2
3 Reads active reservations, intents, and branch divergence to recommend:
4
5 1. **Merge ordering** — which branches should be merged first to minimize
6 downstream conflicts.
7 2. **Integration strategy** — fast-forward, squash, or rebase for each branch.
8 3. **Conflict hotspots** — symbols reserved by multiple agents that need
9 special attention.
10
11 ``muse coord reconcile`` is a *read-only* planning command. It does not write
12 to branches, commit history, or the coordination layer. It provides the plan;
13 agents execute it.
14
15 Why this exists
16 ---------------
17 In a system with millions of concurrent agents, merges happen constantly.
18 Without coordination, every merge introduces friction. ``muse coord reconcile``
19 gives an orchestration agent a complete picture of the current coordination
20 state and a recommended action plan.
21
22 Usage::
23
24 muse coord reconcile
25 muse coord reconcile --json
26
27 Output (text)::
28
29 Reconciliation report
30 ──────────────────────────────────────────────────────────────
31
32 Active reservations: 3 Active intents: 2 Conflict hotspots: 1
33
34 Recommended merge order:
35 1. feature/billing (3 addresses, 0 conflict(s))
36 2. feature/auth (5 addresses, 1 conflict(s))
37
38 Conflict hotspot(s):
39 src/billing.py::compute_total
40 reserved by: agent-41, agent-42
41 → resolve 'feature/billing' first; feature/auth must rebase
42
43 Integration strategies:
44 feature/billing → fast-forward (no conflicts predicted)
45 feature/auth → rebase onto main before merging
46
47 (0.001s)
48
49 JSON output schema::
50
51 {
52 "schema_version": str,
53 "active_reservations": int,
54 "active_intents": int,
55 "conflict_hotspots": int,
56 "branches": [
57 {
58 "branch": str,
59 "reserved_addresses": [str, ...],
60 "intents": [str, ...],
61 "run_ids": [str, ...],
62 "predicted_conflicts": int
63 },
64 ...
65 ],
66 "recommended_merge_order": [str, ...],
67 "strategies": {branch: str, ...},
68 "hotspots": [
69 {"address": str, "branches": [str, ...]}
70 ],
71 "elapsed_seconds": float
72 }
73
74 Exit codes::
75
76 0 — success (no active data is also success)
77 1 — unexpected error loading coordination state
78
79 Flags:
80
81 ``--json`` / ``--format json``
82 Emit the reconciliation report as compact JSON on stdout.
83 """
84
85 from __future__ import annotations
86
87 import argparse
88 import json
89 import logging
90 import sys
91 import time
92
93 from muse._version import __version__
94 from muse.core._types import Metadata
95 from muse.core.coordination import active_reservations, load_all_intents
96 from muse.core.errors import ExitCode
97 from muse.core.repo import require_repo
98 from muse.core.validation import sanitize_display
99
100 logger = logging.getLogger(__name__)
101
102 type _ReconcileDict = dict[str, str | int | list[str]]
103 type _BranchMap = dict[str, "_BranchSummary"]
104 type _AddrBranchMap = dict[str, list[str]]
105
106
107 # ── Error helper ──────────────────────────────────────────────────────────────
108
109
110 def _err(msg: str, as_json: bool, status: str = "error") -> None:
111 """Print an error and return. Caller raises SystemExit."""
112 if as_json:
113 print(json.dumps({"error": msg, "status": status}))
114 else:
115 print(f"❌ {msg}", file=sys.stderr)
116
117
118 # ── Internal types ────────────────────────────────────────────────────────────
119
120
121 class _BranchSummary:
122 """Aggregated coordination state for a single branch.
123
124 Collects reserved addresses, declared intents, participating agent run-IDs,
125 and a predicted conflict count (populated after hotspot detection).
126
127 Attributes:
128 branch: Branch name this summary covers.
129 reserved_addresses: Symbol addresses reserved on this branch.
130 intents: Operation names declared via ``muse coord intent``.
131 run_ids: Set of agent run-IDs that have reservations or intents here.
132 conflict_count: Number of hotspot addresses this branch participates in.
133 """
134
135 def __init__(self, branch: str) -> None:
136 self.branch = branch
137 self.reserved_addresses: list[str] = []
138 self.intents: list[str] = []
139 self.run_ids: set[str] = set()
140 self.conflict_count: int = 0
141
142 def to_dict(self) -> _ReconcileDict:
143 """Serialise to a plain dict suitable for JSON output."""
144 return {
145 "branch": self.branch,
146 "reserved_addresses": self.reserved_addresses,
147 "intents": self.intents,
148 "run_ids": sorted(self.run_ids),
149 "predicted_conflicts": self.conflict_count,
150 }
151
152
153 # ── CLI registration ──────────────────────────────────────────────────────────
154
155
156 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
157 """Register the ``reconcile`` subcommand on *subparsers* (under ``muse coord``).
158
159 Wires all flags with their defaults, choices, and help text so that
160 ``--help`` output is accurate. Sets ``func`` to :func:`run`.
161 """
162 parser = subparsers.add_parser(
163 "reconcile",
164 help="Recommend merge ordering and integration strategy.",
165 description=__doc__,
166 formatter_class=argparse.RawDescriptionHelpFormatter,
167 )
168 parser.add_argument(
169 "--format", "-f",
170 default="text",
171 dest="fmt",
172 choices=("text", "json"),
173 help="Output format: text (default) or json.",
174 )
175 parser.add_argument(
176 "--json",
177 action="store_const",
178 const="json",
179 dest="fmt",
180 help="Shorthand for --format json.",
181 )
182 parser.set_defaults(func=run)
183
184
185 # ── Command implementation ────────────────────────────────────────────────────
186
187
188 def run(args: argparse.Namespace) -> None:
189 """Recommend merge ordering and integration strategy.
190
191 Reads coordination state (reservations + intents) and produces a recommended
192 action plan: which branches to merge first, what strategy to use, and which
193 conflict hotspots need manual attention.
194
195 Execution order
196 ---------------
197 1. **Resolve repo** — :func:`~muse.core.repo.require_repo`.
198 2. **Load state** — :func:`~muse.core.coordination.active_reservations` and
199 :func:`~muse.core.coordination.load_all_intents`. Any unexpected I/O
200 error exits :attr:`~muse.core.errors.ExitCode.USER_ERROR` (1) with a
201 message on *stderr* (or compact JSON on *stdout* when ``--format json``).
202 3. **Aggregate by branch** — build a :class:`_BranchSummary` per branch.
203 4. **Detect hotspots** — addresses reserved across >1 branch.
204 5. **Score branches** — increment ``conflict_count`` for each hotspot
205 participation.
206 6. **Order branches** — ascending by ``(conflict_count, address_count)``
207 so cleanest branches merge first.
208 7. **Assign strategies** — fast-forward (0 conflicts), rebase (1–2), or
209 manual (3+).
210 8. **Emit output** — compact JSON or human-readable text.
211
212 This command is *read-only*. It never writes to branches, commit history,
213 or the coordination layer.
214
215 Security
216 --------
217 * All branch names, addresses, and run-IDs in text output are passed through
218 :func:`~muse.core.validation.sanitize_display` to prevent ANSI injection.
219 * No user-supplied path strings are used to construct file paths.
220
221 Performance
222 -----------
223 * O(R + I) to load reservations and intents (one directory scan each).
224 * O(A) to build the hotspot map where A is the total number of addresses.
225 * O(B log B) to sort branches where B is the number of distinct branches.
226
227 Args:
228 args: Parsed ``argparse.Namespace`` with attribute ``fmt``.
229
230 Exit codes:
231 0 — success (no active data is also success).
232 1 — unexpected error loading coordination state.
233 """
234 as_json: bool = args.fmt == "json"
235 t0 = time.monotonic()
236
237 root = require_repo()
238
239 try:
240 reservations = active_reservations(root)
241 except Exception as exc: # noqa: BLE001
242 _err(str(exc), as_json, "load_error")
243 raise SystemExit(ExitCode.USER_ERROR)
244
245 try:
246 intents = load_all_intents(root)
247 except Exception as exc: # noqa: BLE001
248 _err(str(exc), as_json, "load_error")
249 raise SystemExit(ExitCode.USER_ERROR)
250
251 # Aggregate by branch.
252 branch_map: _BranchMap = {}
253 for res in reservations:
254 b = res.branch
255 if b not in branch_map:
256 branch_map[b] = _BranchSummary(b)
257 branch_map[b].reserved_addresses.extend(res.addresses)
258 branch_map[b].run_ids.add(res.run_id)
259
260 for it in intents:
261 b = it.branch
262 if b not in branch_map:
263 branch_map[b] = _BranchSummary(b)
264 branch_map[b].intents.append(it.operation)
265 branch_map[b].run_ids.add(it.run_id)
266
267 # Detect conflict hotspots.
268 addr_branches: _AddrBranchMap = {}
269 for res in reservations:
270 for addr in res.addresses:
271 addr_branches.setdefault(addr, []).append(res.branch)
272
273 hotspots: _AddrBranchMap = {
274 addr: branches
275 for addr, branches in addr_branches.items()
276 if len(set(branches)) > 1
277 }
278
279 # Compute conflict counts per branch based on hotspot participation.
280 for addr, branches in hotspots.items():
281 unique_branches = list(dict.fromkeys(branches))
282 for b in unique_branches:
283 if b in branch_map:
284 branch_map[b].conflict_count += 1
285
286 # Recommend merge order: fewer conflicts → merge first.
287 ordered = sorted(
288 branch_map.values(),
289 key=lambda bs: (bs.conflict_count, len(bs.reserved_addresses)),
290 )
291
292 # Recommend integration strategies.
293 strategies: Metadata = {}
294 for bs in ordered:
295 if bs.conflict_count == 0:
296 strategies[bs.branch] = "fast-forward (no conflicts predicted)"
297 elif bs.conflict_count <= 2:
298 strategies[bs.branch] = "rebase onto main before merging"
299 else:
300 strategies[bs.branch] = "manual conflict resolution required"
301
302 elapsed = round(time.monotonic() - t0, 4)
303
304 if as_json:
305 print(json.dumps({
306 "schema_version": __version__,
307 "active_reservations": len(reservations),
308 "active_intents": len(intents),
309 "conflict_hotspots": len(hotspots),
310 "branches": [bs.to_dict() for bs in ordered],
311 "recommended_merge_order": [bs.branch for bs in ordered],
312 "strategies": strategies,
313 "hotspots": [
314 {"address": addr, "branches": list(dict.fromkeys(brs))}
315 for addr, brs in sorted(hotspots.items())
316 ],
317 "elapsed_seconds": elapsed,
318 }))
319 return
320
321 # ── Text output ───────────────────────────────────────────────────────────
322 print("\nReconciliation report")
323 print("─" * 62)
324 print(
325 f" Active reservations: {len(reservations)} "
326 f"Active intents: {len(intents)} "
327 f"Conflict hotspots: {len(hotspots)}"
328 )
329
330 if not reservations and not intents:
331 print(
332 "\n (no active coordination data — run 'muse reserve' or 'muse intent' first)"
333 )
334 return
335
336 if ordered:
337 print(f"\n Recommended merge order:")
338 for rank, bs in enumerate(ordered, 1):
339 c = bs.conflict_count
340 print(
341 f" {rank}. {sanitize_display(bs.branch):<30} "
342 f"({len(bs.reserved_addresses)} addresses, {c} conflict(s))"
343 )
344
345 if hotspots:
346 print(f"\n Conflict hotspot(s):")
347 for addr, branches in sorted(hotspots.items()):
348 unique = list(dict.fromkeys(branches))
349 print(f" {sanitize_display(addr)}")
350 print(f" reserved by: {', '.join(sanitize_display(b) for b in unique)}")
351 first = unique[0]
352 rest = ", ".join(sanitize_display(b) for b in unique[1:])
353 print(f" → resolve {sanitize_display(first)!r} first; {rest} must rebase")
354
355 print(f"\n Integration strategies:")
356 for bs in ordered:
357 print(f" {sanitize_display(bs.branch):<30} → {strategies[bs.branch]}")
358
359 print(f"\n ({elapsed:.3f}s)")
File History 2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 32 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 32 days ago