From 287c9055c2d5ab41620c11c62713da586bf2ea63 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:22:26 +0100 Subject: [PATCH] chore(docs): unbreak just; gate every .adoc on actually rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two toolchain defects found while verifying #70. 1. `just` was completely broken. build/contractile.just:136 wrote a literal `{{[A-Z_]*}}` grep pattern unescaped, so just parsed it as an interpolation and aborted before running anything — `just --list`, `just docs`, `just quality`, all 124 recipes. `{{{{` is just's escape for a literal `{{`. The file is hand-maintained (see its header), so this fix holds. 2. Nothing checked that .adoc files parse. check-no-md-in-docs.sh enforces the AsciiDoc rule by *extension*; CI's docs job only checked that files *exist*. An unterminated block or malformed table renders to mangled output and ships unnoticed — which is why #69 shipped on hand-counted delimiters. Adds scripts/check-docs-render.sh (renders every .adoc at --failure-level=WARN), wires it into `just docs-check`, `just quality`, and CI's docs job. Also adds `just docs-html` for local reading. 125 of 131 .adoc render clean. The 6 that do not are quarantined in an ALLOWED list rather than skipped silently: they are half-converted Markdown carrying an .adoc extension (a `= Title` bolted onto a Markdown body), which passes the extension check while still being Markdown. All 6 are RSR boilerplate; no game-design doc is affected. No CI impact beyond the new gate: no workflow invokes `just quality` or the dust-* recipes, so unbreaking just cannot turn anything red. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/quality.yml | 8 +++ Justfile | 19 ++++++- build/contractile.just | 5 +- scripts/check-docs-render.sh | 96 +++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100755 scripts/check-docs-render.sh diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 94112d3..4cec20e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -66,3 +66,11 @@ jobs: else echo "✅ Core documentation present" fi + + # The check above tests that doc files *exist*; this tests they *parse*. + # Ruby ships preinstalled on ubuntu-latest, so no setup action is needed. + - name: Install asciidoctor + run: gem install asciidoctor --no-document + + - name: Check every .adoc renders + run: ./scripts/check-docs-render.sh . diff --git a/Justfile b/Justfile index de0e84f..cee85f9 100644 --- a/Justfile +++ b/Justfile @@ -187,7 +187,7 @@ test-all: test e2e aspect bench readiness @echo "All test categories passed — safe to merge!" # Run all quality checks -quality: fmt-check lint test +quality: fmt-check lint docs-check test @echo "All quality checks passed!" # Fix all auto-fixable issues [reversible: git checkout] @@ -281,6 +281,23 @@ docs: just man @echo "Documentation generated in docs/" +# Check every .adoc actually renders (content-side counterpart to the .md ban) +docs-check: + @./scripts/check-docs-render.sh . + +# Render the docs to HTML for local reading [reversible: rm -rf docs/generated/html] +docs-html: + #!/usr/bin/env bash + set -euo pipefail + if ! command -v asciidoctor >/dev/null 2>&1; then + echo "asciidoctor not found. Install with: gem install asciidoctor" >&2 + exit 2 + fi + mkdir -p docs/generated/html + find docs -name '*.adoc' -type f -print0 \ + | xargs -0 asciidoctor -D docs/generated/html + echo "Rendered to docs/generated/html/" + # Generate justfile cookbook documentation cookbook: #!/usr/bin/env bash diff --git a/build/contractile.just b/build/contractile.just index bb67700..4a01d4c 100644 --- a/build/contractile.just +++ b/build/contractile.just @@ -132,8 +132,11 @@ dust-no-ai-djot: dust-no-scm-files: ! find .machine_readable/ -name '*.scm' 2>/dev/null | head -1 | grep -q . +# NOTE: `{{{{` is just's escape for a literal `{{`. Writing the pattern +# unescaped makes just parse it as an interpolation and abort — which broke +# *every* recipe in the repo, not merely this one. dust-no-placeholder-tokens: - ! grep -rl '{{[A-Z_]*}}' --include='*.a2ml' --include='*.adoc' --exclude-dir='.git' . 2>/dev/null | head -1 | grep -q . + ! grep -rl '{{{{[A-Z_]*}}' --include='*.a2ml' --include='*.adoc' --exclude-dir='.git' . 2>/dev/null | head -1 | grep -q . dust-no-tracked-artifacts: ! git ls-files lib/bs/ lib/ocaml/ target/release/ _build/ zig-cache/ zig-out/ 2>/dev/null | head -1 | grep -q . diff --git a/scripts/check-docs-render.sh b/scripts/check-docs-render.sh new file mode 100755 index 0000000..5d757ab --- /dev/null +++ b/scripts/check-docs-render.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# check-docs-render.sh — every .adoc must actually render. +# +# The estate rule "AsciiDoc by default" is enforced by extension +# (check-no-md-in-docs.sh). Nothing enforced that the *content* parses. A +# malformed table, an unterminated listing block, or a stray level-0 heading +# renders to silently mangled output and ships unnoticed. +# +# This renders every .adoc with --failure-level=WARN and fails on any +# diagnostic. It is the content-side counterpart to the extension-side check. +# +# Known-bad files are quarantined in ALLOWED below rather than silently +# skipped — the list is meant to shrink, and an empty list is the goal. +# +# Exit codes: +# 0 — every non-allow-listed .adoc renders without warnings +# 1 — one or more failed to render cleanly +# 2 — usage / setup error (asciidoctor missing) + +set -euo pipefail + +REPO_ROOT="${1:-.}" + +# Quarantined: half-converted Markdown carrying an .adoc extension (a `= Title` +# line bolted onto a Markdown body — `#` headings, comments, | tables). +# These satisfy check-no-md-in-docs.sh by extension while still being Markdown. +# All are RSR boilerplate, not game design. Fix by converting the body properly, +# then delete the entry. Do not add game-design docs to this list. +ALLOWED=( + "docs/architecture/THREAT-MODEL.adoc" + "docs/decisions/0000-template.adoc" + "docs/decisions/0001-adopt-rsr-standard.adoc" + "docs/developer/ABI-FFI-README.adoc" + "docs/practice/AI-CONVENTIONS.adoc" + "docs/STATE-VISUALIZER.adoc" +) + +if ! command -v asciidoctor >/dev/null 2>&1; then + echo "FAIL: asciidoctor not found. Install with: gem install asciidoctor" >&2 + exit 2 +fi + +if [ ! -d "$REPO_ROOT" ]; then + echo "FAIL: not a directory: $REPO_ROOT" >&2 + exit 2 +fi + +mapfile -t FILES < <(find "$REPO_ROOT" -name '*.adoc' -type f -not -path '*/.git/*' | sort) + +if [ ${#FILES[@]} -eq 0 ]; then + echo "PASS: no .adoc files found (nothing to check)" + exit 0 +fi + +BROKEN=() +CLEAN=0 +SKIPPED=0 + +for f in "${FILES[@]}"; do + rel="${f#"$REPO_ROOT/"}" + rel="${rel#./}" + + skip=0 + for allowed in "${ALLOWED[@]}"; do + if [ "$rel" = "$allowed" ]; then skip=1; break; fi + done + if [ $skip -eq 1 ]; then + SKIPPED=$((SKIPPED + 1)) + continue + fi + + if out=$(asciidoctor --failure-level=WARN -o /dev/null "$f" 2>&1) && [ -z "$out" ]; then + CLEAN=$((CLEAN + 1)) + else + BROKEN+=("$rel") + BROKEN+=("$(echo "$out" | sed 's/^/ /')") + fi +done + +if [ ${#BROKEN[@]} -eq 0 ]; then + echo "PASS: $CLEAN .adoc files render cleanly (${SKIPPED} quarantined in ALLOWED)" + exit 0 +fi + +echo "FAIL: $(( ${#BROKEN[@]} / 2 )) .adoc file(s) do not render cleanly:" >&2 +for ((i = 0; i < ${#BROKEN[@]}; i += 2)); do + echo " - ${BROKEN[i]}" >&2 + echo "${BROKEN[i + 1]}" >&2 +done +echo "" >&2 +echo "Fix the markup, or — only for pre-existing boilerplate — add a justified" >&2 +echo "entry to the ALLOWED list in scripts/check-docs-render.sh." >&2 +exit 1