70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
#!/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()
|