From da074340369fb21027df97c66c8686ba99ef61e7 Mon Sep 17 00:00:00 2001 From: transitrix Date: Sat, 22 Aug 2026 09:53:42 +0400 Subject: [PATCH 1/2] site: fix the reference-sheet library's shape and no-gate contract Adds the convention note, a machine check for the shape and gate rules, and an empty-but-valid checksum manifest so the guard is in place before any sheet lands. --- .github/workflows/reference-sheets.yml | 39 +++ reference-sheets/README.md | 20 ++ reference-sheets/SHA256SUMS | 0 scripts/check_reference_sheets.py | 402 +++++++++++++++++++++++++ 4 files changed, 461 insertions(+) create mode 100644 .github/workflows/reference-sheets.yml create mode 100644 reference-sheets/README.md create mode 100644 reference-sheets/SHA256SUMS create mode 100644 scripts/check_reference_sheets.py diff --git a/.github/workflows/reference-sheets.yml b/.github/workflows/reference-sheets.yml new file mode 100644 index 0000000..f82daee --- /dev/null +++ b/.github/workflows/reference-sheets.yml @@ -0,0 +1,39 @@ +name: Reference sheets + +# The reference-sheet library's shape and no-gate contract - see +# reference-sheets/README.md - is machine-enforced so a sheet that violates +# it fails the build rather than reaching a reader. Passes vacuously with +# zero sheets present. + +on: + push: + branches: [main] + paths: + - 'reference-sheets/**' + - 'scripts/check_reference_sheets.py' + - '.github/workflows/reference-sheets.yml' + pull_request: + branches: [main] + paths: + - 'reference-sheets/**' + - 'scripts/check_reference_sheets.py' + - '.github/workflows/reference-sheets.yml' + +permissions: + contents: read + +jobs: + check-reference-sheets: + runs-on: ubuntu-latest + name: reference-sheets/ shape and no-gate contract hold + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Check reference-sheets/ contract + run: python3 scripts/check_reference_sheets.py diff --git a/reference-sheets/README.md b/reference-sheets/README.md new file mode 100644 index 0000000..7c5f131 --- /dev/null +++ b/reference-sheets/README.md @@ -0,0 +1,20 @@ +# Reference sheets — contract + +This directory holds the transitrix.com reference-sheet library. The shape below is +enforced by `scripts/check_reference_sheets.py` in CI +(`.github/workflows/reference-sheets.yml`) — a sheet that violates it fails the check +rather than reaching a reader. + +- **One directory per sheet:** `reference-sheets//`, served at + `https://transitrix.com/reference-sheets//`. +- **The PDF lives at `reference-sheets//.pdf`.** The filename is the slug, + so a revision replaces the file in place and never mints a second URL. +- **The page carries the sheet's content as HTML.** The PDF is the take-away download, + never the only copy of the content. +- **The page `` leads with the sheet's own printed name** — the same name printed + on the sheet itself — with no suffix beyond the site's existing ` | Transitrix`. +- **No gate of any kind.** No form, no email field, no sign-up, no interstitial, no + tracking wall, on the index or on any sheet page. +- **`SHA256SUMS`** lists every committed PDF's checksum, one `<sha256> <slug>/<slug>.pdf` + line per sheet (`sha256sum` output format). It is updated in the same commit as the + PDF it describes. diff --git a/reference-sheets/SHA256SUMS b/reference-sheets/SHA256SUMS new file mode 100644 index 0000000..e69de29 diff --git a/scripts/check_reference_sheets.py b/scripts/check_reference_sheets.py new file mode 100644 index 0000000..27bcee6 --- /dev/null +++ b/scripts/check_reference_sheets.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python3 +"""Guard the reference-sheet library's shape and no-gate contract. + +Checked against reference-sheets/ (see reference-sheets/README.md for the contract +in prose): every sheet directory carries index.html and <slug>.pdf and nothing else +named .pdf, no sheet page contains a form/input/submit-button/signup-script gate, +and reference-sheets/SHA256SUMS matches the committed PDFs byte-for-byte. + +Usage: python3 scripts/check_reference_sheets.py +Runs a self-test against throwaway fixtures in a temp directory before checking the +real tree - see .github/workflows/indexing-declarations.yml for the same shape. +""" +import hashlib +import os +import re +import shutil +import sys +import tempfile + +REFERENCE_SHEETS_DIR = "reference-sheets" +CHECKSUMS_FILE = "SHA256SUMS" +NON_SHEET_ENTRIES = {"README.md", CHECKSUMS_FILE} + +GATE_PATTERNS = [ + (re.compile(r"<form\b", re.IGNORECASE), "a <form> element"), + (re.compile(r"<input\b", re.IGNORECASE), "an <input> element"), + (re.compile(r'<button[^>]*type\s*=\s*["\']?submit', re.IGNORECASE), 'a <button type="submit">'), + ( + re.compile( + r"(mailchimp|convertkit|hubspot|getresponse|klaviyo|substack|beehiiv|typeform)", + re.IGNORECASE, + ), + "a mail-capture/signup script", + ), + ( + re.compile(r"(interstitial|signup-modal|email-gate|paywall)", re.IGNORECASE), + "an interstitial/gate marker", + ), +] + + +def find_slug_dirs(sheets_root): + if not os.path.isdir(sheets_root): + return [] + slugs = [] + for name in sorted(os.listdir(sheets_root)): + if name in NON_SHEET_ENTRIES or name.startswith("."): + continue + if os.path.isdir(os.path.join(sheets_root, name)): + slugs.append(name) + return slugs + + +def check_shape(sheets_root, slug): + errors = [] + slug_dir = os.path.join(sheets_root, slug) + index_path = os.path.join(slug_dir, "index.html") + if not os.path.isfile(index_path): + errors.append(f"{slug_dir}: missing index.html") + + expected_pdf = f"{slug}.pdf" + pdfs = [f for f in os.listdir(slug_dir) if f.lower().endswith(".pdf")] + if expected_pdf not in pdfs: + errors.append(f"{slug_dir}: missing {expected_pdf}") + for f in pdfs: + if f != expected_pdf: + errors.append(f"{slug_dir}: PDF named {f!r}, expected only {expected_pdf!r}") + return errors + + +def check_gate(sheets_root, slug): + errors = [] + index_path = os.path.join(sheets_root, slug, "index.html") + if not os.path.isfile(index_path): + return errors + with open(index_path, encoding="utf-8") as f: + html = f.read() + for pattern, label in GATE_PATTERNS: + if pattern.search(html): + errors.append(f"{index_path}: contains {label} - no gate is allowed on a reference-sheet page") + return errors + + +def parse_checksums(checksums_path): + """Returns {relative_path: lowercase_hex_sha256}, or None if the file is missing.""" + if not os.path.isfile(checksums_path): + return None + entries = {} + with open(checksums_path, encoding="utf-8") as f: + for lineno, line in enumerate(f, start=1): + line = line.rstrip("\n") + if not line.strip(): + continue + m = re.match(r"^([0-9a-fA-F]{64})\s+(.+)$", line) + if not m: + raise ValueError(f"{checksums_path}:{lineno}: unparseable line {line!r}") + entries[m.group(2).strip()] = m.group(1).lower() + return entries + + +def sha256_of(path): + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1 << 16), b""): + h.update(chunk) + return h.hexdigest() + + +def check_checksums(sheets_root, slugs): + checksums_path = os.path.join(sheets_root, CHECKSUMS_FILE) + try: + entries = parse_checksums(checksums_path) + except ValueError as e: + return [str(e)] + if entries is None: + return [f"{checksums_path}: missing"] + + errors = [] + pdf_by_rel = {} + for slug in slugs: + rel = f"{slug}/{slug}.pdf" + abs_path = os.path.join(sheets_root, slug, f"{slug}.pdf") + if os.path.isfile(abs_path): + pdf_by_rel[rel] = abs_path + + for rel, abs_path in pdf_by_rel.items(): + if rel not in entries: + errors.append(f"{checksums_path}: no entry for {rel}") + continue + actual = sha256_of(abs_path) + if actual != entries[rel]: + errors.append( + f"{checksums_path}: checksum for {rel} does not match the committed PDF " + f"(expected {entries[rel]}, got {actual})" + ) + + for rel in entries: + if rel not in pdf_by_rel: + errors.append(f"{checksums_path}: entry for {rel} but no such PDF exists") + + return errors + + +def run_checks(root): + sheets_root = os.path.join(root, REFERENCE_SHEETS_DIR) + slugs = find_slug_dirs(sheets_root) + errors = [] + for slug in slugs: + errors.extend(check_shape(sheets_root, slug)) + errors.extend(check_gate(sheets_root, slug)) + errors.extend(check_checksums(sheets_root, slugs)) + return errors, slugs + + +def _write(path, content=""): + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def _sha256sums_line(root_for_hash, slug): + pdf_path = os.path.join(root_for_hash, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf") + return f"{sha256_of(pdf_path)} {slug}/{slug}.pdf\n" + + +def _fresh_sheet(tmp, slug="widgets", pdf_bytes=b"%PDF-1.4 fixture\n"): + """A minimal, contract-clean sheet: index.html, <slug>.pdf, and a matching + SHA256SUMS entry. Callers mutate one thing at a time to demonstrate a failure.""" + _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), "<html><body>content</body></html>") + _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf")) + with open(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf"), "wb") as f: + f.write(pdf_bytes) + _write( + os.path.join(tmp, REFERENCE_SHEETS_DIR, CHECKSUMS_FILE), + _sha256sums_line(tmp, slug), + ) + return slug + + +def _expect(condition, ok_msg, fail_msg, failures): + if condition: + print(f"ok: {ok_msg}") + else: + print(f"::error::self-test regression - {fail_msg}") + failures.append(fail_msg) + + +def _selftest(): + print("== Self-test: the checker itself must catch what it's meant to catch ==") + failures = [] + + # A clean sheet passes outright. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + errors, slugs = run_checks(tmp) + _expect(errors == [], "a contract-clean sheet passes with zero errors", "a clean sheet was flagged", failures) + finally: + shutil.rmtree(tmp) + + # Zero sheets present passes vacuously, provided SHA256SUMS exists (empty). + tmp = tempfile.mkdtemp() + try: + _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, CHECKSUMS_FILE), "") + errors, slugs = run_checks(tmp) + _expect(errors == [] and slugs == [], "zero sheets present passes vacuously", "an empty library was flagged", failures) + finally: + shutil.rmtree(tmp) + + # Missing index.html. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + os.remove(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html")) + errors, _ = run_checks(tmp) + _expect( + any("missing index.html" in e for e in errors), + "a sheet directory missing index.html is caught", + "a missing index.html was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # Missing PDF. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + os.remove(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf")) + errors, _ = run_checks(tmp) + _expect( + any(f"missing {slug}.pdf" in e for e in errors), + "a sheet directory missing <slug>.pdf is caught", + "a missing PDF was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # PDF named wrong (not <slug>.pdf). + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + os.rename( + os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf"), + os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "final-v2.pdf"), + ) + errors, _ = run_checks(tmp) + _expect( + any("PDF named 'final-v2.pdf'" in e for e in errors), + "a PDF whose name is not <slug>.pdf is caught", + "a misnamed PDF was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # Gate: <form>. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), '<html><body><form></form></body></html>') + errors, _ = run_checks(tmp) + _expect( + any("<form>" in e for e in errors), + "a page with a <form> gate is caught", + "a <form> gate was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # Gate: <input>. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), '<html><body><input type="email"></body></html>') + errors, _ = run_checks(tmp) + _expect( + any("<input>" in e for e in errors), + "a page with a bare <input> gate is caught", + "an <input> gate was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # Gate: <button type="submit">. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + _write( + os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), + '<html><body><button type="submit">Get it</button></body></html>', + ) + errors, _ = run_checks(tmp) + _expect( + any('type="submit"' in e for e in errors), + 'a page with a <button type="submit"> gate is caught', + "a submit-button gate was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # Gate: mail-capture/signup script. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + _write( + os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), + '<html><head><script src="https://embed.mailchimp.com/embed.js"></script></head><body>content</body></html>', + ) + errors, _ = run_checks(tmp) + _expect( + any("mail-capture/signup script" in e for e in errors), + "a page embedding a signup-form script is caught", + "a signup-script gate was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # Gate: interstitial marker. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + _write( + os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), + '<html><body><div class="signup-modal">Before you download...</div></body></html>', + ) + errors, _ = run_checks(tmp) + _expect( + any("interstitial/gate marker" in e for e in errors), + "a page carrying an interstitial/gate marker is caught", + "an interstitial marker was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # SHA256SUMS mismatch. + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + with open(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf"), "ab") as f: + f.write(b"tampered after the checksum was recorded") + errors, _ = run_checks(tmp) + _expect( + any("does not match the committed PDF" in e for e in errors), + "a PDF that no longer matches SHA256SUMS is caught", + "a checksum mismatch was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + # SHA256SUMS stale entry (PDF removed, entry left behind). + tmp = tempfile.mkdtemp() + try: + slug = _fresh_sheet(tmp) + second = os.path.join(tmp, REFERENCE_SHEETS_DIR, CHECKSUMS_FILE) + with open(second, "a", encoding="utf-8") as f: + f.write("0" * 64 + " ghost/ghost.pdf\n") + errors, _ = run_checks(tmp) + _expect( + any("entry for ghost/ghost.pdf but no such PDF exists" in e for e in errors), + "a SHA256SUMS entry with no matching PDF is caught", + "a stale checksum entry was not caught", + failures, + ) + finally: + shutil.rmtree(tmp) + + if failures: + print(f"::error::check_reference_sheets self-test failed - {len(failures)} check(s) did not behave as expected, not trusting its verdict on the real tree") + return 1 + print("self-test: all failure modes are caught, and a clean tree passes.") + return 0 + + +def main(): + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + selftest_status = _selftest() + if selftest_status != 0: + return selftest_status + + print("== Checking reference-sheets/ against the committed tree ==") + errors, slugs = run_checks(root) + for e in errors: + print(f"::error::{e}") + if errors: + print(f"check_reference_sheets: {len(errors)} problem(s), see above") + return 1 + note = " (zero sheets present)" if not slugs else f" ({len(slugs)} sheet(s))" + print(f"check_reference_sheets: reference-sheets/ contract holds{note}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 6b2085874c47f2b83ed79f65739c67ec709b2284 Mon Sep 17 00:00:00 2001 From: transitrix <automation@transitrix.com> Date: Sat, 22 Aug 2026 10:54:58 +0400 Subject: [PATCH 2/2] site: rename the reference-sheet library route to /library/ The section will carry more than one kind of material, and a route named after one kind would have to move the first time a second kind arrives. Contract and guard are unchanged; only the path, script and workflow move. --- .github/workflows/library.yml | 38 +++++ .github/workflows/reference-sheets.yml | 39 ----- library/README.md | 22 +++ {reference-sheets => library}/SHA256SUMS | 0 reference-sheets/README.md | 20 --- ...k_reference_sheets.py => check_library.py} | 134 +++++++++--------- 6 files changed, 127 insertions(+), 126 deletions(-) create mode 100644 .github/workflows/library.yml delete mode 100644 .github/workflows/reference-sheets.yml create mode 100644 library/README.md rename {reference-sheets => library}/SHA256SUMS (100%) delete mode 100644 reference-sheets/README.md rename scripts/{check_reference_sheets.py => check_library.py} (68%) diff --git a/.github/workflows/library.yml b/.github/workflows/library.yml new file mode 100644 index 0000000..690a59e --- /dev/null +++ b/.github/workflows/library.yml @@ -0,0 +1,38 @@ +name: Library + +# The library's shape and no-gate contract - see library/README.md - is +# machine-enforced so an item that violates it fails the build rather than +# reaching a reader. Passes vacuously with zero items present. + +on: + push: + branches: [main] + paths: + - 'library/**' + - 'scripts/check_library.py' + - '.github/workflows/library.yml' + pull_request: + branches: [main] + paths: + - 'library/**' + - 'scripts/check_library.py' + - '.github/workflows/library.yml' + +permissions: + contents: read + +jobs: + check-library: + runs-on: ubuntu-latest + name: library/ shape and no-gate contract hold + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Check library/ contract + run: python3 scripts/check_library.py diff --git a/.github/workflows/reference-sheets.yml b/.github/workflows/reference-sheets.yml deleted file mode 100644 index f82daee..0000000 --- a/.github/workflows/reference-sheets.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Reference sheets - -# The reference-sheet library's shape and no-gate contract - see -# reference-sheets/README.md - is machine-enforced so a sheet that violates -# it fails the build rather than reaching a reader. Passes vacuously with -# zero sheets present. - -on: - push: - branches: [main] - paths: - - 'reference-sheets/**' - - 'scripts/check_reference_sheets.py' - - '.github/workflows/reference-sheets.yml' - pull_request: - branches: [main] - paths: - - 'reference-sheets/**' - - 'scripts/check_reference_sheets.py' - - '.github/workflows/reference-sheets.yml' - -permissions: - contents: read - -jobs: - check-reference-sheets: - runs-on: ubuntu-latest - name: reference-sheets/ shape and no-gate contract hold - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Check reference-sheets/ contract - run: python3 scripts/check_reference_sheets.py diff --git a/library/README.md b/library/README.md new file mode 100644 index 0000000..bdc870f --- /dev/null +++ b/library/README.md @@ -0,0 +1,22 @@ +# Library — contract + +This directory holds the transitrix.com library of free reference material. The shape +below is enforced by `scripts/check_library.py` in CI (`.github/workflows/library.yml`) +— an item that violates it fails the check rather than reaching a reader. + +- **One directory per item:** `library/<slug>/`, served at + `https://transitrix.com/library/<slug>/`. +- **The PDF lives at `library/<slug>/<slug>.pdf`.** The filename is the slug, so a + revision replaces the file in place and never mints a second URL. +- **The page carries the item's content as HTML.** The PDF is the take-away download, + never the only copy of the content. +- **The page `<title>` leads with the item's own printed name** — the same name printed + on the item itself — with no suffix beyond the site's existing ` | Transitrix`. +- **Kinds of material are not path segments.** A cheat sheet, a template and a worked + example all sit directly under `library/<slug>/`; grouping belongs on the index page, + where it can be revised, never in an address that has been printed and handed out. +- **No gate of any kind.** No form, no email field, no sign-up, no interstitial, no + tracking wall, on the index or on any item page. +- **`SHA256SUMS`** lists every committed PDF's checksum, one `<sha256> <slug>/<slug>.pdf` + line per item (`sha256sum` output format). It is updated in the same commit as the + PDF it describes. diff --git a/reference-sheets/SHA256SUMS b/library/SHA256SUMS similarity index 100% rename from reference-sheets/SHA256SUMS rename to library/SHA256SUMS diff --git a/reference-sheets/README.md b/reference-sheets/README.md deleted file mode 100644 index 7c5f131..0000000 --- a/reference-sheets/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Reference sheets — contract - -This directory holds the transitrix.com reference-sheet library. The shape below is -enforced by `scripts/check_reference_sheets.py` in CI -(`.github/workflows/reference-sheets.yml`) — a sheet that violates it fails the check -rather than reaching a reader. - -- **One directory per sheet:** `reference-sheets/<slug>/`, served at - `https://transitrix.com/reference-sheets/<slug>/`. -- **The PDF lives at `reference-sheets/<slug>/<slug>.pdf`.** The filename is the slug, - so a revision replaces the file in place and never mints a second URL. -- **The page carries the sheet's content as HTML.** The PDF is the take-away download, - never the only copy of the content. -- **The page `<title>` leads with the sheet's own printed name** — the same name printed - on the sheet itself — with no suffix beyond the site's existing ` | Transitrix`. -- **No gate of any kind.** No form, no email field, no sign-up, no interstitial, no - tracking wall, on the index or on any sheet page. -- **`SHA256SUMS`** lists every committed PDF's checksum, one `<sha256> <slug>/<slug>.pdf` - line per sheet (`sha256sum` output format). It is updated in the same commit as the - PDF it describes. diff --git a/scripts/check_reference_sheets.py b/scripts/check_library.py similarity index 68% rename from scripts/check_reference_sheets.py rename to scripts/check_library.py index 27bcee6..aba492e 100644 --- a/scripts/check_reference_sheets.py +++ b/scripts/check_library.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -"""Guard the reference-sheet library's shape and no-gate contract. +"""Guard the library's shape and no-gate contract. -Checked against reference-sheets/ (see reference-sheets/README.md for the contract -in prose): every sheet directory carries index.html and <slug>.pdf and nothing else -named .pdf, no sheet page contains a form/input/submit-button/signup-script gate, -and reference-sheets/SHA256SUMS matches the committed PDFs byte-for-byte. +Checked against library/ (see library/README.md for the contract in prose): every +item directory carries index.html and <slug>.pdf and nothing else named .pdf, no item +page contains a form/input/submit-button/signup-script gate, and library/SHA256SUMS +matches the committed PDFs byte-for-byte. -Usage: python3 scripts/check_reference_sheets.py +Usage: python3 scripts/check_library.py Runs a self-test against throwaway fixtures in a temp directory before checking the real tree - see .github/workflows/indexing-declarations.yml for the same shape. """ @@ -17,9 +17,9 @@ import sys import tempfile -REFERENCE_SHEETS_DIR = "reference-sheets" +LIBRARY_DIR = "library" CHECKSUMS_FILE = "SHA256SUMS" -NON_SHEET_ENTRIES = {"README.md", CHECKSUMS_FILE} +NON_ITEM_ENTRIES = {"README.md", CHECKSUMS_FILE} GATE_PATTERNS = [ (re.compile(r"<form\b", re.IGNORECASE), "a <form> element"), @@ -39,21 +39,21 @@ ] -def find_slug_dirs(sheets_root): - if not os.path.isdir(sheets_root): +def find_slug_dirs(library_root): + if not os.path.isdir(library_root): return [] slugs = [] - for name in sorted(os.listdir(sheets_root)): - if name in NON_SHEET_ENTRIES or name.startswith("."): + for name in sorted(os.listdir(library_root)): + if name in NON_ITEM_ENTRIES or name.startswith("."): continue - if os.path.isdir(os.path.join(sheets_root, name)): + if os.path.isdir(os.path.join(library_root, name)): slugs.append(name) return slugs -def check_shape(sheets_root, slug): +def check_shape(library_root, slug): errors = [] - slug_dir = os.path.join(sheets_root, slug) + slug_dir = os.path.join(library_root, slug) index_path = os.path.join(slug_dir, "index.html") if not os.path.isfile(index_path): errors.append(f"{slug_dir}: missing index.html") @@ -68,16 +68,16 @@ def check_shape(sheets_root, slug): return errors -def check_gate(sheets_root, slug): +def check_gate(library_root, slug): errors = [] - index_path = os.path.join(sheets_root, slug, "index.html") + index_path = os.path.join(library_root, slug, "index.html") if not os.path.isfile(index_path): return errors with open(index_path, encoding="utf-8") as f: html = f.read() for pattern, label in GATE_PATTERNS: if pattern.search(html): - errors.append(f"{index_path}: contains {label} - no gate is allowed on a reference-sheet page") + errors.append(f"{index_path}: contains {label} - no gate is allowed on a library page") return errors @@ -106,8 +106,8 @@ def sha256_of(path): return h.hexdigest() -def check_checksums(sheets_root, slugs): - checksums_path = os.path.join(sheets_root, CHECKSUMS_FILE) +def check_checksums(library_root, slugs): + checksums_path = os.path.join(library_root, CHECKSUMS_FILE) try: entries = parse_checksums(checksums_path) except ValueError as e: @@ -119,7 +119,7 @@ def check_checksums(sheets_root, slugs): pdf_by_rel = {} for slug in slugs: rel = f"{slug}/{slug}.pdf" - abs_path = os.path.join(sheets_root, slug, f"{slug}.pdf") + abs_path = os.path.join(library_root, slug, f"{slug}.pdf") if os.path.isfile(abs_path): pdf_by_rel[rel] = abs_path @@ -142,13 +142,13 @@ def check_checksums(sheets_root, slugs): def run_checks(root): - sheets_root = os.path.join(root, REFERENCE_SHEETS_DIR) - slugs = find_slug_dirs(sheets_root) + library_root = os.path.join(root, LIBRARY_DIR) + slugs = find_slug_dirs(library_root) errors = [] for slug in slugs: - errors.extend(check_shape(sheets_root, slug)) - errors.extend(check_gate(sheets_root, slug)) - errors.extend(check_checksums(sheets_root, slugs)) + errors.extend(check_shape(library_root, slug)) + errors.extend(check_gate(library_root, slug)) + errors.extend(check_checksums(library_root, slugs)) return errors, slugs @@ -159,19 +159,19 @@ def _write(path, content=""): def _sha256sums_line(root_for_hash, slug): - pdf_path = os.path.join(root_for_hash, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf") + pdf_path = os.path.join(root_for_hash, LIBRARY_DIR, slug, f"{slug}.pdf") return f"{sha256_of(pdf_path)} {slug}/{slug}.pdf\n" -def _fresh_sheet(tmp, slug="widgets", pdf_bytes=b"%PDF-1.4 fixture\n"): - """A minimal, contract-clean sheet: index.html, <slug>.pdf, and a matching +def _fresh_item(tmp, slug="widgets", pdf_bytes=b"%PDF-1.4 fixture\n"): + """A minimal, contract-clean item: index.html, <slug>.pdf, and a matching SHA256SUMS entry. Callers mutate one thing at a time to demonstrate a failure.""" - _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), "<html><body>content</body></html>") - _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf")) - with open(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf"), "wb") as f: + _write(os.path.join(tmp, LIBRARY_DIR, slug, "index.html"), "<html><body>content</body></html>") + _write(os.path.join(tmp, LIBRARY_DIR, slug, f"{slug}.pdf")) + with open(os.path.join(tmp, LIBRARY_DIR, slug, f"{slug}.pdf"), "wb") as f: f.write(pdf_bytes) _write( - os.path.join(tmp, REFERENCE_SHEETS_DIR, CHECKSUMS_FILE), + os.path.join(tmp, LIBRARY_DIR, CHECKSUMS_FILE), _sha256sums_line(tmp, slug), ) return slug @@ -189,33 +189,33 @@ def _selftest(): print("== Self-test: the checker itself must catch what it's meant to catch ==") failures = [] - # A clean sheet passes outright. + # A clean item passes outright. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) + slug = _fresh_item(tmp) errors, slugs = run_checks(tmp) - _expect(errors == [], "a contract-clean sheet passes with zero errors", "a clean sheet was flagged", failures) + _expect(errors == [], "a contract-clean item passes with zero errors", "a clean item was flagged", failures) finally: shutil.rmtree(tmp) - # Zero sheets present passes vacuously, provided SHA256SUMS exists (empty). + # Zero items present passes vacuously, provided SHA256SUMS exists (empty). tmp = tempfile.mkdtemp() try: - _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, CHECKSUMS_FILE), "") + _write(os.path.join(tmp, LIBRARY_DIR, CHECKSUMS_FILE), "") errors, slugs = run_checks(tmp) - _expect(errors == [] and slugs == [], "zero sheets present passes vacuously", "an empty library was flagged", failures) + _expect(errors == [] and slugs == [], "zero items present passes vacuously", "an empty library was flagged", failures) finally: shutil.rmtree(tmp) # Missing index.html. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) - os.remove(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html")) + slug = _fresh_item(tmp) + os.remove(os.path.join(tmp, LIBRARY_DIR, slug, "index.html")) errors, _ = run_checks(tmp) _expect( any("missing index.html" in e for e in errors), - "a sheet directory missing index.html is caught", + "an item directory missing index.html is caught", "a missing index.html was not caught", failures, ) @@ -225,12 +225,12 @@ def _selftest(): # Missing PDF. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) - os.remove(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf")) + slug = _fresh_item(tmp) + os.remove(os.path.join(tmp, LIBRARY_DIR, slug, f"{slug}.pdf")) errors, _ = run_checks(tmp) _expect( any(f"missing {slug}.pdf" in e for e in errors), - "a sheet directory missing <slug>.pdf is caught", + "an item directory missing <slug>.pdf is caught", "a missing PDF was not caught", failures, ) @@ -240,10 +240,10 @@ def _selftest(): # PDF named wrong (not <slug>.pdf). tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) + slug = _fresh_item(tmp) os.rename( - os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf"), - os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "final-v2.pdf"), + os.path.join(tmp, LIBRARY_DIR, slug, f"{slug}.pdf"), + os.path.join(tmp, LIBRARY_DIR, slug, "final-v2.pdf"), ) errors, _ = run_checks(tmp) _expect( @@ -258,8 +258,8 @@ def _selftest(): # Gate: <form>. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) - _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), '<html><body><form></form></body></html>') + slug = _fresh_item(tmp) + _write(os.path.join(tmp, LIBRARY_DIR, slug, "index.html"), '<html><body><form></form></body></html>') errors, _ = run_checks(tmp) _expect( any("<form>" in e for e in errors), @@ -273,8 +273,8 @@ def _selftest(): # Gate: <input>. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) - _write(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), '<html><body><input type="email"></body></html>') + slug = _fresh_item(tmp) + _write(os.path.join(tmp, LIBRARY_DIR, slug, "index.html"), '<html><body><input type="email"></body></html>') errors, _ = run_checks(tmp) _expect( any("<input>" in e for e in errors), @@ -288,9 +288,9 @@ def _selftest(): # Gate: <button type="submit">. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) + slug = _fresh_item(tmp) _write( - os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), + os.path.join(tmp, LIBRARY_DIR, slug, "index.html"), '<html><body><button type="submit">Get it</button></body></html>', ) errors, _ = run_checks(tmp) @@ -306,9 +306,9 @@ def _selftest(): # Gate: mail-capture/signup script. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) + slug = _fresh_item(tmp) _write( - os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), + os.path.join(tmp, LIBRARY_DIR, slug, "index.html"), '<html><head><script src="https://embed.mailchimp.com/embed.js"></script></head><body>content</body></html>', ) errors, _ = run_checks(tmp) @@ -324,9 +324,9 @@ def _selftest(): # Gate: interstitial marker. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) + slug = _fresh_item(tmp) _write( - os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, "index.html"), + os.path.join(tmp, LIBRARY_DIR, slug, "index.html"), '<html><body><div class="signup-modal">Before you download...</div></body></html>', ) errors, _ = run_checks(tmp) @@ -342,8 +342,8 @@ def _selftest(): # SHA256SUMS mismatch. tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) - with open(os.path.join(tmp, REFERENCE_SHEETS_DIR, slug, f"{slug}.pdf"), "ab") as f: + slug = _fresh_item(tmp) + with open(os.path.join(tmp, LIBRARY_DIR, slug, f"{slug}.pdf"), "ab") as f: f.write(b"tampered after the checksum was recorded") errors, _ = run_checks(tmp) _expect( @@ -358,8 +358,8 @@ def _selftest(): # SHA256SUMS stale entry (PDF removed, entry left behind). tmp = tempfile.mkdtemp() try: - slug = _fresh_sheet(tmp) - second = os.path.join(tmp, REFERENCE_SHEETS_DIR, CHECKSUMS_FILE) + slug = _fresh_item(tmp) + second = os.path.join(tmp, LIBRARY_DIR, CHECKSUMS_FILE) with open(second, "a", encoding="utf-8") as f: f.write("0" * 64 + " ghost/ghost.pdf\n") errors, _ = run_checks(tmp) @@ -373,7 +373,7 @@ def _selftest(): shutil.rmtree(tmp) if failures: - print(f"::error::check_reference_sheets self-test failed - {len(failures)} check(s) did not behave as expected, not trusting its verdict on the real tree") + print(f"::error::check_library self-test failed - {len(failures)} check(s) did not behave as expected, not trusting its verdict on the real tree") return 1 print("self-test: all failure modes are caught, and a clean tree passes.") return 0 @@ -386,15 +386,15 @@ def main(): if selftest_status != 0: return selftest_status - print("== Checking reference-sheets/ against the committed tree ==") + print("== Checking library/ against the committed tree ==") errors, slugs = run_checks(root) for e in errors: print(f"::error::{e}") if errors: - print(f"check_reference_sheets: {len(errors)} problem(s), see above") + print(f"check_library: {len(errors)} problem(s), see above") return 1 - note = " (zero sheets present)" if not slugs else f" ({len(slugs)} sheet(s))" - print(f"check_reference_sheets: reference-sheets/ contract holds{note}") + note = " (zero items present)" if not slugs else f" ({len(slugs)} item(s))" + print(f"check_library: library/ contract holds{note}") return 0