Record round-2 founder decisions; add deterministic flow, requalify and decision-sync scripts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
085153f9a7
commit
0f032696c3
3 changed files with 191 additions and 0 deletions
58
scripts/sync_resolved_decisions.py
Normal file
58
scripts/sync_resolved_decisions.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#!/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, urllib.request
|
||||
|
||||
|
||||
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 = json.load(urllib.request.urlopen(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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue