Files
settled-reach/tooling/archive/pql-migrate/restructure_decisions.py
T
jpmschweitzerandClaude Opus 5.5 4537b71b92 refactor(tooling): T-1290 — the wiki domain, and the renderer that must not run
`reach wiki stats` and `reach wiki gttr-hook` replace tooling/db/wiki_sync.py
and populate_gttr_hook.py. Both are output-identical to the originals:
`stats` byte-for-byte, and all 301 extracted GTTR hooks line-for-line.

wiki_sync.py moved whole, but generate_wiki() and import_from_wiki() are NOT
verbs. Before porting, the old `--generate` was run against a clean tree to get
a parity baseline. It changed all 301 system pages, +940 / -10,761, and was
reverted at once. It deletes the Celestial Bodies / Stations blocks (owned by
the Rust atlas sync, which it does not know about), deletes the
Industries / Exports / Imports rows (nothing writes those any more), and
rewrites star types where systems.db and the pages disagree. D-262, CLAUDE.md
and the wiki skill all described it as the routine, prose-preserving render.
CLAUDE.md and the skill now say not to run it; D-262 needs amending — T-1292.

Provenance moves to tooling/archive/, with a README naming what each script
did and why it is not run:

- pql-migrate/ (the T-1271 ruling)
- wiki-bootstrap/: assign-astro-ids + its catalog, migrate-s-to-gj,
  patch-core-sector (hardcodes a dead path), fill-missing-globes,
  generate-stubs and find-stubs (finds 0 stubs — Phase 1 is done),
  backfill_cultural_corridor (a raw systems.db patch script, outside D-262),
  and process-wiki-system-changes, whose last step is the destructive render

Also:

- stats() printed "run import first" and exited 0 when a table was missing;
  it now fails with a remedy. generate_wiki() counted created pages after
  writing them, so `created` was always 0.
- tooling/godot-cold-parse and godot-parse-sweep were never retired after
  T-1283, and the pr-process skill still told agents to run them. Removed;
  the skill and parse_sweep.gd now name the reach verbs.
- systems.db re-stamped: schema comments changed, and the stamp records the
  schema file's SHA for tamper detection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:25:51 +02:00

158 lines
6.0 KiB
Python

#!/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}/<domain>.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<path>[A-Za-z0-9_-]+\.md)(?P<frag>#[^)]*)?\)")
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-<domain>.md -> governance/questions/<domain>.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/<domain>.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()