#!/usr/bin/env python3 """Restructure decisions/ into the pql governance/ DQR tree (pql migration, Phase 1). Moves the flat decisions/*.md layout into governance/{decisions,questions,rejected}/.md, which is what `pql decisions sync` parses (type from subdir, domain from filename stem). Link rewrites are TOKEN-PRESERVING: only the relative `foo.md` path portion of a markdown link changes; the `[D-NNN]` bracket text and `#d-nnn-...` anchor stay byte-identical, so pql's reference extraction is unaffected regardless of whether it reads bracket text, anchor, or path. Default is a dry run (prints every planned move + link change). Pass --apply to execute. Idempotent-ish: re-running after apply is a no-op for moves (sources already gone). """ import re import subprocess import sys from pathlib import Path # One-shot transform, already applied (decisions/ -> governance/). Re-running is # unsupported: the git mv calls would fail on already-moved sources. The path below # is the canonical main checkout where it was run; kept committed for provenance. ROOT = Path("/var/mnt/data/projects/settled-reach/main") APPLY = "--apply" in sys.argv DSTEMS = {"architecture", "content", "economics", "perception", "process", "scope"} QSTEMS = ["architecture", "content", "perception", "process", "scope"] # R-record -> domain (R-001..R-010 platform/engine = architecture; R-011 economics; R-012 perception) def rdomain(n: int) -> str: if n == 11: return "economics" if n == 12: return "perception" return "architecture" REJECTED_DOMAINS = sorted({rdomain(n) for n in range(1, 13)}) # architecture, economics, perception LINK = re.compile(r"\]\((?P[A-Za-z0-9_-]+\.md)(?P#[^)]*)?\)") def new_target_path(path: str, frag: str, src_subdir: str): """Return the rewritten relative path for a link target, or None to leave unchanged.""" stem = path[:-3] if path == "rejected.md": m = re.match(r"#r-0*(\d+)", frag or "") rd = rdomain(int(m.group(1))) if m else "architecture" ttype, tdom = "rejected", rd elif path.startswith("questions-"): ttype, tdom = "questions", stem[len("questions-"):] elif path in ("questions.md", "README.md"): ttype, tdom = "index", None elif stem in DSTEMS: ttype, tdom = "decisions", stem else: return None # unknown stem; leave as-is if ttype == "index": return "README.md" if src_subdir == "root" else "../README.md" if ttype == src_subdir: # same subdir: bare filename return f"{tdom}.md" # cross-subdir prefix = "" if src_subdir == "root" else "../" return f"{prefix}{ttype}/{tdom}.md" def rewrite_links(text: str, src_subdir: str): changes = [] def repl(mo): path, frag = mo.group("path"), mo.group("frag") or "" np = new_target_path(path, frag, src_subdir) if np is None or np == path: return mo.group(0) changes.append((f"{path}{frag}", f"{np}{frag}")) return f"]({np}{frag})" return LINK.sub(repl, text), changes def git(*args): subprocess.run(["git", "-C", str(ROOT), *args], check=True) def log_changes(rel, changes): if changes: print(f" {rel}: {len(changes)} link(s) rewritten") for old, new in changes: print(f" {old} -> {new}") def move_and_rewrite(src_rel, dst_rel, src_subdir): src, dst = ROOT / src_rel, ROOT / dst_rel text = src.read_text(encoding="utf-8") new_text, changes = rewrite_links(text, src_subdir) print(f"MOVE {src_rel} -> {dst_rel}") log_changes(dst_rel, changes) if APPLY: dst.parent.mkdir(parents=True, exist_ok=True) git("mv", src_rel, dst_rel) dst.write_text(new_text, encoding="utf-8") def split_rejected(): src_rel = "decisions/rejected.md" text = (ROOT / src_rel).read_text(encoding="utf-8") # split into records on '### R-' headings parts = re.split(r"(?m)^(?=### R-\d)", text) head = parts[0] # title + intro before first record records = parts[1:] buckets = {d: [] for d in REJECTED_DOMAINS} for rec in records: m = re.match(r"### R-0*(\d+)", rec) n = int(m.group(1)) # strip a trailing '---' / footer that may cling to the last record body = re.split(r"(?m)^---\s*$", rec)[0].rstrip() + "\n" buckets[rdomain(n)].append(body) print(f"SPLIT {src_rel} -> {len(REJECTED_DOMAINS)} domain files") for dom, recs in buckets.items(): dst_rel = f"governance/rejected/{dom}.md" title = f"# Rejected Alternatives — {dom.capitalize()}\n\nRejected proposals in the **{dom}** domain, rationale preserved for the audit trail.\n\n" content = title + "\n".join(recs).rstrip() + "\n" content, changes = rewrite_links(content, "rejected") print(f" -> {dst_rel}: {len(recs)} record(s)") log_changes(dst_rel, changes) if APPLY: (ROOT / dst_rel).parent.mkdir(parents=True, exist_ok=True) (ROOT / dst_rel).write_text(content, encoding="utf-8") if APPLY: git("rm", src_rel) def main(): print(f"=== restructure_decisions.py ({'APPLY' if APPLY else 'DRY-RUN'}) ===\n") # 1. D-domain files -> governance/decisions/ for s in sorted(DSTEMS): move_and_rewrite(f"decisions/{s}.md", f"governance/decisions/{s}.md", "decisions") print() # 2. questions-.md -> governance/questions/.md for q in QSTEMS: move_and_rewrite(f"decisions/questions-{q}.md", f"governance/questions/{q}.md", "questions") print() # 3. rejected.md -> governance/rejected/.md (split by domain) split_rejected() print() # 4. index files (README.md, questions.md) are folded into governance/README.md by hand; # remove the originals here. for idx in ("decisions/README.md", "decisions/questions.md"): print(f"REMOVE {idx} (folded into governance/README.md)") if APPLY: git("rm", idx) print("\nDone." + ("" if APPLY else " (dry run — pass --apply to execute)")) if __name__ == "__main__": main()