From 17bfdb1924e1f59ceb031fda76463e730d5525de Mon Sep 17 00:00:00 2001 From: vk Date: Thu, 3 Sep 2026 00:32:45 +0530 Subject: [PATCH 1/3] tests(inventory-contract): add --write/--repoint to fix the two-field stale dance Every stale-inventory failure resolves the same hand-rolled way: recompute payload_digest, set derived_at.head to HEAD, commit. Agents kept getting one field wrong. --write (alias --repoint) reuses payload_digest_compute(), refuses on a dirty payload tree, writes both fields with jq, re-validates, and prints the commit command. FAIL messages for both stale fields now end with the fix. --- tests/README.md | 12 ++++++++ tests/inventory-contract.sh | 55 +++++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index dfad4f4..bfa7eab 100644 --- a/tests/README.md +++ b/tests/README.md @@ -106,6 +106,18 @@ check-time oracle, not a source of truth `install.sh` consumes. If the installer file, the question "does the inventory match what installs?" becomes unfalsifiable, because the two would no longer be independent derivations of the same fact. +`tests/inventory-contract.sh --print-digest` is the only supported way to recompute +`derived_at.payload_digest` by hand -- it prints the value `payload_digest_compute()` produces and +nothing else, so checking a number against the file never means retyping the recipe. + +`tests/inventory-contract.sh --write` (alias `--repoint`) is the fix for the recurring failure +this file goes stale with: a payload commit lands and `derived_at.head`/`derived_at.payload_digest` +still name the commit before it. It refuses on a dirty payload tree (commit the payload first -- +the head must name a commit), otherwise sets `derived_at.head` to `git rev-parse HEAD` and +`derived_at.payload_digest` to `payload_digest_compute()`, writes the file back with jq (same key +order and indent already on disk), re-validates, and on a clean result prints the commit command to +run. Idempotent: running it again on an already-repointed clean tree changes nothing. + ## What this proves This setup's core property is that Claude Code skills fire on the situation diff --git a/tests/inventory-contract.sh b/tests/inventory-contract.sh index 595ee28..131b992 100755 --- a/tests/inventory-contract.sh +++ b/tests/inventory-contract.sh @@ -10,6 +10,13 @@ # # Not yet wired into .claude/verify.sh or CI (see tests/README.md). Run by hand: # tests/inventory-contract.sh +# +# `--write` (alias `--repoint`) is the ONLY supported way to re-point derived_at.head and +# derived_at.payload_digest at HEAD after a payload commit. It refuses on a dirty payload tree, +# reuses payload_digest_compute() below rather than a second implementation, writes the file with +# jq (stable key order, same 2-space indent already on disk), re-validates, and on a clean result +# prints the commit command to run: +# tests/inventory-contract.sh --write set -u cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1 @@ -83,6 +90,43 @@ command -v jq >/dev/null 2>&1 || { echo "FAIL jq is required to validate $INV" [ -f "$INV" ] || { echo "FAIL $INV not found" >&2; exit 1; } jq empty "$INV" >/dev/null 2>&1 || { echo "FAIL $INV does not parse as JSON" >&2; exit 1; } +# --- --write / --repoint --------------------------------------------------------------------- +# The two-step dance every stale-inventory failure below actually asks for: recompute +# payload_digest with the one blessed implementation (payload_digest_compute(), never a hand-typed +# recipe), set derived_at.head to the commit that recipe just ran against, write both fields back +# with jq so key order and indent survive untouched, then re-run the validation this same script +# does on every other invocation so `--write` cannot itself ship a stale file. +# +# Refuses on a dirty payload tree rather than repointing at a HEAD the digest was not actually +# computed from: derived_at.head names the commit payload_digest describes, and a payload file +# with uncommitted changes means HEAD is not that commit yet -- committing first is the only way +# to make the claim true, not something this flag can paper over. +WRITE_MODE=0 +if [ "${1:-}" = "--write" ] || [ "${1:-}" = "--repoint" ]; then + WRITE_MODE=1 + # shellcheck disable=SC2086 + dirty=$(git status --porcelain -- $PAYLOAD_PATHS) + if [ -n "$dirty" ]; then + echo "FAIL inventory-contract --write: the payload has uncommitted changes:" >&2 + printf '%s\n' "$dirty" >&2 + echo "commit the payload first, the head must name a commit" >&2 + exit 2 + fi + write_head=$(git rev-parse HEAD) + write_digest=$(payload_digest_compute) + write_tmp=$(mktemp "${INV}.XXXXXX") || { echo "FAIL inventory-contract --write: mktemp failed" >&2; exit 1; } + if ! jq --arg h "$write_head" --arg d "$write_digest" \ + '.derived_at.head = $h | .derived_at.payload_digest = $d' \ + "$INV" > "$write_tmp"; then + echo "FAIL inventory-contract --write: jq failed to write $INV" >&2 + rm -f "$write_tmp" + exit 1 + fi + mv "$write_tmp" "$INV" + echo "inventory-contract --write: derived_at.head -> $write_head, derived_at.payload_digest -> $write_digest" + echo +fi + # --- contract_version: the hand-bumped SCHEMA version. An unknown value must fail loudly -- a # validator that silently accepts a schema it does not understand is worse than no validator, # because everything below reads fields whose meaning may have changed out from under it. @@ -116,7 +160,7 @@ stated_digest=$(jq -r '.derived_at.payload_digest' "$INV") if [ "$computed_digest" = "$stated_digest" ]; then pass "payload_digest matches the tree ($computed_digest)" else - fail "payload_digest" "claude/inventory.json's derived_at.payload_digest is $stated_digest; the tree's is $computed_digest. The snapshot is stale." + fail "payload_digest" "claude/inventory.json's derived_at.payload_digest is $stated_digest; the tree's is $computed_digest. The snapshot is stale. run: tests/inventory-contract.sh --write" fi # The file no longer carries an executable recipe, so nothing here can be tricked into running @@ -172,7 +216,7 @@ elif git merge-base --is-ancestor "$stated_head" HEAD 2>/dev/null; then # thing that is actually checkable, and which is false in exactly the case above. _hd_moved=$(git diff --name-only "$stated_head" HEAD -- $PAYLOAD_PATHS 2>/dev/null) if [ -n "$_hd_moved" ]; then - fail "derived_at.head" "derived_at.head is $(printf '%.12s' "$stated_head") and is an ancestor of HEAD, but payload has changed since it: $(printf '%s' "$_hd_moved" | tr '\n' ' ') -- so payload_digest was not computed from that commit, whatever this field says. Re-point head at the commit whose payload the digest describes" + fail "derived_at.head" "derived_at.head is $(printf '%.12s' "$stated_head") and is an ancestor of HEAD, but payload has changed since it: $(printf '%s' "$_hd_moved" | tr '\n' ' ') -- so payload_digest was not computed from that commit, whatever this field says. Re-point head at the commit whose payload the digest describes. run: tests/inventory-contract.sh --write" else pass "derived_at.head is an ancestor of HEAD with no payload change since ($(printf '%.12s' "$stated_head"), $(git rev-list --count "$stated_head..HEAD" 2>/dev/null) commit(s) ago)" fi @@ -308,8 +352,15 @@ if [ "$FAIL" -eq 0 ]; then else echo "inventory-contract: $RAN checks, all clean" fi + if [ "$WRITE_MODE" -eq 1 ]; then + echo + echo "commit: git commit -am \"Re-point inventory at $(git rev-parse --short HEAD)\"" + fi exit 0 else echo "inventory-contract: $RAN checks, $SKIPPED skipped, FAILURES ABOVE" + if [ "$WRITE_MODE" -eq 1 ]; then + echo "--write updated derived_at.head/payload_digest but validation still fails on the above -- not printing a commit command over a file that is not clean" >&2 + fi exit 1 fi From 146420919f9b4485eb1386ad527d63dca5c845c1 Mon Sep 17 00:00:00 2001 From: vk Date: Thu, 3 Sep 2026 00:35:51 +0530 Subject: [PATCH 2/3] fix(inventory-contract): make --write a true no-op when already clean Unconditionally bumping derived_at.head to current HEAD made --write rewrite the file after any non-payload commit, even when the contract already validated. needs_repoint() mirrors the contract's own staleness definition so --write only touches the file when payload_digest or derived_at.head actually needs fixing, matching the idempotence the flag promises. --- tests/inventory-contract.sh | 53 ++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/tests/inventory-contract.sh b/tests/inventory-contract.sh index 131b992..73f8d5e 100755 --- a/tests/inventory-contract.sh +++ b/tests/inventory-contract.sh @@ -101,7 +101,29 @@ jq empty "$INV" >/dev/null 2>&1 || { echo "FAIL $INV does not parse as JSON" >& # computed from: derived_at.head names the commit payload_digest describes, and a payload file # with uncommitted changes means HEAD is not that commit yet -- committing first is the only way # to make the claim true, not something this flag can paper over. +# `needs_repoint` mirrors the contract's own definition of stale for these two fields ONLY well +# enough to answer "is there anything to fix" -- it is not a second implementation of the digest +# (payload_digest_compute() is still the only place that runs) and it does not replace the real +# checks below, which still run afterward and print the authoritative pass/fail lines. Without +# this, --write would rewrite derived_at.head to current HEAD on every call, even one made right +# after an unrelated non-payload commit where the file already validates clean -- which is not a +# fix, it is churn, and it breaks the "second --write on a clean tree changes nothing" contract. +needs_repoint(){ + [ "$(payload_digest_compute)" != "$(jq -r '.derived_at.payload_digest' "$INV")" ] && return 0 + h=$(jq -r '.derived_at.head // empty' "$INV") + [ -z "$h" ] && return 0 + printf '%s' "$h" | grep -qE '^[0-9a-f]{40}$' || return 0 + # Absent from a shallow checkout: the real check below SKIPs rather than fails this case, so + # --write must not force a rewrite it cannot justify either. + git cat-file -e "$h^{commit}" 2>/dev/null || return 1 + git merge-base --is-ancestor "$h" HEAD 2>/dev/null || return 0 + # shellcheck disable=SC2086 + [ -n "$(git diff --name-only "$h" HEAD -- $PAYLOAD_PATHS 2>/dev/null)" ] && return 0 + return 1 +} + WRITE_MODE=0 +WROTE=0 if [ "${1:-}" = "--write" ] || [ "${1:-}" = "--repoint" ]; then WRITE_MODE=1 # shellcheck disable=SC2086 @@ -112,18 +134,23 @@ if [ "${1:-}" = "--write" ] || [ "${1:-}" = "--repoint" ]; then echo "commit the payload first, the head must name a commit" >&2 exit 2 fi - write_head=$(git rev-parse HEAD) - write_digest=$(payload_digest_compute) - write_tmp=$(mktemp "${INV}.XXXXXX") || { echo "FAIL inventory-contract --write: mktemp failed" >&2; exit 1; } - if ! jq --arg h "$write_head" --arg d "$write_digest" \ - '.derived_at.head = $h | .derived_at.payload_digest = $d' \ - "$INV" > "$write_tmp"; then - echo "FAIL inventory-contract --write: jq failed to write $INV" >&2 - rm -f "$write_tmp" - exit 1 + if needs_repoint; then + write_head=$(git rev-parse HEAD) + write_digest=$(payload_digest_compute) + write_tmp=$(mktemp "${INV}.XXXXXX") || { echo "FAIL inventory-contract --write: mktemp failed" >&2; exit 1; } + if ! jq --arg h "$write_head" --arg d "$write_digest" \ + '.derived_at.head = $h | .derived_at.payload_digest = $d' \ + "$INV" > "$write_tmp"; then + echo "FAIL inventory-contract --write: jq failed to write $INV" >&2 + rm -f "$write_tmp" + exit 1 + fi + mv "$write_tmp" "$INV" + WROTE=1 + echo "inventory-contract --write: derived_at.head -> $write_head, derived_at.payload_digest -> $write_digest" + else + echo "inventory-contract --write: derived_at.head and derived_at.payload_digest already satisfy the contract; nothing to do" fi - mv "$write_tmp" "$INV" - echo "inventory-contract --write: derived_at.head -> $write_head, derived_at.payload_digest -> $write_digest" echo fi @@ -352,14 +379,14 @@ if [ "$FAIL" -eq 0 ]; then else echo "inventory-contract: $RAN checks, all clean" fi - if [ "$WRITE_MODE" -eq 1 ]; then + if [ "$WRITE_MODE" -eq 1 ] && [ "$WROTE" -eq 1 ]; then echo echo "commit: git commit -am \"Re-point inventory at $(git rev-parse --short HEAD)\"" fi exit 0 else echo "inventory-contract: $RAN checks, $SKIPPED skipped, FAILURES ABOVE" - if [ "$WRITE_MODE" -eq 1 ]; then + if [ "$WRITE_MODE" -eq 1 ] && [ "$WROTE" -eq 1 ]; then echo "--write updated derived_at.head/payload_digest but validation still fails on the above -- not printing a commit command over a file that is not clean" >&2 fi exit 1 From 073f8ccd144cecf5792e4313a08f6505e8ca1712 Mon Sep 17 00:00:00 2001 From: vk Date: Thu, 3 Sep 2026 00:46:16 +0530 Subject: [PATCH 3/3] fix(inventory-contract): give the two new shellcheck disables a reason Check 30 (shellcheck suppressions carry a reason) failed on both new SC2086 disables in needs_repoint() and the --write dirty check -- bare, no same-line or preceding-comment reason. Matches the inline-reason convention used elsewhere in the repo (bootstrap.sh:228, install.sh:751). --- tests/inventory-contract.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/inventory-contract.sh b/tests/inventory-contract.sh index 73f8d5e..7bdd704 100755 --- a/tests/inventory-contract.sh +++ b/tests/inventory-contract.sh @@ -117,7 +117,7 @@ needs_repoint(){ # --write must not force a rewrite it cannot justify either. git cat-file -e "$h^{commit}" 2>/dev/null || return 1 git merge-base --is-ancestor "$h" HEAD 2>/dev/null || return 0 - # shellcheck disable=SC2086 + # shellcheck disable=SC2086 # PAYLOAD_PATHS is a deliberate word list of pathspecs, same as payload_digest_compute() above [ -n "$(git diff --name-only "$h" HEAD -- $PAYLOAD_PATHS 2>/dev/null)" ] && return 0 return 1 } @@ -126,7 +126,7 @@ WRITE_MODE=0 WROTE=0 if [ "${1:-}" = "--write" ] || [ "${1:-}" = "--repoint" ]; then WRITE_MODE=1 - # shellcheck disable=SC2086 + # shellcheck disable=SC2086 # PAYLOAD_PATHS is a deliberate word list of pathspecs, same as payload_digest_compute() above dirty=$(git status --porcelain -- $PAYLOAD_PATHS) if [ -n "$dirty" ]; then echo "FAIL inventory-contract --write: the payload has uncommitted changes:" >&2