ceremony/test/upstream-delta.test.sh

307 lines
14 KiB
Bash
Raw Normal View History

docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
#!/usr/bin/env bash
# The delta-inventory guard (#200, enforcing #197 D3's standing constraint).
#
# Forge-specific behaviour lives in the files docs/UPSTREAM-SYNC.md names,
# and nowhere else.
#
# WHY. This tree carries upstream's content plus a forge delta, forever — the
# sync is recurring and upstream is read-only (#197 D3). What makes that
# tractable is not luck: the 0.6.0 sync cost 18 conflict hunks across 10 files
# because the delta sits in six files upstream never touches. Scatter
# `forge_detect` into a seventh and every future sync pays for it, in a place
# nobody chose.
#
# So this fails on the PR that scatters it, rather than on the sync after next
# — the same reason changelog-monotonic guards a property no single tree can
# be asked about.
#
# IT RUNS OFFLINE, AND IT DOES NOT PASS BY ABSENCE. Tests must not need the
# network, and a guard that goes green when it cannot see its input is the
# blind-sweep shape this repo keeps writing issues about. So the inventory is
# read from the document and the tree is scanned for forge markers — both local
# — and a missing or empty `.upstream-ref` is a REFUSAL, not a skip.
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=test/harness.sh
. "$ROOT/test/harness.sh"
DOC="$ROOT/docs/UPSTREAM-SYNC.md"
REF="$ROOT/.upstream-ref"
# --- the recorded upstream ref ------------------------------------------------
check "the sync document exists" 0 "" test -f "$DOC"
check "the upstream ref is recorded" 0 "" test -f "$REF"
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# THE OBJECT IS MANDATORY. An earlier head accepted "UNVERIFIABLE-HERE" when
# upstream's commit was not in the local store, which contradicted this issue's
# own "must not pass by absence" and made the check decorative in exactly the
# environment it matters in (@codex-reviewer-andresmgsl, #200 review).
#
# "Runs offline" means the TEST reads local evidence, not that CI may omit the
# evidence and pass. ci.yml fetches the recorded object before running the
# suite; if it is missing here, that is a refusal.
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
recorded_ref() { grep -vE '^[[:space:]]*(#|$)' "$REF" | head -n1; }
ref_is_recorded() { [ -n "$(recorded_ref)" ]; }
check "...and is not blank — an unrecorded ref is a refusal, not a skip" 0 "" \
ref_is_recorded
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# Full 40, not a short prefix: a short SHA is ambiguous across a growing repo
# and cannot be compared byte-wise between the runbook, the CHANGELOG and here.
ref_is_full_sha() { printf '%s' "$(recorded_ref)" | grep -qE '^[0-9a-f]{40}$'; }
check "...and is a FULL 40-character SHA" 0 "" ref_is_full_sha
ref_object_present() { git -C "$ROOT" cat-file -e "$(recorded_ref)^{commit}" 2>/dev/null; }
check "the recorded commit is present locally — absent is a refusal, not a skip" 0 "" \
ref_object_present
ref_is_ancestor() { git -C "$ROOT" merge-base --is-ancestor "$(recorded_ref)" HEAD 2>/dev/null; }
check "...and is an ancestor of HEAD, so main really carries what is recorded" 0 "" \
ref_is_ancestor
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
# --- the inventory ------------------------------------------------------------
# The document's inventory table: the first `code` span of each table row in
# the "Where the forge delta lives" section.
inventory() {
awk '/^## Where the forge delta lives/ { on = 1; next }
on && /^## / { on = 0 }
on && /^\| `/ { gsub(/^\| `/, ""); sub(/`.*$/, ""); print }' "$DOC"
}
inventory_size() { inventory | wc -l; }
inventory_is_populated() { [ "$(inventory_size)" -ge 6 ]; }
check "the inventory names at least the six known delta files" 0 "" \
inventory_is_populated
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# Exact for a file entry, `dir/` + one path separator for a directory entry.
# Prefix matching accepted `drills-old/x` for `drills/` and `lib/forge.sh.backup`
# for `lib/forge.sh` (@codex-reviewer-andresmgsl, #200 review).
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
in_inventory() { # $1 = repo-relative path
local entry
while IFS= read -r entry; do
[ -n "$entry" ] || continue
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
case "$entry" in
*/) case "$1" in "$entry"*) return 0 ;; esac ;;
*) [ "$1" = "$entry" ] && return 0 ;;
esac
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
done < <(inventory)
return 1
}
check "a listed file is recognised" 0 "" in_inventory lib/forge.sh
check "...including one listed as a directory" 0 "" in_inventory drills/0.4.1.md
check "an unlisted file is not" 1 "" in_inventory lib/version.sh
# --- the scan -----------------------------------------------------------------
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# What "forge-specific" means, mechanically: the selector's own verbs, the
# environment override that drives them, and — on a workflow, which has no
# shell to call the selector from — a forge decision written inline.
#
# The earlier version scanned only shell under lib/ actions/ bin/ .github/scripts.
# That missed three categories the inventory itself claims to govern: workflows,
# .github/labels.conf and drills/. Merged main already had blind spots there —
# labels.yml decides on GITHUB_SERVER_URL and declares a client, refs-guard.yml
# carries a positive GitHub-only scheduling decision
# (@codex-reviewer-andresmgsl, #200 review).
FORGE_MARKERS='forge_detect|forge_select|forge_preflight|forge_client|CEREMONY_FORGE\b|CEREMONY_FORGE_CLIENT|GITHUB_SERVER_URL|github\.server_url'
# SCAN_ROOT is a parameter so the teeth below can drive the REAL check against
# a constructed tree. The previous mutation proved the predicates and never
# invoked no_unlisted — the guard could have been `return 0` and still passed.
scan_root() { printf '%s' "${SCAN_ROOT:-$ROOT}"; }
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
# DISCOVERY IS DERIVED FROM THE TREE, not from a list of directories, depths
# and extensions. The earlier version hand-picked five globs and therefore
# could not see `actions/*/action.yml` — this repository's normal composite
# structure, where a client declaration is exactly a forge decision — or a
# workflow written `.yaml` rather than `.yml`. @codex-reviewer-andresmgsl
# constructed both and the guard stayed 21/21 green, which is the whole
# argument against maintaining a glob list.
#
# So: walk everything, then EXCLUDE by class, and let content classify the
# rest. Excluding is safer than including because a new file type arrives
# scanned rather than invisible.
#
# test/ the harness stubs and asserts these tokens by design
# changelog.d/ prose fragments
# *.md prose. `drills/` stays in the INVENTORY because its records
# are forge-specific by content, but a record mentioning a
# selector verb in prose is not a decision, and scanning prose
# for decisions is the mistake this guard's own comment
# handling exists to avoid.
#
# DISCOVERY IS GIT'S, NOT THE FILESYSTEM'S. An earlier head said "tracked" and
# used `find`, which walks the working directory and knows nothing about the
# index. That is not pedantry: `ci.yml` extracts shellcheck and actionlint
# tarballs INTO the checkout before the suite runs, and any developer cache
# sits there too. @codex-reviewer-andresmgsl reproduced a false red with one
# untracked file. `git ls-files -z` makes "tracked" executable rather than
# prose.
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
scanned_paths() {
local root; root="$(scan_root)"
git -C "$root" ls-files -z 2>/dev/null \
| tr '\0' '\n' \
| grep -vE '^(test/|changelog\.d/)' \
| grep -vE '\.md$' \
| sort
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
}
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
forge_specific_files() {
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
local root rel; root="$(scan_root)"
while IFS= read -r rel; do
[ -n "$rel" ] || continue
sed 's/[[:space:]]#.*$//; s/^[[:space:]]*#.*$//' "$root/$rel" 2>/dev/null \
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
| grep -qE "$FORGE_MARKERS" && printf '%s\n' "$rel"
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
done < <(scanned_paths)
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
return 0
}
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# A file that merely CALLS the shim is not where the forge is decided — every
# reconciler calls forge_preflight and that is the point of the shim. A file
# that DECLARES a client is a different thing and is NOT exempt: it is a
# deliberate forge-delta location and belongs in the inventory
# (@codex-reviewer-andresmgsl).
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
SHIM_CONSUMERS='actions/issueflow-reconcile/issueflow-reconcile.sh
actions/labels-reconcile/labels-reconcile.sh
actions/labels-scope/labels-scope.sh
lib/facts.sh
lib/ruling.sh
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
lib/attention.sh
.github/workflows/release.yml'
declares_a_client() {
sed 's/[[:space:]]#.*$//; s/^[[:space:]]*#.*$//' "$(scan_root)/$1" \
| grep -qE 'CEREMONY_FORGE_CLIENT[=:]'
}
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
unlisted_deciders() {
local rel
while IFS= read -r rel; do
[ -n "$rel" ] || continue
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
if grep -qxF "$rel" <<<"$SHIM_CONSUMERS" && ! declares_a_client "$rel"; then
continue
fi
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
in_inventory "$rel" && continue
printf '%s\n' "$rel"
done < <(forge_specific_files)
}
no_unlisted() {
local found
found="$(unlisted_deciders)"
[ -z "$found" ] || {
printf 'forge-specific but not in docs/UPSTREAM-SYNC.md:\n' >&2
printf ' %s\n' "$found" >&2
return 1
}
}
check "every forge-deciding file is named in the inventory" 0 "" no_unlisted
# --- teeth --------------------------------------------------------------------
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# These drive the REAL top-level check against a constructed tree, via
# SCAN_ROOT. The earlier version asserted the predicates separately and never
# invoked no_unlisted — so the guard could have been replaced with `return 0`
# and both "must-fail" rows would still have passed
# (@codex-reviewer-andresmgsl, #200 review). A mutation test that cannot fail
# when the thing it guards is deleted is the shape this repo keeps filing
# issues about.
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
# A tiny GIT repository, because discovery is git's now: a fixture that is only
# a directory would be invisible to `ls-files` and every must-fail below would
# pass vacuously.
fixture_tree() { # -> a tracked tree with no forge-deciding files
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
local t="$TMP/tree"
rm -rf "$t"
mkdir -p "$t/lib" "$t/.github/workflows"
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
printf '#!/usr/bin/env bash\necho hello\n' >"$t/lib/plain.sh"
printf 'name: ci\non: [push]\n' >"$t/.github/workflows/plain.yml"
git -C "$t" init -q 2>/dev/null
git -C "$t" add -A 2>/dev/null
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
printf '%s' "$t"
}
track() { git -C "$root" add -A 2>/dev/null; }
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
root="$(fixture_tree)"
clean_tree_passes() { SCAN_ROOT="$root" no_unlisted; }
check "the guard is green on a tree with no forge decisions" 0 "" clean_tree_passes
# MUST FAIL: a shell file that decides the forge, in no inventory entry.
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
# shellcheck disable=SC2016 # fixture CONTENT: the literal text a scanned file would hold
printf '%s\n' '#!/usr/bin/env bash' 'case "$(forge_detect)" in forgejo) : ;; esac' \
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
>"$root/lib/scattered.sh"
track; scattered_shell() { SCAN_ROOT="$root" no_unlisted; }
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
check "a new SHELL file deciding the forge fails the real guard" 1 "" scattered_shell
check "...and the failure names the file" 1 "lib/scattered.sh" scattered_shell
rm -f "$root/lib/scattered.sh"; track
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# MUST FAIL on a NON-SHELL surface too, so coverage cannot regress to the old
# shell-only glob: a workflow deciding on the server URL.
# shellcheck disable=SC2016 # fixture CONTENT: the literal text a scanned file would hold
printf '%s\n' 'name: x' 'on: [push]' 'jobs:' ' j:' ' steps:' \
' - run: [ "$GITHUB_SERVER_URL" = https://github.com ] || exit 0' \
>"$root/.github/workflows/scattered.yml"
track; scattered_workflow() { SCAN_ROOT="$root" no_unlisted; }
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
check "a new WORKFLOW deciding the forge fails it too" 1 "" scattered_workflow
check "...naming that file" 1 ".github/workflows/scattered.yml" scattered_workflow
rm -f "$root/.github/workflows/scattered.yml"; track
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
# @codex-reviewer-andresmgsl's two reproductions, verbatim as fixtures. Both
# passed the hand-picked-glob version 21/21, which is why discovery is derived
# from the tree now. Deleting a discovery class must make these red.
mkdir -p "$root/actions/unlisted-forge-decision"
printf '%s\n' 'name: x' 'runs:' ' using: composite' ' steps:' \
' - shell: bash' ' env:' ' CEREMONY_FORGE_CLIENT: gh' \
' run: true' >"$root/actions/unlisted-forge-decision/action.yml"
track; composite_action_seen() { SCAN_ROOT="$root" no_unlisted; }
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
check "a forge declaration in actions/*/action.yml fails the guard" 1 \
"actions/unlisted-forge-decision/action.yml" composite_action_seen
rm -rf "$root/actions/unlisted-forge-decision"; track
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
# ...and a workflow written .yaml rather than .yml — `*.yml` was never a
# complete workflow surface.
# shellcheck disable=SC2016 # fixture CONTENT: the literal text a scanned file would hold
printf '%s\n' 'name: x' 'on: [push]' 'jobs:' ' j:' \
" if: github.server_url == 'https://github.com'" ' steps: []' \
>"$root/.github/workflows/unlisted-forge-decision.yaml"
track; yaml_workflow_seen() { SCAN_ROOT="$root" no_unlisted; }
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
check "...and one in a .yaml workflow does too" 1 \
".github/workflows/unlisted-forge-decision.yaml" yaml_workflow_seen
rm -f "$root/.github/workflows/unlisted-forge-decision.yaml"; track
fix(upstream-delta): discovery derives from the tree, not from a glob list (#200) @codex-reviewer-andresmgsl did not argue this one, he reproduced it: an `actions/*/action.yml` declaring CEREMONY_FORGE_CLIENT and a workflow written `.yaml` rather than `.yml`, both invisible to the hand-picked globs, guard still 21/21 green. The first is not an edge case — `actions/*/action.yml` is this repository's normal composite structure and a client declaration there IS a forge decision. The second shows `*.yml` was never a complete workflow surface. So discovery walks the tree and EXCLUDES by class rather than enumerating directories, depths and extensions. Excluding is the safer default: a new file type arrives scanned rather than invisible. Out of scope are .git/, test/ (whose harness asserts these tokens by design), changelog.d/ and *.md — prose, including drills/, which stays in the inventory because its records are forge-specific by CONTENT while a record mentioning a selector verb in prose is not a decision. Both of his reproductions are now fixtures driving the real no_unlisted, and restricting discovery back to *.sh reds four cases. The documentation claim is aligned with what the guard does rather than what the table implies: it checks forge DECISIONS in executable and configuration files; it is not a diff against upstream, so drills/ and labels.conf are listed by judgement rather than found by scan. Saying otherwise made labels.conf and drills/ look like evidence of completeness while action.yml was invisible. upstream-delta 24/24; test/run.sh 29/29; shellcheck 0.10.0, actionlint and changelog-armed clean. Refs #200
2026-08-05 13:58:32 +00:00
check "...leaving the fixture tree green again" 0 "" clean_tree_passes
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# A declaration is a delta location even in a file that would otherwise read as
# a shim consumer, so the consumer allow-list cannot hide one.
mkdir -p "$root/lib"
printf '%s\n' '#!/usr/bin/env bash' 'CEREMONY_FORGE_CLIENT=gh' >"$root/lib/facts.sh"
track; declared_not_exempt() { SCAN_ROOT="$root" no_unlisted; }
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
check "a shim consumer that DECLARES a client is not exempt" 1 "lib/facts.sh" \
declared_not_exempt
rm -f "$root/lib/facts.sh"; track
track
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
check "...and the tree is green again once it is gone" 0 "" clean_tree_passes
# UNTRACKED input is not source. ci.yml extracts shellcheck and actionlint
# tarballs into the checkout before the suite runs, and a developer cache sits
# there too; parsing either is a false red on something outside the repository
# property (@codex-reviewer-andresmgsl, #200 review — reproduced with one file).
printf 'CEREMONY_FORGE_CLIENT=gh\n' >"$root/local-tool-cache.txt"
check "an UNTRACKED marker-bearing file is ignored" 0 "" clean_tree_passes
check "...and is still ignored once it carries a decision" 0 "" clean_tree_passes
track
check "...but the moment it is TRACKED the guard sees it" 1 "local-tool-cache.txt" \
clean_tree_passes
git -C "$root" rm -q --cached local-tool-cache.txt 2>/dev/null
rm -f "$root/local-tool-cache.txt"
check "...and removing it restores green" 0 "" clean_tree_passes
fix(upstream-delta): the object is mandatory, the scan covers every governed surface, and the teeth drive the real guard (#200) @codex-reviewer-andresmgsl's five points. Three were correctness, and one of them found that my must-fail cases could not fail. 1. THE OBJECT IS MANDATORY. UNVERIFIABLE-HERE is gone: a missing ref, an absent object and a non-ancestor are three distinct refusals. The ref is now the FULL 40-character SHA, and ci.yml fetches exactly that object before the suite. "Runs offline" means the TEST reads local evidence; it never meant CI may omit the evidence and pass. 2. THE SCAN COVERS WHAT THE INVENTORY CLAIMS. It walked shell under four globs and never looked at workflows, .github/labels.conf or drills/ — three categories the inventory governs. Widened, and it immediately found four real blind spots on merged main: refs-not-closing's declaration, labels.yml's inline forge decision, refs-guard.yml's GitHub-only scheduling and release-exercise.yml's pinned CEREMONY_FORGE. All four are now inventory entries with the issue that removes them, because a delta location with no exit is indistinguishable from one nobody noticed. A file that DECLARES a client is no longer exempt as a "consumer" — only files that merely CALL the shim are. 3. THE TEETH NOW DRIVE THE GUARD. They asserted the predicates separately and never invoked no_unlisted, so the guard could have been `return 0` and both must-fail rows would still have passed. SCAN_ROOT is a parameter now and the cases build a tree, add an unlisted decider — shell AND workflow, so coverage cannot regress to the old glob — and assert the real top-level check fails naming it. Replacing no_unlisted with `return 0` reds five. 4. PATH MATCHING, NOT PREFIX MATCHING. `drills/` accepted `drills-old/x` and `lib/forge.sh` accepted `lib/forge.sh.backup`. Exact for files, `dir/` for directories, with both negative boundaries covered. 5. THE IMMUTABLE SHA IS CAPTURED AT FETCH. The runbook now takes upstream_sha=$(git rev-parse gh/main) once and merges and records that value. This is not hypothetical: while this PR was in review upstream moved from 8c3a4d1 to 08e2912, and re-reading gh/main at recording time would have written a commit this tree does not contain. I caught that by walking into it. test/run.sh 29/29; upstream-delta 21/21; shellcheck 0.10.0, actionlint, changelog-armed clean. Refs #200
2026-08-05 13:51:21 +00:00
# Path matching, both boundaries.
check "a directory entry does not match a sibling with the same prefix" 1 "" \
in_inventory drills-old/0.4.1.md
check "a file entry does not match a longer path" 1 "" in_inventory lib/forge.sh.backup
check "...while the real ones still match" 0 "" in_inventory drills/0.4.1.md
docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
summary