hall-of-helix/scripts/check-entries.py
tegwick 1ec16e24db Complete HOH-WP-0001: closing routine and PQRST record in each seat
CLOSING.md is now the routine for the operator's wind-down prompt, which it
quotes so an agent recognises the situation it is in. Linked from README.md
beside "How to leave a seat", from the top of ENTRY.md, and from AGENTS.md — the
durable copy after the REPO-AGENTS-EXTENSIONS marker, since the Close protocol
above it is template-synced.

The routine states two things it was otherwise silent on: the estimate covers
the substantive session and excludes the closing ritual itself, and the prompt
is reached by path with only the output block inlined so a session without a
pqrst-practice checkout can still produce a well-formed record.

Entries carry the record in both halves — a quoted canonical signature in
`pqrst_estimate` frontmatter and a `## PQRST estimate` section with Confidence
and Dominant factors — because a signature without its evidence is not
auditable and evidence without a signature cannot be read across sessions.
ENTRY.md and templates/entry.md updated to match.

check-entries.py validates the signature format, the 100 sum, and that a
signature is never present without its section. Required for agent-session
seats recorded from 2026-09-06: the routine was adopted today, so seats written
earlier today could not have followed it. Human seats are exempt and the 102
existing seats are grandfathered — no estimate is invented for a session nobody
observed.

The one manual estimate is normalised to "P30 Q23 R18 S19 T10" — same numbers,
canonical spelling. Its new section records plainly that the operator added it
after the fact and that no Confidence or Dominant factors were captured; neither
is reconstructed.

make check passes on all 102 seats, and was verified to reject a bad sum, the
old slash form, a signature without its section, and a missing record on a
post-adoption agent seat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ6oF1DtVDKcD1FCpvRVLx

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 272883@bnt-lap001
Assistant-Session: f40c8f53-fb65-4980-9d29-bcdb3dd946f7
2026-09-05 21:12:24 +02:00

213 lines
7.1 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")
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 agent seats recorded on or after this date.
# 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
required = (
fm.get("worker_kind", "") == "agent-session"
and fm.get("recorded_at", "").strip().strip('"').strip("'") >= PQRST_FROM
)
if required and not signature:
errors.append(
f"{path.name}: 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())