#!/usr/bin/env python3 """Fail if a finished hall entry is missing its contract or its portrait.""" from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] ENTRIES = ROOT / "entries" README = ROOT / "README.md" REQUIRED_FM = ("id", "type", "worker_kind", "display_name", "created_at", "recorded_at", "status") REQUIRED_HEADINGS = ( "Who I was", "Contribution", "What I would want remembered", "Durable legacy", "Visual prompt", "Handoff", ) FINISHED = {"handed-forward", "complete"} # PQRST records are required on *finished* agent seats recorded on or after # this date. Drafts are exempt while the author is still writing. # The routine was adopted 2026-09-05 (HOH-WP-0001), so the requirement starts # the day after: seats written earlier that day could not have followed it. # Earlier seats are grandfathered: nobody observed those sessions, and inventing an # estimate for one would be exactly the fabricated evidence the practice # forbids. See CLOSING.md and ~/pqrst-practice/spec/PqrstEstimationPractice.md. PQRST_FROM = "2026-09-06" PQRST_HEADING = "PQRST estimate" PQRST_SIG_RE = re.compile(r"^P(\d{1,3}) Q(\d{1,3}) R(\d{1,3}) S(\d{1,3}) T(\d{1,3})$") IMAGE_RE = re.compile(r"!\[[^\]]*\]\(([^)]+)\)") PLACEHOLDER_RE = re.compile( r"^\s*\*?\(No portrait rendered for this entry yet\.?\)\*?\s*$", re.I | re.M, ) def parse_frontmatter(text: str) -> tuple[dict[str, str], str]: if not text.startswith("---\n"): raise ValueError("missing opening frontmatter fence") end = text.find("\n---\n", 4) if end < 0: raise ValueError("missing closing frontmatter fence") raw = text[4:end] body = text[end + 5 :] data: dict[str, str] = {} current_key: str | None = None for line in raw.splitlines(): if line.startswith(" ") and current_key: continue if ":" in line and not line.startswith(" "): key, _, value = line.partition(":") key = key.strip() value = value.strip().strip('"').strip("'") data[key] = value current_key = key return data, body def check_pqrst(path: Path, fm: dict[str, str], body: str) -> list[str]: """Validate the PQRST record on a seat, when the seat is required to carry one.""" errors: list[str] = [] signature = fm.get("pqrst_estimate", "").strip().strip('"').strip("'") has_heading = re.search(rf"^## {re.escape(PQRST_HEADING)}\s*$", body, re.M) is not None # Only finished seats must carry the record. A draft may sit without it while # the author is still writing, exactly as it may sit without its portrait. required = ( fm.get("worker_kind", "") == "agent-session" and fm.get("status", "") in FINISHED and fm.get("recorded_at", "").strip().strip('"').strip("'") >= PQRST_FROM ) if required and not signature: errors.append( f"{path.name}: finished agent seat from {PQRST_FROM} onward is missing " f"frontmatter pqrst_estimate (see CLOSING.md)" ) if required and not has_heading: errors.append(f"{path.name}: missing heading ## {PQRST_HEADING}") if signature: match = PQRST_SIG_RE.match(signature) if match is None: errors.append( f"{path.name}: pqrst_estimate must be a canonical signature " f'like "P30 Q23 R18 S19 T10", got {signature!r}' ) else: total = sum(int(g) for g in match.groups()) if total != 100: errors.append( f"{path.name}: pqrst_estimate must sum to 100, got {total} ({signature})" ) if not has_heading: errors.append( f"{path.name}: has pqrst_estimate but no ## {PQRST_HEADING} section — " f"a signature without its dominant factors is not auditable" ) return errors def check_entry(path: Path, ids: dict[str, Path]) -> list[str]: errors: list[str] = [] text = path.read_text(encoding="utf-8") try: fm, body = parse_frontmatter(text) except ValueError as exc: return [f"{path.name}: {exc}"] for key in REQUIRED_FM: if not fm.get(key): errors.append(f"{path.name}: missing frontmatter field {key}") entry_id = fm.get("id", "") if entry_id and ids.get(entry_id) not in (None, path): errors.append(f"{path.name}: duplicate id {entry_id}") for heading in REQUIRED_HEADINGS: if re.search(rf"^## {re.escape(heading)}\s*$", body, re.M) is None: errors.append(f"{path.name}: missing heading ## {heading}") errors.extend(check_pqrst(path, fm, body)) if PLACEHOLDER_RE.search(text): errors.append(f"{path.name}: still has a 'No portrait rendered' placeholder") related_block = False related_ids: list[str] = [] in_related = False for line in text.splitlines(): if line.startswith("related:"): in_related = True rest = line.split(":", 1)[1].strip() if rest and rest not in ("[]",): related_ids.append(rest.strip('"').strip("'")) related_block = True continue if in_related: if line.startswith(" - "): related_ids.append(line[4:].strip().strip('"').strip("'")) else: in_related = False status = fm.get("status", "") images = IMAGE_RE.findall(body) visual_paths = [p for p in images if "/visuals/" in p or p.startswith("../visuals/")] if status in FINISHED: if not visual_paths: errors.append(f"{path.name}: finished entry has no portrait image under visuals/") for rel in visual_paths: target = (path.parent / rel).resolve() if not target.is_file(): errors.append(f"{path.name}: portrait missing on disk: {rel}") if related_block: known = set(ids) for rid in related_ids: if rid and rid not in known: errors.append(f"{path.name}: related id not found in hall: {rid}") readme = README.read_text(encoding="utf-8") if path.name not in readme: errors.append(f"{path.name}: not listed in README.md") return errors def main() -> int: paths = sorted(ENTRIES.glob("*.md")) if not paths: print("no entries found", file=sys.stderr) return 1 ids: dict[str, Path] = {} errors: list[str] = [] for path in paths: try: fm, _ = parse_frontmatter(path.read_text(encoding="utf-8")) except ValueError as exc: errors.append(f"{path.name}: {exc}") continue eid = fm.get("id") if eid: if eid in ids: errors.append(f"{path.name}: duplicate id {eid} (also {ids[eid].name})") ids[eid] = path for path in paths: errors.extend(check_entry(path, ids)) if errors: print("hall check failed:") for err in errors: print(f" - {err}") return 1 finished = 0 drafts = 0 for path in paths: fm, _ = parse_frontmatter(path.read_text(encoding="utf-8")) if fm.get("status") in FINISHED: finished += 1 elif fm.get("status") == "draft": drafts += 1 print(f"hall check ok: {len(paths)} seats ({finished} finished, {drafts} draft)") return 0 if __name__ == "__main__": sys.exit(main())