diff --git a/.editorconfig b/.editorconfig index 50421b8..26d65d3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,13 +14,17 @@ indent_size = 2 indent_style = tab # Vendored/generated trees and never-edit upstream files: leave bytes alone. +# This list is duplicated across .pre-commit-config.yaml, .typos.toml, +# .gitleaks.toml, .editorconfig, and .github/scripts/check-hashes.sh — +# update all together. [{fonts,stagit,migration,pubkeys}/**] insert_final_newline = false trim_trailing_whitespace = false -# Binary and hashed image/icon assets anywhere in the tree: an editor save +# Binary and hashed media assets anywhere in the tree: an editor save # must never alter their bytes — content-hashed filenames break otherwise -# (favicon..svg legitimately lacks a final newline). -[*.{png,webp,svg,ico,woff2}] +# (favicon..svg legitimately lacks a final newline). Covers types +# not present yet so a future asset starts protected; extend on new types. +[*.{png,webp,svg,ico,woff2,jpg,jpeg,gif,avif,mp4,webm}] insert_final_newline = false trim_trailing_whitespace = false diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 69dec5e..402e9b4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,5 @@ version: 2 -# The pre-commit ecosystem GA'd in March 2026, but some dependabot-core -# paths still gate it behind this flag; harmless if unneeded. -enable-beta-ecosystems: true - updates: - package-ecosystem: github-actions directory: / diff --git a/.github/scripts/check-hashes.sh b/.github/scripts/check-hashes.sh index 9ca0979..4490a29 100755 --- a/.github/scripts/check-hashes.sh +++ b/.github/scripts/check-hashes.sh @@ -8,9 +8,14 @@ # discovered, not hardcoded, so renames cannot leave stale references # behind; vendored trees are skipped (FONTLOG.txt holds historical # commit-pinned permalinks that are valid but not present on disk); -# (c) _header.html and errdocs/err.html agree on the hash tokens they -# reference (compared by basename, since the two files may legitimately -# use different path prefixes for the same asset). +# (c) every tracked HTML template that references hashed assets agrees +# with the others on the tokens it references — templates are +# discovered, not hardcoded, so a new template cannot escape the +# check (compared by basename, since templates may legitimately use +# different path prefixes for the same asset); +# (d) every tracked hashed asset is referenced by at least one tracked +# text file — otherwise it is orphaned: published with the site but +# reachable by nothing. # # Word-splitting over `git ls-files` output is safe here: tracked names in # this repo contain no whitespace. @@ -25,6 +30,13 @@ fail() { status=1 } +# Vendored trees never count as reference sources: FONTLOG.txt holds +# historical commit-pinned permalinks whose tokens are valid but stale. +# This list is duplicated across .pre-commit-config.yaml, .typos.toml, +# .gitleaks.toml, .editorconfig, and this script — update all together. +# Word-split deliberately when passed to git. +vendored_pathspecs=':!fonts/ :!stagit/ :!migration/ :!pubkeys/' + # --- (a) filename hash matches content hash -------------------------------- for f in $(git ls-files | grep -E '\.[0-9a-f]{64}\.' || true); do # Extract the hash by pattern, not positional dot-splitting: several @@ -40,28 +52,55 @@ refs() { grep -oE '[A-Za-z0-9_/.-]*\.[0-9a-f]{64}\.[A-Za-z0-9.]+' "$1" | sort -u } -for src in $(git grep -I -l -E '\.[0-9a-f]{64}\.' -- \ - ':!fonts/' ':!stagit/' ':!migration/' ':!pubkeys/'); do +# shellcheck disable=SC2086 # vendored_pathspecs must word-split +for src in $(git grep -I -l -E '\.[0-9a-f]{64}\.' -- $vendored_pathspecs); do for ref in $(refs "$src"); do [ -f "${ref#/}" ] || fail "$src references missing asset: $ref" done done -# --- (c) header and errdocs reference identical hash tokens ---------------- -for src in _header.html errdocs/err.html; do - [ -f "$src" ] || fail "expected template missing: $src" +# --- (d) every tracked hashed asset is referenced somewhere ---------------- +# One grep collects every .. occurrence in tracked text files; an +# asset whose token never appears is orphaned. Reference chains satisfy +# this naturally (fonts are reachable via the hashed stylesheets). +# shellcheck disable=SC2086 # vendored_pathspecs must word-split +referenced=$(git grep -I -h -oE '\.[0-9a-f]{64}\.' -- $vendored_pathspecs | sort -u) +for f in $(git ls-files | grep -E '\.[0-9a-f]{64}\.' || true); do + tok=$(expr "$f" : '.*\.\([0-9a-f]\{64\}\)\.') + case "$referenced" in + *".$tok."*) ;; + *) fail "orphaned hashed asset (referenced by nothing): $f" ;; + esac done +# --- (c) HTML templates reference identical hash tokens -------------------- tokens() { refs "$1" | sed 's|.*/||' | sort -u } -header_tokens=$(tokens _header.html) -err_tokens=$(tokens errdocs/err.html) -if [ "$header_tokens" != "$err_tokens" ]; then - fail '_header.html and errdocs/err.html disagree on hashed assets:' - printf '%s\n' '--- _header.html:' "$header_tokens" \ - '--- errdocs/err.html:' "$err_tokens" >&2 -fi +templates=$(git grep -I -l -E '\.[0-9a-f]{64}\.' -- '*.html' || true) + +# Guard against the check silently degrading: these two must always be in +# the discovered set. +for required in _header.html errdocs/err.html; do + printf '%s\n' "$templates" | grep -qx "$required" || + fail "expected template missing from agreement check: $required" +done + +base_file='' +base_tokens='' +for t in $templates; do + if [ -z "$base_file" ]; then + base_file=$t + base_tokens=$(tokens "$t") + continue + fi + t_tokens=$(tokens "$t") + if [ "$t_tokens" != "$base_tokens" ]; then + fail "$base_file and $t disagree on hashed assets:" + printf '%s\n' "--- $base_file:" "$base_tokens" \ + "--- $t:" "$t_tokens" >&2 + fi +done exit "$status" diff --git a/.github/scripts/gitleaks-push.sh b/.github/scripts/gitleaks-push.sh new file mode 100755 index 0000000..6ac223c --- /dev/null +++ b/.github/scripts/gitleaks-push.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# gitleaks-push.sh — secret scan for the pre-push hook. +# +# When pre-commit provides the pushed range (PRE_COMMIT_FROM_REF/TO_REF, +# set at the pre-push stage), scan just those commits: anything older is +# already public, so re-scanning ~1,100 commits on every push adds +# latency, not protection. Without a range — CI's +# `pre-commit run --all-files --hook-stage pre-push`, or a manual run — +# fall back to full history, which keeps the exhaustive scan in CI. +# +# gitleaks comes from the hook's golang environment, which pre-commit +# puts on PATH. + +set -eu + +from=${PRE_COMMIT_FROM_REF:-} +to=${PRE_COMMIT_TO_REF:-} + +if [ -n "$from" ] && [ -n "$to" ] && + git rev-parse --verify --quiet "$from^{commit}" > /dev/null; then + exec gitleaks git --redact --verbose --log-opts="$from..$to" +fi + +exec gitleaks git --redact --verbose diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 23b0964..05bcf16 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -27,7 +27,9 @@ jobs: with: path: ~/.cache/pre-commit key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} - - run: pipx install pre-commit==4.6.0 + # uv installs in ~1-3s where pipx takes 5-15s building a venv. + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + - run: uv tool install pre-commit==4.6.0 - run: pre-commit run --all-files --show-diff-on-failure - run: pre-commit run --all-files --hook-stage pre-push @@ -40,7 +42,8 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - - run: pipx install zizmor==1.25.2 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + - run: uv tool install zizmor==1.25.2 - run: zizmor --format=github . env: # Without a token zizmor silently skips its online audits diff --git a/.gitleaks.toml b/.gitleaks.toml index 8bbc891..c762c4d 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -3,6 +3,10 @@ [extend] useDefault = true +# The vendored-tree list is duplicated across .pre-commit-config.yaml, +# .typos.toml, .gitleaks.toml, .editorconfig, and +# .github/scripts/check-hashes.sh — update all together. fonts/ is +# deliberately absent here: font binaries hold nothing secret-shaped. [[allowlists]] description = "Public keys, archived migration content, and stagit output are published on purpose" paths = [ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2c341d..f481fd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,11 @@ # Quality gate for anthes.is. Hygiene fixers must never touch vendored # trees or content-hashed assets (editing one silently breaks its hash) — # the &generated anchor below excludes both. +# +# The vendored-tree list {fonts, stagit, migration, pubkeys} is duplicated +# in each tool's own syntax across .pre-commit-config.yaml, .typos.toml, +# .gitleaks.toml, .editorconfig, and .github/scripts/check-hashes.sh — +# update all together. default_stages: [pre-commit] repos: @@ -60,13 +65,19 @@ repos: entry: .github/scripts/check-hashes.sh language: script pass_filenames: false - always_run: true + # The check always covers the whole tree; this filter only decides + # when it runs: any change to a hashed asset or to a file type that + # references hashed assets (articles, templates, stylesheets, + # webmanifest). CI's --all-files run always triggers it. + files: '\.(md|html|css|webmanifest)$|\.[0-9a-f]{64}\.' # Local hook rather than the official one: upstream hardcodes - # --staged in its entry, which consumers cannot override, and this - # hook should scan full history at push time instead. + # --staged in its entry, which consumers cannot override. The wrapper + # scans only the pushed range locally and full history when no range + # is provided (CI's --hook-stage pre-push run, on a fetch-depth: 0 + # checkout). - id: gitleaks - name: gitleaks (full history) - entry: gitleaks git --redact --verbose + name: gitleaks + entry: .github/scripts/gitleaks-push.sh language: golang additional_dependencies: # gitleaks still declares its Go module path under the old diff --git a/.typos.toml b/.typos.toml index ad2c578..e38fe45 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,4 +1,7 @@ [files] +# The vendored-tree list is duplicated across .pre-commit-config.yaml, +# .typos.toml, .gitleaks.toml, .editorconfig, and +# .github/scripts/check-hashes.sh — update all together. extend-exclude = [ "stagit/", "migration/", diff --git a/images/Prism_slide_5.4da4ad1641e116a8dcade5317b7a3a5b35ffe193c5d7917e9598b5f6ec809c5e.2.png b/images/Prism_slide_5.4da4ad1641e116a8dcade5317b7a3a5b35ffe193c5d7917e9598b5f6ec809c5e.2.png deleted file mode 100644 index 9252dfa..0000000 Binary files a/images/Prism_slide_5.4da4ad1641e116a8dcade5317b7a3a5b35ffe193c5d7917e9598b5f6ec809c5e.2.png and /dev/null differ diff --git a/images/add-keyword-1.e909ba9f910966a19a70b462f5f8b454ca787a64be8e26ebc245a3c773328c5c.2.webp b/images/add-keyword-1.e909ba9f910966a19a70b462f5f8b454ca787a64be8e26ebc245a3c773328c5c.2.webp deleted file mode 100644 index 5b199f0..0000000 Binary files a/images/add-keyword-1.e909ba9f910966a19a70b462f5f8b454ca787a64be8e26ebc245a3c773328c5c.2.webp and /dev/null differ diff --git a/images/add-keyword-2.3d234f209c3fe0a23e66f317b04a8fe7ea8775ab51891f31606c6449b53876dc.2.webp b/images/add-keyword-2.3d234f209c3fe0a23e66f317b04a8fe7ea8775ab51891f31606c6449b53876dc.2.webp deleted file mode 100644 index ec2b09e..0000000 Binary files a/images/add-keyword-2.3d234f209c3fe0a23e66f317b04a8fe7ea8775ab51891f31606c6449b53876dc.2.webp and /dev/null differ diff --git a/images/atkinson-hyperlegible-aesu-diacritics.abafca3f601fb95c5ecb5b49f8195e58b0b7d808112c42981d3e4c00622ad865.png b/images/atkinson-hyperlegible-aesu-diacritics.abafca3f601fb95c5ecb5b49f8195e58b0b7d808112c42981d3e4c00622ad865.png deleted file mode 100644 index 220ba8c..0000000 Binary files a/images/atkinson-hyperlegible-aesu-diacritics.abafca3f601fb95c5ecb5b49f8195e58b0b7d808112c42981d3e4c00622ad865.png and /dev/null differ