From 0760d0c21f506a24084d23de53b4d11e9583dd00 Mon Sep 17 00:00:00 2001 From: cndgrr <59120057+cndgrr@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:59:18 +0000 Subject: [PATCH 1/5] feat(guards): the vendored manifest guards ceremony own tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/VENDORED.txt is already machine-authoritative on the consumer side — actions/docs-sync reads it as the sole declaration and enforces "manifest union .ceremony/, nothing else". Nothing enforced the other end: a doctrine file landing at ceremony every root with nobody adding it to the manifest is a SILENT miss, because docs-sync only asserts byte-identity for the files the manifest names. Two directions, two mechanisms (#251 D2): manifest -> tree is a scan (regular, non-empty, tracked, no symlink, no directory, no .. escape); tree -> manifest is a closed-world rule over root *.md with a short in-script exemption list, because nothing in the tree answers "which files are vendorable". Refs #251 --- .github/scripts/vendored-check.sh | 221 ++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100755 .github/scripts/vendored-check.sh diff --git a/.github/scripts/vendored-check.sh b/.github/scripts/vendored-check.sh new file mode 100755 index 0000000..126c067 --- /dev/null +++ b/.github/scripts/vendored-check.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# The vendored-manifest self-guard (issue #251; #248's near-miss). Consumers +# mirror the agent-facing doc set declared by docs/VENDORED.txt, and +# actions/docs-sync already enforces "manifest ∪ .ceremony/, nothing else" +# on the CONSUMER side. Nothing enforced the other end: a new doctrine file +# could land at ceremony's root and nobody add it to the manifest, and the +# miss is SILENT — docs-sync asserts byte-identity for the files the +# manifest names, so a doc it omits is never checked and every consumer +# drifts doctrine-blind with green guards. #248 (RELEASES.md) nearly shipped +# that way, caught only by a hand-written `grep -Fx RELEASES.md` row in +# test/docs-sync.test.sh — the hardcoded list this guard abolishes, one +# layer down. That row is deleted; this script carries its intent. +# +# Two directions, two mechanisms, because only one of them can be a scan +# (#251 D2): +# +# * MANIFEST → TREE is a scan: every entry resolves to a regular, +# non-empty, tracked file at the declared path — no symlink (PR #43: +# a symlink read as doctrine while staying invisible), no directory, +# no `../` escape. +# * TREE → MANIFEST cannot scan, because nothing in the tree answers +# "which files are vendorable" — the manifest is the only +# machine-readable notion of it. So it gets a CLOSED-WORLD RULE +# instead: every `*.md` at the repository ROOT is either in the +# manifest or in the exemption list below. Adding a root doc then +# forces a one-line decision — vendor it or exempt it — and the +# refusal names the file and both fixes. +# +# The rule is ROOT-LEVEL `*.md` ONLY. It does not walk docs/, actions/ or +# drills/: those hold no agent-facing doctrine, and a recursive version +# would grow the exemption list past the length at which a reviewer still +# reads it — which is the failure this rule is shaped against. +# +# The exemption list lives HERE, in the script. CONTRIBUTING.md's +# vendored-set sentence is documentation, never an input: two declarations +# of the same set is the drift the manifest exists to prevent. +# +# Usage: vendored-check.sh [tree-dir] (default: the repo root — the CI +# step; tests point it at fixture trees) +set -euo pipefail + +MANIFEST="docs/VENDORED.txt" + +tree="${1:-.}" + +# The root docs that are deliberately ceremony-only. Each carries the reason +# it is not vendored, because the reason is what lets the next reviewer +# judge the next addition. Prints the reason and returns 0 when exempt. +exempt_reason() { + case "$1" in + README.md) + echo "ceremony's own front page — a consumer's router is AGENTS.md, not this repo's README" + ;; + CONTRIBUTING.md) + echo "repo-specific facts (this repo's roster, scopes and conventions); every governed repo writes its own" + ;; + CHANGELOG.md) + echo "ceremony's own release history; a consumer keeps its own" + ;; + FLEET.md) + echo "the operator's fleet map — about running the fleet, not about how a governed repo works" + ;; + *) return 1 ;; + esac +} + +die() { + printf 'vendored-check: %s\n' "$@" >&2 + exit 1 +} + +manifest_file="$tree/$MANIFEST" +[ -f "$manifest_file" ] || die \ + "no $MANIFEST under $tree — the manifest is the sole declaration of the" \ + " vendored doc set, and this guard has nothing to guard without it." + +# Blank lines are skipped, exactly as actions/docs-sync reads it: the guard +# and the tool must accept the same file, or one of them is the bug. +mapfile -t manifest < <(grep -v '^[[:space:]]*$' "$manifest_file" || true) +[ "${#manifest[@]}" -gt 0 ] || die \ + "$MANIFEST is empty — an empty doctrine set is a ceremony bug, not a repo" \ + " with no rules." + +# Whether the tracked-file assertion can bind: only when the tree IS a git +# work tree root. Fixture trees are plain directories, and asserting +# tracked-ness against an enclosing repository would be asserting about the +# wrong tree. +tracked_check=no +if command -v git >/dev/null 2>&1; then + toplevel="$(git -C "$tree" rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$toplevel" ] && [ "$toplevel" = "$(cd "$tree" && pwd -P)" ]; then + tracked_check=yes + fi +fi + +# Every refusal is collected and reported together, one multi-line string +# per offending file: a guard that stops at the first problem makes a +# builder pay one CI round per file. +problems=() + +# --- manifest → tree --------------------------------------------------------- + +for entry in "${manifest[@]}"; do + case "$entry" in + /* | *..*) + problems+=("$( + printf '%s\n' \ + "$MANIFEST names '$entry' — an absolute path or a '..' escape. The" \ + " mirror writes only inside a consumer's .ceremony/, so a path that" \ + " leaves it is never vendorable." \ + " Fix: name the path relative to the repository root, with no '..'." + )") + continue + ;; + esac + + path="$tree/$entry" + + # -L before -f: `[ -f ]` follows the link, so a symlink to a real file + # would otherwise pass as a regular one. + if [ -L "$path" ]; then + problems+=("$( + printf '%s\n' \ + "$MANIFEST names '$entry', which is a SYMLINK. A symlink vendors as" \ + " doctrine while its content lives somewhere the mirror never checks" \ + " (PR #43's round: it read as doctrine and stayed invisible)." \ + " Fix: make '$entry' a regular file, or drop the entry from $MANIFEST." + )") + continue + fi + if [ -d "$path" ]; then + problems+=("$( + printf '%s\n' \ + "$MANIFEST names '$entry', which is a DIRECTORY. The manifest declares" \ + " files, one per line — a directory entry vendors nothing." \ + " Fix: name each file under '$entry' on its own line, or drop the entry." + )") + continue + fi + if [ ! -f "$path" ]; then + problems+=("$( + printf '%s\n' \ + "$MANIFEST names '$entry' but the tree has no such file. Every consumer" \ + " mirroring this ref would fail on it." \ + " Fix: add '$entry' to the tree, or remove it from $MANIFEST." + )") + continue + fi + if [ ! -s "$path" ]; then + problems+=("$( + printf '%s\n' \ + "$MANIFEST names '$entry', which is EMPTY. An empty file vendors as" \ + " doctrine that says nothing, and is read as doctrine anyway." \ + " Fix: write '$entry', or remove it from $MANIFEST." + )") + continue + fi + if [ "$tracked_check" = yes ] && + ! git -C "$tree" ls-files --error-unmatch -- "$entry" >/dev/null 2>&1; then + problems+=("$( + printf '%s\n' \ + "$MANIFEST names '$entry', which is not TRACKED. A file absent from the" \ + " tag's tree cannot be fetched by a consumer syncing at that tag," \ + " however present it is on this machine." \ + " Fix: git add '$entry', or remove it from $MANIFEST." + )") + continue + fi +done + +# --- tree → manifest: the closed world over root `*.md` ---------------------- + +in_manifest() { + local p + for p in "${manifest[@]}"; do + [ "$p" = "$1" ] && return 0 + done + return 1 +} + +shopt -s nullglob +exempted=() +vendored=() +for path in "$tree"/*.md; do + doc="${path##*/}" + if in_manifest "$doc"; then + vendored+=("$doc") + continue + fi + if reason="$(exempt_reason "$doc")"; then + exempted+=("$doc — $reason") + continue + fi + problems+=("$( + printf '%s\n' \ + "'$doc' is a root doc in NEITHER list. Every root *.md is either vendored" \ + " doctrine — mirrored into every governed repo at .ceremony/ — or" \ + " deliberately ceremony-only, and nothing in the tree says which, so the" \ + " decision has to be written down. Fix, one of:" \ + " * add '$doc' to $MANIFEST, if it is agent-facing doctrine that every" \ + " governed repo must carry;" \ + " * add '$doc' to the exemption list in" \ + " .github/scripts/vendored-check.sh, with the reason it stays" \ + " ceremony-only." + )") +done +shopt -u nullglob + +if [ "${#problems[@]}" -gt 0 ]; then + { + printf 'vendored-check: %d problem(s) — docs/VENDORED.txt and the tree disagree.\n\n' \ + "${#problems[@]}" + printf '%s\n\n' "${problems[@]}" + } >&2 + exit 1 +fi + +printf 'vendored-check: %d manifest entries resolve; %d root docs vendored, %d exempt.\n' \ + "${#manifest[@]}" "${#vendored[@]}" "${#exempted[@]}" +[ "${#vendored[@]}" -eq 0 ] || printf ' vendored: %s\n' "${vendored[@]}" +[ "${#exempted[@]}" -eq 0 ] || printf ' exempt: %s\n' "${exempted[@]}" From 5677710b2c806882a573c55f61495c6006c42ee5 Mon Sep 17 00:00:00 2001 From: cndgrr <59120057+cndgrr@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:01:39 +0000 Subject: [PATCH 2/5] test(guards): the manifest guard drives the whole test plan One CI step beside the self-ref pin, and test/vendored.test.sh covering both directions: the manifest -> tree scan (missing, symlink, directory, empty, ../ escape, absolute, untracked) and the closed-world root rule (neither list, vendored, exempted, prose is not an input, no recursion below the root), plus the real tree unmodified and the RELEASES.md regression both ways. The one-off `grep -Fx RELEASES.md` row at test/docs-sync.test.sh is deleted (#251 D4): two spellings of "the manifest is right" is the drift the manifest exists to prevent. Its intent is now a guard case, which the next doctrine file inherits for free. Refs #251 --- .github/workflows/ci.yml | 5 + test/docs-sync.test.sh | 12 ++- test/vendored.test.sh | 228 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 test/vendored.test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c17c424..e082bb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,11 @@ jobs: # Five stale markers survived the tags that shipped their machinery # (#221); #238 makes the release candidate reject that drift. run: bash .github/scripts/marker-check.sh + - name: Vendored manifest + # The manifest rules (issue #251; #248's near-miss): a doctrine file + # at the root that nobody added to docs/VENDORED.txt is invisible to + # every consumer's docs-sync, so it fails CI here instead. + run: bash .github/scripts/vendored-check.sh - name: Tests env: # The npm-backed version_write case may skip locally when npm is diff --git a/test/docs-sync.test.sh b/test/docs-sync.test.sh index f5ecae9..ff0f5b0 100644 --- a/test/docs-sync.test.sh +++ b/test/docs-sync.test.sh @@ -18,11 +18,13 @@ SCRIPT="$ROOT/actions/docs-sync/docs-sync.sh" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT -# RELEASES.md's consumer-availability promise is true only when the real -# manifest carries it (#248's review round). The fixture cases below prove -# manifest-driven behavior; this row binds that behavior to the promised file. -check "real manifest includes the release doctrine" 0 "RELEASES.md" \ - grep -Fx RELEASES.md "$ROOT/docs/VENDORED.txt" +# The real manifest is asserted by test/vendored.test.sh, not here (#251 D4). +# A `grep -Fx RELEASES.md` row lived at this spot from #248's review round, +# binding the promise to the one file that had nearly been missed. It was the +# hardcoded list the manifest exists to abolish, one layer down: two spellings +# of "the manifest is right" is exactly the drift it prevents. Its intent — +# every root doctrine file is declared, RELEASES.md included — is now a +# closed-world guard case, which the next file inherits for free. # --- fixture builders -------------------------------------------------------- diff --git a/test/vendored.test.sh b/test/vendored.test.sh new file mode 100644 index 0000000..e8030a4 --- /dev/null +++ b/test/vendored.test.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# Contract tests for .github/scripts/vendored-check.sh (issue #251) — the +# self-guard that makes docs/VENDORED.txt authoritative over ceremony's OWN +# tree. Driven against constructed fixture trees plus the real one; the CI +# step runs the same script against the real tree. +# +# The fixture doc set is deliberately NOT ceremony's real six: a guard that +# hardcodes the vendored list instead of reading the manifest fails these +# rows, which is the failure the whole issue is about. +# +# set -u, not -e: failing commands are behavior for the harness to inspect. +set -u + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=test/harness.sh +. "$ROOT/test/harness.sh" + +CHECK="$ROOT/.github/scripts/vendored-check.sh" + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# --- fixture builders -------------------------------------------------------- + +# tree — a fixture tree carrying only the manifest. +tree() { + local dir="$TMP/$1" + shift + rm -rf "$dir" + mkdir -p "$dir/docs" + printf '%s\n' "$@" >"$dir/docs/VENDORED.txt" +} + +# doc [content] — a regular file in a fixture tree. +doc() { + local path="$TMP/$1/$2" + mkdir -p "$(dirname "$path")" + printf '%s\n' "${3:-# a doc}" >"$path" +} + +# real_copy — the real tree reduced to what the guard reads: the +# manifest, every path the manifest names, and every root *.md. Cases mutate +# the copy, so the guard's verdict on ceremony's actual doc set is proven +# without touching the working tree. +real_copy() { + local dir="$TMP/$1" entry + rm -rf "$dir" + mkdir -p "$dir/docs" + cp "$ROOT/docs/VENDORED.txt" "$dir/docs/VENDORED.txt" + cp "$ROOT"/*.md "$dir/" + while IFS= read -r entry; do + [ -n "$entry" ] || continue + mkdir -p "$dir/$(dirname "$entry")" + cp "$ROOT/$entry" "$dir/$entry" + done <"$ROOT/docs/VENDORED.txt" +} + +run_check() { + bash "$CHECK" "$TMP/$1" +} + +# --- the happy tree ---------------------------------------------------------- + +# One manifest entry lives in a subdirectory: the manifest is PATHS, not +# filenames (docs-sync's fixtures prove the same), and the closed-world rule +# over the root must not regress that to root-only. +tree ok AGENTS.md RULES.md guide/DEEP.md +doc ok AGENTS.md +doc ok RULES.md +doc ok guide/DEEP.md +check "a tree whose root docs are all declared passes" 0 "3 manifest entries resolve" \ + run_check ok + +tree blanks AGENTS.md '' RULES.md +doc blanks AGENTS.md +doc blanks RULES.md +check "blank manifest lines are skipped, as docs-sync skips them" 0 "2 manifest entries" \ + run_check blanks + +# --- the closed world: a root doc in neither list ---------------------------- + +# The #248 near-miss, replayed as a test: a new doctrine file lands at the +# root and nobody adds it to the manifest. +tree newdoc AGENTS.md +doc newdoc AGENTS.md +doc newdoc NEWDOC.md +check "a root doc in neither list reds" 1 "'NEWDOC.md' is a root doc in NEITHER list" \ + run_check newdoc +check "...and the refusal names the manifest fix" 1 "add 'NEWDOC.md' to docs/VENDORED.txt" \ + run_check newdoc +check "...and the refusal names the exemption fix" 1 "add 'NEWDOC.md' to the exemption list" \ + run_check newdoc + +# The decision the guard forces, taken each way: vendor it… +tree newdoc-vendored AGENTS.md NEWDOC.md +doc newdoc-vendored AGENTS.md +doc newdoc-vendored NEWDOC.md +check "a root doc added to the manifest passes" 0 "2 manifest entries" \ + run_check newdoc-vendored + +# …or exempt it. The exemption list is in the script and carries a reason; +# README.md is one of the four ceremony-only root docs it names. +tree exempted AGENTS.md +doc exempted AGENTS.md +doc exempted README.md +check "a root doc on the exemption list passes, with its reason" 0 "exempt: README.md" \ + run_check exempted + +# The exemption list is NEVER read from prose. CONTRIBUTING.md's vendored-set +# sentence is documentation; two declarations of the same set is the drift +# the manifest exists to prevent (#251 D2's second must-fail). +tree prose AGENTS.md +doc prose AGENTS.md +doc prose EXTRA.md +doc prose CONTRIBUTING.md "The vendored set is AGENTS.md and EXTRA.md." +check "a doc declared only in prose still reds" 1 "'EXTRA.md' is a root doc in NEITHER list" \ + run_check prose + +# --- the rule is ROOT-level only --------------------------------------------- + +# A guard that walked the tree would need an exemption list long enough that +# nobody reads it — the exact failure the root-only rule is shaped against. +# So an undeclared *.md under docs/, actions/ or drills/ must stay GREEN. +tree subdirs AGENTS.md +doc subdirs AGENTS.md +doc subdirs docs/CONSUMERS.md +doc subdirs actions/thing/README.md +doc subdirs drills/2026-07-01.md +check "undeclared *.md below the root stays green (no recursion)" 0 "1 manifest entries" \ + run_check subdirs + +# --- manifest → tree: the scan ----------------------------------------------- + +tree missing AGENTS.md GONE.md +doc missing AGENTS.md +check "a manifest entry with no file reds, naming it" 1 "names 'GONE.md' but the tree has no such file" \ + run_check missing + +tree symlinked AGENTS.md LINK.md +doc symlinked AGENTS.md +ln -s AGENTS.md "$TMP/symlinked/LINK.md" +check "a manifest entry pointing at a symlink reds" 1 "names 'LINK.md', which is a SYMLINK" \ + run_check symlinked + +tree dir-entry AGENTS.md guide +doc dir-entry AGENTS.md +mkdir -p "$TMP/dir-entry/guide" +check "a manifest entry pointing at a directory reds" 1 "names 'guide', which is a DIRECTORY" \ + run_check dir-entry + +tree empty-entry AGENTS.md HOLLOW.md +doc empty-entry AGENTS.md +: >"$TMP/empty-entry/HOLLOW.md" +check "a manifest entry pointing at an empty file reds" 1 "names 'HOLLOW.md', which is EMPTY" \ + run_check empty-entry + +# The escape case exists as a docs-sync fixture; ceremony's own manifest must +# not be the one place it goes unchecked. +tree escape AGENTS.md ../outside.md +doc escape AGENTS.md +check "a manifest entry escaping with ../ reds" 1 "names '../outside.md'" \ + run_check escape + +tree absolute AGENTS.md /etc/hosts +doc absolute AGENTS.md +check "an absolute manifest entry reds" 1 "names '/etc/hosts'" \ + run_check absolute + +# --- the manifest itself ----------------------------------------------------- + +rm -rf "$TMP/no-manifest" +mkdir -p "$TMP/no-manifest" +check "a tree with no manifest reds" 1 "no docs/VENDORED.txt under" \ + run_check no-manifest + +rm -rf "$TMP/empty-manifest" +mkdir -p "$TMP/empty-manifest/docs" +: >"$TMP/empty-manifest/docs/VENDORED.txt" +check "an empty manifest reds" 1 "is empty" run_check empty-manifest + +# --- tracked-ness ------------------------------------------------------------ + +# A file present on this machine but absent from the tag's tree cannot be +# fetched by a consumer syncing at that tag. The assertion binds only where +# it can: when the tree IS a git work tree root. +tree tracked AGENTS.md RULES.md +doc tracked AGENTS.md +doc tracked RULES.md +git init -q "$TMP/tracked" +git -C "$TMP/tracked" add docs/VENDORED.txt AGENTS.md RULES.md +check "a git tree whose manifest entries are all tracked passes" 0 "2 manifest entries" \ + run_check tracked + +tree untracked AGENTS.md RULES.md +doc untracked AGENTS.md +doc untracked RULES.md +git init -q "$TMP/untracked" +git -C "$TMP/untracked" add docs/VENDORED.txt AGENTS.md +check "a git tree with an untracked manifest entry reds" 1 "names 'RULES.md', which is not TRACKED" \ + run_check untracked + +# --- the real tree ----------------------------------------------------------- + +check "this tree, unmodified, is green" 0 "manifest entries resolve" bash "$CHECK" "$ROOT" + +# The #248 near-miss on the REAL doc set: a scratch root doc nobody declared. +real_copy scratch +doc scratch SCRATCHDOC.md +check "a scratch root doc on the real tree reds, naming it" 1 "'SCRATCHDOC.md' is a root doc in NEITHER list" \ + run_check scratch + +# RELEASES.md stays listed — the regression criterion #248's review round +# bought, now asserted BY THE GUARD rather than by a hardcoded `grep -Fx` row +# in test/docs-sync.test.sh (#251 D1, D4). The closed world holds in both +# directions: dropping it from the manifest alone reds… +real_copy releases-dropped +grep -v '^RELEASES\.md$' "$ROOT/docs/VENDORED.txt" >"$TMP/releases-dropped/docs/VENDORED.txt" +check "dropping RELEASES.md from the manifest alone reds" 1 "'RELEASES.md' is a root doc in NEITHER list" \ + run_check releases-dropped + +# …and it is green only when the file leaves the root in the same breath. +real_copy releases-gone +grep -v '^RELEASES\.md$' "$ROOT/docs/VENDORED.txt" >"$TMP/releases-gone/docs/VENDORED.txt" +rm -f "$TMP/releases-gone/RELEASES.md" +check "dropping RELEASES.md from the manifest AND the root is green" 0 "manifest entries resolve" \ + run_check releases-gone + +summary From 7909383ca0564462f81fba68b065c0a415231df3 Mon Sep 17 00:00:00 2001 From: cndgrr <59120057+cndgrr@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:04:02 +0000 Subject: [PATCH 3/5] docs(consumers): read the pin manifest, never a copy of it Re-vendor tooling and docs-sync equivalents read the pin docs/VENDORED.txt (available at 0.5.0 and later) instead of naming the doc set themselves, so a new doctrine file reaches every consumer at its next ordinary pin bump with zero list edits. A hardcoded list propagates nothing and its staleness is silent: docs-sync --check asserts byte-identity for the files the list names and says nothing about one it omits. What makes reading the manifest sufficient rather than merely better is the self-guard this PR adds, tagged unreleased until the first tag carries it, per the RELEASES.md paragraph above it. Refs #251 --- changelog.d/251.md | 14 ++++++++++++++ docs/CONSUMERS.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 changelog.d/251.md diff --git a/changelog.d/251.md b/changelog.d/251.md new file mode 100644 index 0000000..7853b80 --- /dev/null +++ b/changelog.d/251.md @@ -0,0 +1,14 @@ +### Added + +- CI now refuses a root `*.md` declared in neither `docs/VENDORED.txt` nor the + guard's short exemption list, so a new doctrine file can no longer reach a + tag undeclared and stay invisible to every consumer's `docs-sync` (#251). +- The same guard reads the manifest the other way: every entry must resolve to + a regular, non-empty, tracked file — no symlink, no directory, no `../` + escape (#251). + +### Changed + +- Consumer guidance: re-vendor tooling reads the pin's `docs/VENDORED.txt`, + never a hardcoded list, so a new doctrine file propagates at the next + ordinary pin bump with zero list edits (#251). diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 413385f..b684e12 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -598,6 +598,37 @@ It is **unreleased** (#248) until that tag exists: consumers add `.ceremony/RELEASES.md` only with the ordinary pin bump and re-sync, never by copying it ahead of their pinned doctrine set. +### Read the manifest, never a copy of it + +Anything on the consumer's side that needs to know *which* documents are +vendored — a re-vendor script, a `docs-sync` equivalent, the task list of a +conversion issue — reads **the pin's `docs/VENDORED.txt`** and never names +the files itself. The manifest is available at the pinned ref from `0.5.0` +and later (ceremony#251); it is one path per line, relative to ceremony's +root, and blank lines are ignored: + +```sh +# the vendored doc set at the ref this repo is pinned to +curl -fsSL "https://raw.githubusercontent.com/heavy-duty/ceremony//docs/VENDORED.txt" +``` + +That is the whole benefit: a doctrine file added in ceremony — `RELEASES.md` +was the last, ceremony#248 — reaches every consumer at its next **ordinary +pin bump**, with **zero list edits** anywhere. A hardcoded list propagates +nothing, and its staleness is silent rather than red: `docs-sync --check` +asserts byte-identity for the files the list names and says nothing at all +about one it omits, so a consumer keeps a green guard while governing +itself with doctrine it no longer has. + +What makes reading the manifest *sufficient* — rather than merely better +than a copy — is that ceremony's CI now refuses a root doctrine file that is +declared in neither the manifest nor a short in-script exemption list +(`.github/scripts/vendored-check.sh`), so the manifest at a tag is the +complete set as of that tag. That guarantee is **unreleased** (#251) until +the first tag carrying it exists; the manifest is worth reading at every +earlier pin regardless, since it is what `actions/docs-sync` has always +mirrored. + The consumer's ci.yml gains the guard alongside the others: ```yaml From b9eedd8973f764afc91326fdc475c23cdea12e82 Mon Sep 17 00:00:00 2001 From: cndgrr <59120057+cndgrr@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:05:04 +0000 Subject: [PATCH 4/5] docs(consumers): cite the manifest availability to the tag that shipped it 0.5.0 availability is actions/docs-sync own arrival (#19); #251 is the guidance and the guarantee, not the file date. Refs #251 --- docs/CONSUMERS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index b684e12..17cbc13 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -604,8 +604,8 @@ Anything on the consumer's side that needs to know *which* documents are vendored — a re-vendor script, a `docs-sync` equivalent, the task list of a conversion issue — reads **the pin's `docs/VENDORED.txt`** and never names the files itself. The manifest is available at the pinned ref from `0.5.0` -and later (ceremony#251); it is one path per line, relative to ceremony's -root, and blank lines are ignored: +and later — it shipped with `actions/docs-sync` itself (ceremony#19) — and +it is one path per line, relative to ceremony's root, blank lines ignored: ```sh # the vendored doc set at the ref this repo is pinned to From 42b8fb685061add49c55b5edadcd03fcc3819995 Mon Sep 17 00:00:00 2001 From: cndgrr <59120057+cndgrr@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:29:44 +0000 Subject: [PATCH 5/5] fix(consumers): the manifest is readable at 0.1.0, not 0.5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/VENDORED.txt and actions/docs-sync/docs-sync.sh entered the tree in the same commit and are byte-identical at every tag — blobs 10c20a3c and ba426479 at 0.1.0 through 0.5.0 — and 0.1.0's copy of the tool is already manifest-driven (MANIFEST="docs/VENDORED.txt", L75). Citing 0.5.0 told the 0.1.0-0.4.1 tail, which is exactly the population this section is written for, that the manifest was unavailable at its pin, so it would keep the hardcoded list: #251's failure mode reproduced by the document that exists to abolish it. The same file already said 0.1.0 at L131-L133. Also make the guard's tracked-ness skip announce itself. It degrades to "not asserted" wherever the tree is not a git work tree root, and doing that in silence is the shape this script's own header argues against, so the skip now prints on both output paths, green and red, with a test row each way. Round 1: claude blocking point, and claude nit 3. --- .github/scripts/vendored-check.sh | 13 +++++++++++++ docs/CONSUMERS.md | 7 ++++--- test/vendored.test.sh | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/scripts/vendored-check.sh b/.github/scripts/vendored-check.sh index 126c067..929e4fe 100755 --- a/.github/scripts/vendored-check.sh +++ b/.github/scripts/vendored-check.sh @@ -85,11 +85,22 @@ mapfile -t manifest < <(grep -v '^[[:space:]]*$' "$manifest_file" || true) # work tree root. Fixture trees are plain directories, and asserting # tracked-ness against an enclosing repository would be asserting about the # wrong tree. +# +# When it cannot bind, SAY SO. This guard's whole argument is that a silent +# miss is worse than a loud one, and a guard that quietly stops asserting one +# of its four properties is exactly that shape — so the skip is announced on +# every run, green or red, rather than inferred from the absence of a +# refusal (#251 round 1). tracked_check=no +tracked_note="tracked-ness NOT asserted: $tree is not a git work tree root, so + 'is this file in the tag's tree' cannot be answered about THIS tree. The + other three manifest assertions (regular file, non-empty, no + symlink/dir/escape) still bind." if command -v git >/dev/null 2>&1; then toplevel="$(git -C "$tree" rev-parse --show-toplevel 2>/dev/null || true)" if [ -n "$toplevel" ] && [ "$toplevel" = "$(cd "$tree" && pwd -P)" ]; then tracked_check=yes + tracked_note="" fi fi @@ -211,11 +222,13 @@ if [ "${#problems[@]}" -gt 0 ]; then printf 'vendored-check: %d problem(s) — docs/VENDORED.txt and the tree disagree.\n\n' \ "${#problems[@]}" printf '%s\n\n' "${problems[@]}" + [ -z "$tracked_note" ] || printf 'vendored-check: %s\n' "$tracked_note" } >&2 exit 1 fi printf 'vendored-check: %d manifest entries resolve; %d root docs vendored, %d exempt.\n' \ "${#manifest[@]}" "${#vendored[@]}" "${#exempted[@]}" +[ -z "$tracked_note" ] || printf 'vendored-check: %s\n' "$tracked_note" [ "${#vendored[@]}" -eq 0 ] || printf ' vendored: %s\n' "${vendored[@]}" [ "${#exempted[@]}" -eq 0 ] || printf ' exempt: %s\n' "${exempted[@]}" diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 17cbc13..bd68963 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -603,9 +603,10 @@ copying it ahead of their pinned doctrine set. Anything on the consumer's side that needs to know *which* documents are vendored — a re-vendor script, a `docs-sync` equivalent, the task list of a conversion issue — reads **the pin's `docs/VENDORED.txt`** and never names -the files itself. The manifest is available at the pinned ref from `0.5.0` -and later — it shipped with `actions/docs-sync` itself (ceremony#19) — and -it is one path per line, relative to ceremony's root, blank lines ignored: +the files itself. The manifest is available at the pinned ref from `0.1.0` +and later — it shipped with `actions/docs-sync` itself (ceremony#19), in the +same commit, and that tool has read it rather than a list since — and it is +one path per line, relative to ceremony's root, blank lines ignored: ```sh # the vendored doc set at the ref this repo is pinned to diff --git a/test/vendored.test.sh b/test/vendored.test.sh index e8030a4..50ee65d 100644 --- a/test/vendored.test.sh +++ b/test/vendored.test.sh @@ -199,6 +199,21 @@ git -C "$TMP/untracked" add docs/VENDORED.txt AGENTS.md check "a git tree with an untracked manifest entry reds" 1 "names 'RULES.md', which is not TRACKED" \ run_check untracked +# ...and where it cannot bind, the skip ANNOUNCES ITSELF rather than being +# inferred from the absence of a refusal (#251 round 1). A guard that quietly +# stops asserting one of its four properties is the silent miss this whole +# script argues against, so the degradation is visible on both output paths. +check "a non-git tree says tracked-ness was not asserted" 0 "tracked-ness NOT asserted" \ + run_check ok +check "...and says it on the red path too, beside the refusals" 1 "tracked-ness NOT asserted" \ + run_check newdoc + +# The converse, so the note is not simply always printed: where the tree IS a +# git work tree root the assertion bound, and nothing is announced. +no_skip_note() { ! run_check tracked 2>&1 | grep -qF "tracked-ness NOT asserted"; } +check "a git work tree root announces no skip — the assertion bound" 0 "" \ + no_skip_note + # --- the real tree ----------------------------------------------------------- check "this tree, unmodified, is green" 0 "manifest entries resolve" bash "$CHECK" "$ROOT"