HOH-WP-0002 T01–T03. Public slug is frontmatter id. Rendering lives in hall-render. T04 still needs the operator corpus call. Assistant: grok Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
224 lines
7.7 KiB
Python
Executable file
224 lines
7.7 KiB
Python
Executable file
#!/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")
|
|
# HOH-WP-0002-T01: the public URI slug is frontmatter id verbatim.
|
|
ID_RE = re.compile(r"^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$")
|
|
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}")
|
|
if entry_id and ID_RE.match(entry_id) is None:
|
|
errors.append(
|
|
f"{path.name}: id {entry_id!r} is not URL-safe [A-Za-z0-9-]+ "
|
|
"(HOH-WP-0002-T01: URI is this id verbatim)"
|
|
)
|
|
|
|
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())
|