fix(lint): restore release-gate coverage of bundled Vale assets

The gate derived release-relevant paths from the dirname of each
entry's --config target. Dropping --config from .pre-commit-hooks.yaml
left that loop dead, silently removing both assets/vale/ trees from
coverage — a Vale rule change could land on main without demanding a
release tag, leaving consumers pinned to an old rev: with stale rules.

Coverage now derives from tokens[0] instead: double-dirname for the ..
normalization, guarded on the tree existing and on the bundle root not
resolving to "." so skill-size-check.sh cannot invent a bogus path.

The --config branch is removed rather than kept as dead code. Since
pre-commit rewrites only entry[0], no argument in any entry can ever
name a file this repo ships, so that shape is broken by design.

Known gap: deleting a hook's entire assets/ tree is not flagged, as the
candidate path stops existing. Deletions within a surviving tree are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
This commit is contained in:
2026-08-09 13:06:37 +00:00
parent 714e8a0c78
commit 348dd9f665
2 changed files with 80 additions and 25 deletions

View File

@@ -34,19 +34,29 @@ fi
# instead of hand-maintaining a parallel list — the manifest is the single # instead of hand-maintaining a parallel list — the manifest is the single
# source of truth for what external consumers actually pull at a pinned rev, # source of truth for what external consumers actually pull at a pinned rev,
# so a hook added/removed/renamed there can't silently drift out of sync here. # so a hook added/removed/renamed there can't silently drift out of sync here.
# Each entry is "<script> [--config <path>] [...]"; the script itself and the # Everything is derived from tokens[0], the hook's script: pre-commit prefixes
# directory holding any --config target (vale-wrap.sh needs its .vale.ini's # only entry[0] with the hook-repo clone path, so any later token that looks
# sibling styles/ tree, not just the ini file) are release-relevant. # like a path resolves against the *consuming* repo and can never name a file
# this repo ships. A hook's bundled data therefore has to be self-located
# relative to the script — vale-wrap.sh reads its own
# <script-dir>/../assets/vale/.vale.ini plus the sibling styles/ tree — which
# makes <script-dir>/../assets release-relevant alongside the script itself.
# The ../ is normalised by stripping a path component rather than with
# `realpath -m`, which is a GNU-only extension. Two guards keep the derivation
# from inventing paths: a bundle root of "." is skipped, because a script in a
# top-level directory (scripts/skill-size-check.sh) would derive the repo's own
# shared assets/, which no hook owns and whose churn must not demand a release;
# and the directory is added only when it exists, since a hook that bundles
# nothing must not contribute a pathspec matching nothing.
RELEASE_PATHS=("$HOOKS_MANIFEST") RELEASE_PATHS=("$HOOKS_MANIFEST")
while IFS= read -r entry; do while IFS= read -r entry; do
read -ra tokens <<< "$entry" read -ra tokens <<< "$entry"
[[ ${#tokens[@]} -eq 0 ]] && continue [[ ${#tokens[@]} -eq 0 ]] && continue
RELEASE_PATHS+=("${tokens[0]}") RELEASE_PATHS+=("${tokens[0]}")
for ((i = 1; i < ${#tokens[@]}; i++)); do bundle_root="$(dirname "$(dirname "${tokens[0]}")")"
if [[ "${tokens[i]}" == "--config" && -n "${tokens[i + 1]:-}" ]]; then if [[ "$bundle_root" != "." && -d "$bundle_root/assets" ]]; then
RELEASE_PATHS+=("$(dirname "${tokens[i + 1]}")") RELEASE_PATHS+=("$bundle_root/assets")
fi fi
done
done < <(sed -n 's/^[[:space:]]*entry:[[:space:]]*//p' "$HOOKS_MANIFEST") done < <(sed -n 's/^[[:space:]]*entry:[[:space:]]*//p' "$HOOKS_MANIFEST")
# Only vX.Y.Z release tags count as a baseline — an incidental checkpoint or # Only vX.Y.Z release tags count as a baseline — an incidental checkpoint or

View File

@@ -9,32 +9,45 @@ FAIL=0
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
# Helper: write a minimal .pre-commit-hooks.yaml exposing one script entry and # Both entry shapes the real .pre-commit-hooks.yaml ships: a bare script with no
# one --config-bearing entry, so RELEASE_PATHS (derived from the manifest, not # bundled data, and a bare script whose sibling assets/ tree it self-locates at
# hand-maintained) has both shapes to parse. # runtime. Neither carries arguments — pre-commit only rewrites entry[0] to the
# hook-repo clone path, so an argument path would resolve against the consuming
# repo. RELEASE_PATHS is derived from the manifest rather than hand-maintained,
# so it has to cope with both.
HOOK_DIR="plugins/demo/skills/demo-audit"
write_manifest() { write_manifest() {
local dir="$1" local dir="$1"
mkdir -p "$dir/vale" cat > "$dir/.pre-commit-hooks.yaml" <<EOF
cat > "$dir/.pre-commit-hooks.yaml" <<'EOF'
- id: fake-size-check - id: fake-size-check
entry: scripts/skill-size-check.sh entry: scripts/skill-size-check.sh
language: script language: script
- id: fake-vale-check - id: fake-vale-check
entry: scripts/vale-wrap.sh --config vale/.vale.ini entry: $HOOK_DIR/scripts/vale-wrap.sh
language: script language: script
EOF EOF
} }
# Helper: a fixture repo with a manifest and its two referenced release-relevant # Helper: writes the files both manifest entries expose — the two hook scripts
# paths, committed and tagged v1.0.0. # plus the bundled Vale config and style rule the second one self-locates.
write_release_paths() {
local dir="$1"
mkdir -p "$dir/scripts" "$dir/$HOOK_DIR/scripts" "$dir/$HOOK_DIR/assets/vale/styles/Kyberforge"
echo "v1" > "$dir/scripts/skill-size-check.sh"
echo "v1" > "$dir/$HOOK_DIR/scripts/vale-wrap.sh"
echo "cfg" > "$dir/$HOOK_DIR/assets/vale/.vale.ini"
echo "rule: v1" > "$dir/$HOOK_DIR/assets/vale/styles/Kyberforge/DemoRule.yml"
}
# Helper: a fixture repo with a manifest and every release-relevant path it
# exposes, committed and tagged v1.0.0.
make_tagged_fixture() { make_tagged_fixture() {
local dir local dir
dir="$(mktemp -d)" dir="$(mktemp -d)"
(cd "$dir" && git init -q && git config user.email t@t.t && git config user.name t) (cd "$dir" && git init -q && git config user.email t@t.t && git config user.name t)
write_manifest "$dir" write_manifest "$dir"
mkdir -p "$dir/scripts" write_release_paths "$dir"
echo "v1" > "$dir/scripts/skill-size-check.sh"
echo "cfg" > "$dir/vale/.vale.ini"
(cd "$dir" && git add -A && git commit -q -m "initial" && git tag v1.0.0) (cd "$dir" && git add -A && git commit -q -m "initial" && git tag v1.0.0)
echo "$dir" echo "$dir"
} }
@@ -65,9 +78,7 @@ echo "--- exits 1 when targeting main and no tag exists ---"
FIXTURE2="$(mktemp -d)"; track "$FIXTURE2" FIXTURE2="$(mktemp -d)"; track "$FIXTURE2"
(cd "$FIXTURE2" && git init -q && git config user.email t@t.t && git config user.name t) (cd "$FIXTURE2" && git init -q && git config user.email t@t.t && git config user.name t)
write_manifest "$FIXTURE2" write_manifest "$FIXTURE2"
mkdir -p "$FIXTURE2/scripts" write_release_paths "$FIXTURE2"
echo "v1" > "$FIXTURE2/scripts/skill-size-check.sh"
echo "cfg" > "$FIXTURE2/vale/.vale.ini"
(cd "$FIXTURE2" && git add -A && git commit -q -m "initial") (cd "$FIXTURE2" && git add -A && git commit -q -m "initial")
if run_check "$FIXTURE2" "refs/heads/main" > /dev/null; then if run_check "$FIXTURE2" "refs/heads/main" > /dev/null; then
fail "exited 0 when targeting main with no tag — expected exit 1" fail "exited 0 when targeting main with no tag — expected exit 1"
@@ -116,10 +127,10 @@ fi
echo "" echo ""
echo "--- exits 1 when a release-relevant path was deleted since the tag, not just modified ---" echo "--- exits 1 when a release-relevant path was deleted since the tag, not just modified ---"
FIXTURE6="$(make_tagged_fixture)"; track "$FIXTURE6" FIXTURE6="$(make_tagged_fixture)"; track "$FIXTURE6"
rm -rf "$FIXTURE6/vale" rm -f "$FIXTURE6/$HOOK_DIR/assets/vale/.vale.ini"
(cd "$FIXTURE6" && git add -A && git commit -q -m "delete the vale config dir") (cd "$FIXTURE6" && git add -A && git commit -q -m "delete the bundled vale config")
OUT6=$(run_check "$FIXTURE6" "refs/heads/main" || true) OUT6=$(run_check "$FIXTURE6" "refs/heads/main" || true)
if echo "$OUT6" | grep -q "vale/.vale.ini"; then if echo "$OUT6" | grep -q "assets/vale/.vale.ini"; then
pass "flags a deleted release-relevant path instead of silently dropping it from the diff" pass "flags a deleted release-relevant path instead of silently dropping it from the diff"
else else
fail "did not flag deletion of a release-relevant path since the tag" fail "did not flag deletion of a release-relevant path since the tag"
@@ -166,6 +177,40 @@ else
fail "flagged a file that no .pre-commit-hooks.yaml entry actually exposes" fail "flagged a file that no .pre-commit-hooks.yaml entry actually exposes"
fi fi
# --- 10. A change confined to a hook's bundled styles/ tree is release-relevant ---
# The manifest entry names only the wrapper script; the Vale rules it enforces
# live in the sibling assets/ tree it self-locates at runtime. If that tree is
# not covered, editing a rule and landing it on main demands no new tag, and a
# consumer pinned to the old rev keeps the stale rules forever.
echo ""
echo "--- exits 1 when only a bundled Vale style rule changed since the tag ---"
FIXTURE10="$(make_tagged_fixture)"; track "$FIXTURE10"
echo "rule: v2" > "$FIXTURE10/$HOOK_DIR/assets/vale/styles/Kyberforge/DemoRule.yml"
(cd "$FIXTURE10" && git add -A && git commit -q -m "tighten a vale rule")
OUT10=$(run_check "$FIXTURE10" "refs/heads/main" || true)
if echo "$OUT10" | grep -q "assets/vale/styles/Kyberforge/DemoRule.yml"; then
pass "flags a change confined to a hook's bundled assets/vale/styles/ tree"
else
fail "a bundled Vale style rule changed since the tag without demanding a release"
fi
# --- 11. The assets/ derivation must not invent a path for a bundle-less hook ---
# scripts/skill-size-check.sh has no sibling assets/ tree, so its derived
# candidate normalises to a bare top-level assets/ — a directory this repo does
# not ship. Adding it unconditionally would make any unrelated repo-root
# assets/ file falsely demand a release.
echo ""
echo "--- exits 0 when a top-level assets/ file changed but no hook bundles one ---"
FIXTURE11="$(make_tagged_fixture)"; track "$FIXTURE11"
mkdir -p "$FIXTURE11/assets"
echo "unrelated" > "$FIXTURE11/assets/logo.txt"
(cd "$FIXTURE11" && git add -A && git commit -q -m "add an unrelated top-level assets file")
if run_check "$FIXTURE11" "refs/heads/main" > /dev/null; then
pass "exits 0 for a top-level assets/ file that no manifest entry bundles"
else
fail "invented a bogus assets/ path for a hook script with no bundled tree"
fi
echo "" echo ""
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] [[ $FAIL -eq 0 ]]