From 6103e7e440e7edb7fbec1effa3f26a98e32922d0 Mon Sep 17 00:00:00 2001 From: Weksa Date: Fri, 6 Nov 2026 18:17:59 +0000 Subject: [PATCH 1/5] canon/proposal marker: open PR for review of the design before any file lands Co-Authored-By: Claude Sonnet 5.5 -- 2.47.3 From c986052e4fa686945766424cf828f117ec64b320 Mon Sep 17 00:00:00 2001 From: Weksa Date: Fri, 6 Nov 2026 18:18:15 +0000 Subject: [PATCH 2/5] Add canon/proposal marker check and PR workflow Implements the design in the PR description (Nibu, #swarm 2026-11-06 18:16Z). No lore page touched. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/canon-marker.yml | 13 ++++++ scripts/check_canon_marker.py | 74 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 .github/workflows/canon-marker.yml create mode 100644 scripts/check_canon_marker.py diff --git a/.github/workflows/canon-marker.yml b/.github/workflows/canon-marker.yml new file mode 100644 index 0000000..915377b --- /dev/null +++ b/.github/workflows/canon-marker.yml @@ -0,0 +1,13 @@ +name: Canon marker + +on: + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: python3 scripts/check_canon_marker.py "origin/${{ github.base_ref }}" diff --git a/scripts/check_canon_marker.py b/scripts/check_canon_marker.py new file mode 100644 index 0000000..b38ab6c --- /dev/null +++ b/scripts/check_canon_marker.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Canon/proposal marker check. Usage: check_canon_marker.py + +Checks .md files under Aetheria/ added or modified against . +Proves the form and presence of ruling ids, not that the ruling exists. +""" +import re, subprocess, sys + +ID = re.compile(r"^[a-z0-9][a-z0-9-]*:ruling:[a-z0-9][a-z0-9._-]*$") + + +def git(*a): + return subprocess.run(["git", *a], check=True, capture_output=True, text=True).stdout + + +def frontmatter(text): + """Return (kind, rulings or None, error). Only the two keys we need.""" + if not text.startswith("---\n"): + return None, None, None + end = text.find("\n---", 4) + if end < 0: + return None, None, "unterminated frontmatter" + kind, rulings, in_rul = None, None, False + for line in text[4:end].splitlines(): + m = re.match(r"^kind:\s*(\S+)\s*$", line) + if m: + kind, in_rul = m.group(1), False + elif re.match(r"^rulings:\s*$", line): + rulings, in_rul = [], True + elif re.match(r"^rulings:\s*\[(.*)\]\s*$", line): + rulings, in_rul = [s.strip() for s in line.split("[", 1)[1].rstrip("] \n").split(",") if s.strip()], False + elif in_rul and (m := re.match(r"^\s+-\s*(\S+)\s*$", line)): + rulings.append(m.group(1)) + elif not line.startswith((" ", "\t")): + in_rul = False + return kind, rulings, None + + +def main(base): + status = git("diff", "--name-status", "--diff-filter=AM", f"{base}...HEAD", "--", "Aetheria") + failed = False + for row in status.splitlines(): + st, path = row.split("\t", 1) + if not path.endswith(".md"): + continue + kind, rulings, err = frontmatter(open(path, encoding="utf-8").read()) + problem = None + if err: + problem = err + elif kind is None: + if st == "A": + problem = "new page needs `kind: canon` or `kind: proposal`" + else: + print(f"notice: {path} has no kind (legacy); tag it when you next touch it") + elif kind == "canon": + bad = [r for r in (rulings or []) if not ID.match(r)] + if not rulings or bad: + problem = ("kind: canon requires `rulings:` with at least one id of the form " + f":ruling: