Files
claude/lib/tests/doctrine-citers.test.sh
T
bastien 27f201d4aa feat(guardrails): refusal ends the attempt; doctrine-citers census; make test suite=
Root causes of the 2026-09-24 errors turned into mechanisms (BDR-100). hard_deny 'Routing around a guardrail': a refused command is never rerun through a wrapper, alias, heredoc, Makefile target, env file, other shell or other agent; the same clause in 14 agents and in the doctrine's sub-agent rule. make test suite=<file> runs one suite hermetically so the denied env-prefix form is never needed by hand. lib/tests/doctrine-citers.test.sh: every CLAUDE.md "Section" / § Label citation across skills, agents, lib, rules and hooks must resolve to a heading or bold label (flip-tested); its first run fixed rest-api-node.md. Doctrine 'After code changes' step 4: a changed rule, heading, label or threshold → grep every citer in the same commit.
2026-09-24 20:58:25 +02:00

64 lines
3.4 KiB
Bash

#!/usr/bin/env bash
# lib/tests/doctrine-citers.test.sh — every citation of a CLAUDE.global.md
# section or bold label from skills/agents/lib/rules/hooks must resolve
# (BDR-100). Born from the 2026-09-24 density pass: a bold label was reworded
# and five skills kept pointing at "§ Language" for a day. A flip-test proves
# the checker bites before the real census runs (LRN-096).
#
# Only citations that NAME the doctrine count: `CLAUDE.md "Section"` and
# `CLAUDE.md … § Label`. Bare `section "…"` / `§ X` inside a skill refer to the
# skill's own sections and are out of scope.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
pass=0; fail=0
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
# _citers_extract <file>… → "file:line:name" per cited section (quoted) or label (§)
_citers_extract() {
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^"“]{0,12}["“][^"”]{2,60}["”]' "$@" 2>/dev/null \
| sed -E 's/^([^:]+:[0-9]+):.*["“]([^"”]+)["”]$/\1:\2/'
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^§]{0,60}§ ?[A-Z][A-Za-z][A-Za-z -]{1,40}' "$@" 2>/dev/null \
| sed -E 's/^([^:]+:[0-9]+):.*§ ?([A-Za-z][A-Za-z -]+)$/\1:\2/; s/[[:space:]]+$//'
}
# _citers_resolve <doctrine> <name> → rc 0 when a heading or a bold label starts with <name>
_citers_resolve() {
/usr/bin/grep -qE "^#+ ${2}( |$|:|\(|—)" "$1" && return 0
/usr/bin/grep -qF -- "**${2}" "$1"
}
# citers_check <doctrine> <file>… → prints DANGLING lines; rc = their count (capped 99)
citers_check() {
local doctrine="$1" line name file lno n=0; shift
while IFS= read -r line; do
[ -n "$line" ] || continue
file="${line%%:*}"; lno="$(printf '%s' "$line" | cut -d: -f2)"; name="${line#*:*:}"
_citers_resolve "$doctrine" "$name" || { echo "DANGLING: $file:$lno → \"$name\""; n=$((n+1)); }
done < <(_citers_extract "$@")
return $(( n > 99 ? 99 : n ))
}
# ── flip-test: a synthetic dangling citation must be caught ──────────────────
FIX="$(mktemp -d)"; trap 'rm -rf "$FIX"' EXIT
# shellcheck disable=SC2016 # literal backticks in the fixture heading
printf '## Alpha\n\n**Always English, always caveman**: rule.\n\n## Memory registries (`x`)\n' > "$FIX/doctrine.md"
printf 'ok: see CLAUDE.md "Alpha" and CLAUDE.md "Memory registries" (Always English, always caveman)\n' > "$FIX/good.md"
printf 'bad: (see CLAUDE.md "Memory registries" § Language) and CLAUDE.md "Beta"\n' > "$FIX/bad.md"
citers_check "$FIX/doctrine.md" "$FIX/good.md" >/dev/null; check T1-resolving-citations-pass "$?" 0
out=$(citers_check "$FIX/doctrine.md" "$FIX/bad.md"); rc=$?
check T2-dangling-section-and-label-caught "$rc" 2
check T2b-names-the-culprit "$(printf '%s\n' "$out" | grep -c 'bad.md:1')" 2
check T2c-label-named "$(printf '%s\n' "$out" | grep -c '"Language"')" 1
# ── real census: the repo's own citers against CLAUDE.global.md ──────────────
mapfile -t FILES < <(cd "$ROOT" && find skills agents lib rules hooks -type f \( -name '*.md' -o -name '*.sh' \) \
-not -path 'skills/graphify/*' -not -path 'skills/impeccable/*' -not -path 'skills/synced/*' \
-not -path 'agents/impeccable-*' -not -path 'lib/tests/*' | sort)
out=$(cd "$ROOT" && citers_check CLAUDE.global.md "${FILES[@]}"); rc=$?
[ -n "$out" ] && printf '%s\n' "$out"
check T3-repo-citations-resolve "$rc" 0
echo "PASS=$pass FAIL=$fail"
[ "$fail" -eq 0 ]