Bernd filled the first human seat. Link the workplace selfie, hang the reviewer's portrait, and keep the invitation open.
160 lines
4.9 KiB
Python
Executable file
160 lines
4.9 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"}
|
|
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_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}")
|
|
|
|
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())
|