#!/usr/bin/env python3 """Resolve open hub decisions that a repo decision log already records as resolved. Repo files are authoritative (ADR-001); the hub is a read model. This scans ~/*/decisions/*.md for fenced yaml blocks with `id`, `title`, `status: resolved` and resolves the open hub decision whose title matches exactly (whitespace- normalized, case-insensitive). Uses the sanctioned POST /decisions/{id}/resolve. Usage: sync_resolved_decisions.py [--apply] [--hub http://127.0.0.1:8000] """ import argparse, glob, json, os, re, time, urllib.error, urllib.request def get_json(url, attempts=3, delay=5): # The workstation hub forward occasionally drops a first request; retry reads only. for i in range(attempts): try: with urllib.request.urlopen(url, timeout=30) as r: return json.load(r) except (ConnectionError, urllib.error.URLError): if i == attempts - 1: raise time.sleep(delay * (i + 1)) def norm(s): return re.sub(r"\s+", " ", s.strip().strip("'\"")).lower() def repo_resolved(root): out = {} for f in glob.glob(os.path.join(root, "*", "decisions", "*.md")): text = open(f, encoding="utf-8", errors="ignore").read() for b in re.findall(r"```yaml\n(.*?)```", text, re.S): g = lambda k: (re.search(rf"^{k}:\s*(.*)$", b, re.M) or [None, ""])[1] if g("status").strip() in ("resolved", "accepted") and g("title"): out[norm(g("title"))] = (g("id").strip(), os.path.relpath(f, root)) return out def main(): ap = argparse.ArgumentParser() ap.add_argument("--hub", default="http://127.0.0.1:8000") ap.add_argument("--root", default=os.path.expanduser("~")) ap.add_argument("--apply", action="store_true") a = ap.parse_args() known = repo_resolved(a.root) data = get_json(f"{a.hub}/decisions/?status=open&limit=500") data = data.get("items", data) if isinstance(data, dict) else data hits = 0 for d in data: t = norm(d["title"]) m = known.get(t) or next((v for k, v in known.items() if min(len(k), len(t)) >= 40 and (k.startswith(t) or t.startswith(k))), None) if not m: continue hits += 1 rid, path = m print(f"{d['id'][:8]} {rid} {path} | {d['title'][:70]}") if a.apply: body = json.dumps({"rationale": f"Resolved in repo decision log {path} ({rid}); hub synced to repo state.", "decided_by": "bernd"}).encode() req = urllib.request.Request(f"{a.hub}/decisions/{d['id']}/resolve", data=body, headers={"Content-Type": "application/json"}, method="POST") urllib.request.urlopen(req).read() print(f"{hits} open hub decisions match resolved repo records" + ("" if a.apply else " (dry run)")) if __name__ == "__main__": main()