Files
improvise/scripts/bd-audit-ids
T
Edward Langley 97a9379f79 bd-audit-ids: deleted means a deletion git will record
improvise-cota, found in the delta review of #244. The tdfd fix skipped an
absent tracked path whenever `ls-files -d` listed it. `ls-files -d` also
lists an absent --assume-unchanged path. `status` hides that path, and
`commit -a` never records the deletion, so its citations stayed in HEAD
unread and the audit exited 0.

The deletion oracle is now `diff --name-only --diff-filter=D`. It honours
both index flags and still lists a plain unstaged delete.

The new case in bd-audit-ids-test.sh was red at 33d563a9. A rival that
treats every absent path as unread fails only the deliberate
deleted-file control.

Stacked on #244 (tooling/lqs0-audit-exits).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:06:23 -07:00

106 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# The dead-ID audit: every improvise-<id> the tree cites must be a ticket bd
# knows. See AGENTS.md, "Cite a ticket ID you have read back" (improvise-52eq).
set -euo pipefail
usage() {
cat >&2 <<'EOF'
usage: bd-audit-ids
Run from anywhere inside the repository. Takes no arguments.
exit 0 every cited id is a ticket bd lists
exit 1 at least one cited id is not; each is printed
exit 3 bd could not answer: missing, failing, or listing nothing
exit 4 usage error, not inside a git repository, or a cited file
could not be read or is absent (only a file `git diff
--diff-filter=D` reports as deleted is skipped)
The comparison is POSITIVE: a cited id passes only by appearing in what
`bd list --all` returns. The recipe this replaced looked for bd's "not found"
lines and reported "all resolve" when there were none, so a bd that never ran
read as a clean audit.
THIS SCRIPT'S `$?` MUST BE THE ONE YOU READ. Nothing may follow it in the same
pipeline or `;` list.
EOF
exit 4
}
[ $# -eq 0 ] || usage
BD="${BD_AUDIT_BD:-bd}"
ROOTS=(context README.md AGENTS.md CLAUDE.md crates src tests examples web
harness docs .agents .claude)
# Crate, package, artifact and file names that are shaped like ids. Anchored,
# because an unanchored `gp` would silently exclude any real ticket id starting with it.
EXCLUDE='^improvise-(core|formula|io|protocol|command|gp|ws|worker|wasm|harness|registry|report|side|web)$'
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
top=$(git rev-parse --show-toplevel 2>/dev/null) || {
echo "bd-audit-ids: not inside a git repository, so nothing was checked." >&2
exit 4
}
cd "$top"
# A deleted tracked file is skipped ON PURPOSE: its ids are leaving the tree. Only
# `diff --diff-filter=D` says deleted; any other absent path is exit 4 (tdfd, cota).
: > "$work/absent"
git ls-files -z --cached --others --exclude-standard -- "${ROOTS[@]}" |
while IFS= read -r -d '' f; do
if [ -e "$f" ] || [ -L "$f" ]; then printf '%s\0' "$f"
elif [ -z "$(git --literal-pathspecs diff --name-only --diff-filter=D -- "$f")" ]; then
printf '%s\n' "$f" >> "$work/absent"
fi
done > "$work/files"
if [ -s "$work/absent" ]; then
echo "bd-audit-ids: tracked but absent, and not deleted, so never read:" >&2
sed 's/^/ /' "$work/absent" >&2
exit 4
fi
# xargs reports grep's no-match (1) and its read error (2) alike, as 123, so
# each batch maps 1 to success and 2 to 255: xargs stops, nonzero (GNU 124, BSD 1).
: > "$work/raw"
set +e
if [ -s "$work/files" ]; then
xargs -0 bash -c 'grep -d skip -ohE "$0" "$@"; [ $? -le 1 ] || exit 255' \
'improvise-[a-z0-9]{3,8}\b' < "$work/files" > "$work/raw"
fi
rc=$?
set -e
[ "$rc" -eq 0 ] || {
echo "bd-audit-ids: a cited file could not be read (xargs $rc), so the audit is incomplete." >&2
exit 4
}
sort -u "$work/raw" | { grep -vE "$EXCLUDE" || true; } > "$work/cited"
if ! command -v "$BD" >/dev/null 2>&1; then
echo "bd-audit-ids: '$BD' is not on PATH, so nothing was checked." >&2
exit 3
fi
if ! "$BD" list --all --limit 0 --json > "$work/known.json" 2> "$work/bd.err"; then
echo "bd-audit-ids: '$BD list' failed, so nothing was checked:" >&2
cat "$work/bd.err" >&2
exit 3
fi
if ! jq -e 'type == "array" and all(.[]; (.id | type) == "string")' \
"$work/known.json" > /dev/null 2>&1; then
echo "bd-audit-ids: '$BD list' did not print a JSON list of issues with ids." >&2
exit 3
fi
jq -r '.[].id' "$work/known.json" | sort -u > "$work/known"
if [ ! -s "$work/known" ]; then
echo "bd-audit-ids: '$BD list' returned no issues; refusing to call that a pass." >&2
exit 3
fi
comm -23 "$work/cited" "$work/known" > "$work/dead"
if [ -s "$work/dead" ]; then
echo "bd-audit-ids: cited but not a ticket bd knows:"
sed 's/^/ /' "$work/dead"
exit 1
fi
printf 'bd-audit-ids: all %d cited ids resolve (bd knows %d)\n' \
"$(wc -l < "$work/cited" | tr -d ' ')" "$(wc -l < "$work/known" | tr -d ' ')"