fix(claude-design): derive shipped-content scope from git, not a name list

Checks (i) and (j) decided what counts as "shipped content" from a hardcoded
list of local-only file names (REMEMBER.md, TODO.md, NEXT-SESSION-PROMPT.local.md).
That list was written before the STATE.md convention replaced those three, so
STATE.md fell through it and was scanned as if it shipped. It does not: it is
gitignored and has zero tracked entries.

The resulting false positive was self-reproducing. Any STATE.md note explaining
why the check was red had to name the banned token, which made the check red.
Removing the offending line closed nothing; the next session that documented
the finding recreated it.

Two fixes were considered:

  (a) add STATE.md to the exclude list. One name, but the list stays a name
      list -- it rots again the next time a local-only file is renamed, which
      is precisely how this defect arrived.

  (b) derive the scope from git. No gitignored file can reopen the hole,
      whatever it is called.

(b) is implemented, via `git check-ignore` rather than `git ls-files`. Both
answer "is this shipped", but ls-files also drops untracked Markdown that is
NOT ignored -- new content on its way into the plugin, which is exactly when a
leak check should be looking. check-ignore keeps that in scope and excludes
only what git ignores. When git cannot answer (no repo, no binary) every file
is treated as shipped, so the checks fail loudly instead of passing on an
empty file list.

Verified both directions, denominators reported:
  known-positive: a real shipped reference/*.md carrying the banned token
                  -> FAIL, exit 1 (proves the check can still fire)
  known-negative: STATE.md, gitignored, carrying the same token
                  -> not flagged
  check (j) known-positive: shipped .md with Norwegian diacritics -> WARN
  scope: 22 shipped Markdown files of 31 on disk (9 gitignored: STATE.md +
         8 under .claude/)

validate-plugin.sh: Pass 16 / Fail 0 / Warn 0, exit 0 (was 14 / 1 / 23, exit 1)
verify.sh roll-up:  Pass 41 / Fail 0 / Warn 1, exit 0
Both re-run under /bin/bash 3.2.57 as well as bash 5.3.

The 23 warnings that disappeared were all STATE.md diacritics; they were never
shipped content, and check (j) carried the same name-list defect that (i) did.
Closes ORDRE 64.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToLVakwASPe3pXsdEiothC
This commit is contained in:
Kjell Tore Guttormsen 2026-08-17 23:22:38 +02:00
commit 3696b8e283

View file

@ -206,20 +206,72 @@ if [ "$H_HIT" -eq 0 ]; then
fi
echo ""
# -------------------------------------------------------
# Shipped-content enumeration (shared by checks (i) and (j))
#
# "Shipped" is derived from git, not from a list of file names: any file git
# ignores is session state that never leaves this machine, so it is not shipped
# content. A name list cannot hold this line -- it already rotted once, when the
# STATE.md convention replaced the three local-only files it knows about, and
# the resulting false positive was self-reproducing (any note explaining the
# failure re-triggered it).
#
# Derived from `git check-ignore` rather than `git ls-files` on purpose: an
# untracked Markdown file that is NOT ignored is about to ship and must stay in
# scope, so new content is checked before it is ever staged.
#
# When git cannot answer (no repo, no git binary) every file is treated as
# shipped: conservative, so these checks can still fail rather than silently
# passing on an empty file list.
# -------------------------------------------------------
echo "--- shipped-content enumeration (git-derived) ---"
GIT_AVAILABLE=1
if ! git -C "$PLUGIN_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
GIT_AVAILABLE=0
fi
shipped_md_files() {
find "$PLUGIN_ROOT" -type f -name '*.md' "$@" 2>/dev/null | sort | while IFS= read -r f; do
if [ "$GIT_AVAILABLE" -eq 1 ] && git -C "$PLUGIN_ROOT" check-ignore -q -- "$f" 2>/dev/null; then
continue
fi
printf '%s\n' "$f"
done
}
# grep_shipped <ERE> [find-predicates...] -> "path:line:text" lines
grep_shipped() {
local regex="$1"
shift
local f hits line
shipped_md_files "$@" | while IFS= read -r f; do
[ -n "$f" ] || continue
hits="$(grep -nE "$regex" "$f" 2>/dev/null || true)"
[ -n "$hits" ] || continue
printf '%s\n' "$hits" | while IFS= read -r line; do
printf '%s:%s\n' "$f" "$line"
done
done
}
if [ "$GIT_AVAILABLE" -eq 0 ]; then
warn "git unavailable: checks (i) and (j) treat every Markdown file as shipped content"
fi
printf " shipped Markdown files in scope: %s (of %s on disk)\n" \
"$(shipped_md_files | wc -l | tr -d ' ')" \
"$(find "$PLUGIN_ROOT" -type f -name '*.md' 2>/dev/null | wc -l | tr -d ' ')"
echo ""
# -------------------------------------------------------
# Check (i): operator-private-context grep
# -------------------------------------------------------
echo "--- (i) operator-private-context grep ---"
I_HITS="$(grep -rnE '(kjell|vegvesen|NEXT-SESSION-PROMPT|REMEMBER\.md content from)' \
"$PLUGIN_ROOT" \
--include='*.md' \
--exclude-dir='.claude' \
--exclude-dir='tests' \
--exclude='REMEMBER.md' \
--exclude='TODO.md' \
--exclude='NEXT-SESSION-PROMPT.local.md' \
2>/dev/null || true)"
I_HITS="$(grep_shipped '(kjell|vegvesen|NEXT-SESSION-PROMPT|REMEMBER\.md content from)' \
-not -path "$PLUGIN_ROOT/.claude/*" \
-not -path "$PLUGIN_ROOT/tests/*" \
|| true)"
if [ -z "$I_HITS" ]; then
pass "no operator-private context leaks in shipped content"
@ -235,14 +287,9 @@ echo ""
# -------------------------------------------------------
echo "--- (j) Norwegian-leakage grep ---"
J_HITS="$(grep -rnE '[æøåÆØÅ]' \
"$PLUGIN_ROOT" \
--include='*.md' \
--exclude-dir='.claude' \
--exclude='REMEMBER.md' \
--exclude='TODO.md' \
--exclude='NEXT-SESSION-PROMPT.local.md' \
2>/dev/null || true)"
J_HITS="$(grep_shipped '[æøåÆØÅ]' \
-not -path "$PLUGIN_ROOT/.claude/*" \
|| true)"
if [ -z "$J_HITS" ]; then
pass "no Norwegian diacritics in shipped content"