chore(gates): retire the external pre-commit hook contract

Why: .pre-commit-hooks.yaml and its release-tag gate served external
consumers that do not exist. No repo on the Gitea instance pins these
hooks, and the README names apm as the only supported install path. The
mechanism was also already failing: skill-size-check.sh changed after
v2.0.1 with no tag cut, and the gate cannot fire through Gitea's merge
button. (Simplification audit finding 36.)

Implementation Notes:
- Delete .pre-commit-hooks.yaml, scripts/check-release-needed.sh,
  tests/test-check-release-needed.sh and tests/test-vale-hooks-consumer.sh,
  and remove the check-release-needed pre-push hook. The repo: local
  skill-size-check and vale-audit-prefilter-* hooks are unchanged.
- ADR-0014 is amended, not retired: its runtime decision to bundle Vale
  inside factory-audit stands. The amendment keeps the entry[0]-only
  constraint (LESSONS.md:101,105) in case the export returns. ADR-0025
  gets a pointer.
- test-vale-wrap.sh: drop case 33 (the cross-manifest drift check) and
  case 28's hook-scope half, which read the published manifest. Case 32
  now also requires each hook to select every tracked file of its class,
  which keeps case 33's one-plugin-narrowing guard, with a mutation test.
- test-skill-size-check.sh and test-adr0020-contract.sh now assert the
  hook contract and verbose: true on .pre-commit-config.yaml only.
- gates.md: pre-push count goes from 9 to 8 authored hooks (11 to 10
  reported), and the Release table, the External consumers section and
  the two-manifest scope table are removed. README and script/test
  comments no longer describe the export as live. The resolver comment
  is edited identically in both copies.
- The v1.0.0/v2.0.0/v2.0.1 tags are left in place; they are inert.

ADR: 0014
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 11:52:14 +00:00
parent 2119da9700
commit 4de5b6b355
20 changed files with 233 additions and 1442 deletions

View File

@@ -21,8 +21,8 @@ fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
# Vale absent skips the Vale-DEPENDENT cases, not the suite. An early `exit 77`
# here used to skip everything, including the checks that are plain greps and
# awk over the config and the two hook manifests (cases 16, 26-28's static
# halves, 31 Parts A/B, 32, 33) -- so a machine without vale reported a skip
# awk over the config and the hook config (cases 16, 26-28's static
# halves, 31 Parts A/B, 32) -- so a machine without vale reported a skip
# while never looking at a manifest it could have read. Those still run; the
# suite exits 77 at the end only if they all passed, so run-tests.sh keeps
# reporting SKIPPED and `--strict` keeps turning that skip into a failure. A
@@ -452,10 +452,11 @@ else
fi
# --- 12. With no --config at all, the wrapper falls back to its own sibling
# assets/vale/.vale.ini. `.pre-commit-hooks.yaml` relies on this: pre-commit
# prefixes only entry[0] with the hook-repo clone path, so a --config argument
# there resolves against the consuming repo and hard-errors (E100) for every
# external consumer.
# assets/vale/.vale.ini. factory-audit's Step 1 and both prefilter hooks rely
# on this: they pass no --config. A published hook manifest would too, were it
# restored (ADR-0014): pre-commit prefixes only entry[0] with the hook-repo clone
# path, so a --config argument there resolves against the consuming repo and
# hard-errors (E100) for every external consumer.
echo ""
echo "--- defaults --config to the wrapper's own sibling assets/vale/.vale.ini ---"
FIXTURE12="$(make_fixture 2)"
@@ -464,7 +465,7 @@ OUT12=$(run_wrap "$FIXTURE12" plugins/testplugin/skills/zzzskill/SKILL.md)
if grep -q "VagueWording" <<< "$OUT12"; then
pass "a --config-less invocation uses the wrapper's bundled config"
else
fail "a --config-less invocation found no config — external pre-commit consumers get E100, the bug this test guards against"
fail "a --config-less invocation found no config — factory-audit's Step 1 and both prefilter hooks pass no --config, so they would get E100"
fi
# --- 13. No GNU-only `realpath -m`. macOS ships the BSD realpath, which has no
@@ -1080,7 +1081,7 @@ elif [[ "$WRAPPED21_FILES" != "$BARE21_FILES" ]]; then
fail "the directory walk dropped a symlinked file: wrapper saw '$WRAPPED21_FILES', bare vale '$BARE21_FILES'"
# A here-string, not `echo "$WRAPPED21" | grep -q`: the match sits on line 3 of
# 8, and under pipefail grep -q exiting early can SIGPIPE echo mid-write and
# fail this branch on correct output (see tests/test-check-release-needed.sh).
# fail this branch on correct output (see docs/spec/gates.md, Tests).
elif grep -q "VagueWording" <<< "$WRAPPED21"; then
pass "a symlinked file under a directory argument is mirrored, flattened and flagged"
else
@@ -1341,7 +1342,7 @@ fi
# exits 0. Every gate in this repo reads that as a pass.
#
# ADR-0014's reason for two hook IDs was this same problem, and .pre-commit-
# hooks.yaml still carries both IDs after the merge for that reason.
# config.yaml still carries both IDs after the merge for that reason.
# One representative path per file shape the prefilter is supposed to cover,
# tagged with the `.vale.ini` section that is supposed to cover it and with
@@ -1365,8 +1366,15 @@ fi
#
# `demo.md` (bare, no `.agent.md` suffix) exercises `[**/agents/*.md]` in
# isolation, not because any current `.apm/agents/*` file has that shape -- per
# ADR-0016 they are all `*.agent.md`. `.pre-commit-hooks.yaml`'s agent regex
# still covers the bare shape, which is what keeps the row honest.
# ADR-0016 they are all `*.agent.md`. factory-audit's agent flow can still be
# handed that shape in a consuming repo, which is what keeps the row honest.
#
# These rows describe what factory-audit's own Vale call can be handed at
# runtime in any repo, not what this repo's hooks select: most of them sit
# outside `.pre-commit-config.yaml`'s `^plugins/`-anchored `files:` regexes on
# purpose. The hook-scope half that once held each row to a hook regex read the
# published `.pre-commit-hooks.yaml`, which is retired (ADR-0014, 2026-09-16
# amendment); case 32 owns the local hooks' scope.
PROBE_TABLE28="$(cat <<'EOF_PROBE28'
plugins/demo/.apm/skills/demo/SKILL.md|[**/SKILL.md]|isolating
.claude/skills/demo/SKILL.md|[**/SKILL.md]|isolating
@@ -1428,12 +1436,11 @@ files_scanned28() {
| tail -1
}
# Prints `<id>|<files regex>` for every hook in the given pre-commit manifest
# whose entry is factory-audit's vale-wrap.sh -- either manifest, since the two
# carry the same two hooks in the same shape. Records are delimited by their
# Prints `<id>|<files regex>` for every hook in the given pre-commit config
# whose entry is factory-audit's vale-wrap.sh. Records are delimited by their
# `- id:` line, so this does not depend on `entry:` preceding `files:` within a
# record. Case 28 wants the regexes alone and case 32 needs to know which hook
# each belongs to, so the id is carried here and dropped by the wrapper below.
# record. Case 32 needs to know which hook each regex belongs to, so the id is
# carried here.
hook_records28() {
local manifest="$1" id raw
[[ -f "$manifest" ]] || return 0
@@ -1456,29 +1463,8 @@ hook_records28() {
done
}
# Prints the `files:` regex of every hook in the given manifest whose entry is
# factory-audit's vale-wrap.sh.
hook_file_regexes28() {
hook_records28 "$1" | cut -d'|' -f2-
}
matches_any_regex28() {
local rel="$1" regexes="$2" re
[[ -n "$regexes" ]] || return 1
while IFS= read -r re; do
[[ -n "$re" ]] || continue
if grep -Eq "$re" <<< "$rel"; then
return 0
fi
done <<EOF_RE28
$regexes
EOF_RE28
return 1
}
TREE28="$(build_probe_tree28)"
new_fixture "$TREE28"
HOOK_REGEXES28="$(hook_file_regexes28 "$REPO_ROOT/.pre-commit-hooks.yaml")"
echo ""
echo "--- every .vale.ini glob section actually scans a real file shape ---"
@@ -1526,24 +1512,13 @@ fi
# One `PASS|<rel>|<message>` or `FAIL|<rel>|<message>` line per probe row, for
# the config at $1. A function rather than an inline loop so Part B can hold a
# mutated copy to this exact logic -- a second, "equivalent" loop for the
# fixture would prove nothing about the live check. With $2 = false only the
# hook-scope half runs: that half reads .pre-commit-hooks.yaml, not vale, so a
# machine without vale still gets it.
# fixture would prove nothing about the live check. With $2 = false it prints
# nothing: every remaining half of it needs vale.
probe_coverage28() {
local cfg="$1" with_vale="$2" rel sec report count
[[ "$with_vale" == true ]] || return 0
while IFS='|' read -r rel sec _; do
[[ -n "$rel" ]] || continue
if ! matches_any_regex28 "$rel" "$HOOK_REGEXES28"; then
# Original wording: the probe path is stale, or the hook was rescoped away
# from a shape it still needs to lint. Either way the row below stops
# describing anything the push gate actually hands to vale.
echo "FAIL|$rel|$rel matches no 'files:' regex of any factory-audit vale hook in .pre-commit-hooks.yaml — the probe path is stale, or the hook was rescoped away from a shape it still needs to lint"
continue
fi
if [[ "$with_vale" != true ]]; then
echo "PASS|$rel|$rel is in scope of a published factory-audit vale hook (glob coverage not checked: Vale-dependent half held back)"
continue
fi
report="$(vale_report28 "$cfg" "$TREE28" "$rel")"
count="$(files_scanned28 "$report")"
if [[ -z "$count" ]]; then
@@ -1995,25 +1970,29 @@ fi
# which worked only because the two skills gave the two hooks two distinct entry
# paths. After the merge both hooks share one `entry:`, so that selector can no
# longer tell them apart and a faithful port would have to key on hook `id:`
# instead. Case 33 is that port; this case covers the separate question of
# whether each local hook selects a live corpus at all. The zero-match half of the hole stands on its own, and nothing
# else in the repo covers it: tests/test-vale-hooks-consumer.sh synthesises its
# own consumer config out of `.pre-commit-hooks.yaml` and never reads the local
# one, and cases 28-30 read `.pre-commit-hooks.yaml` too. This repo's OWN
# prefilter regexes -- `.pre-commit-config.yaml`'s vale-audit-prefilter-skill and
# vale-audit-prefilter-agent -- are therefore asserted by no test at all. Narrow
# either one to match zero files and every gate still passes: pre-commit does not
# instead. That port was case 33, deleted with the published
# `.pre-commit-hooks.yaml` it compared against (ADR-0014, 2026-09-16
# amendment); its one guard that did not depend on the second manifest -- a
# local regex narrowed to a single plugin -- is property 3 below. Nothing else
# in the repo asserts this repo's OWN prefilter regexes --
# `.pre-commit-config.yaml`'s vale-audit-prefilter-skill and
# vale-audit-prefilter-agent. Narrow either one to match zero files and every gate still passes: pre-commit does not
# error on a hook that matches nothing, it simply never runs it. That is the same
# silent-zero failure mode case 28 guards on the vale side of this pipeline, one
# layer up -- there the glob scans 0 files and exits 0, here the hook is handed 0
# files and never starts.
#
# Two properties, because matching SOMETHING is not the same as matching the
# Three properties, because matching SOMETHING is not the same as matching the
# right thing: a regex loosened to `^plugins/` would match hundreds of files and
# clear a bare non-emptiness check while handing vale a corpus it has no glob
# for. So each hook must also select only its own artifact class -- ADR-0014's
# reason for two hook IDs, carried across the merge by ADR-0025's comment in the
# config, is precisely that the two scopes stay independently addressable.
# And each hook must select ALL of its class's authoring source: a regex
# narrowed from `^plugins/[^/]+/...` to `^plugins/kyberforge/...` still matches
# tracked files, all of the right class, while silently dropping every other
# plugin out of the prefilter. Measured before any case caught it: that exact
# narrowing left 6 of 38 skills prefiltered and the whole suite green.
echo ""
echo "--- each .pre-commit-config.yaml vale prefilter hook matches a real, correctly-classed file ---"
@@ -2031,7 +2010,7 @@ REPO_FILES32="$(cd "$REPO_ROOT" && git ls-files)"
# the live check.
prefilter_scope_failures32() {
local config="$1" files="$2"
local records id re class matched count offenders offending m
local records id re class matched count offenders offending m corpus missing nmissing
local seen_skill=false seen_agent=false bad=""
records="$(hook_records28 "$config")"
if [[ -z "$records" ]]; then
@@ -2082,6 +2061,19 @@ EOF_MATCHED32
if [[ "$offending" -gt 0 ]]; then
bad+="[$id: 'files: $re' selects $count file(s), $offending of them outside the $class artifact class, so the two prefilter scopes are no longer independently addressable and vale is handed files no glob in its config covers — first: $offenders] "
fi
# The class's full authoring-source corpus, per the layout AGENTS.md fixes
# (`plugins/<name>/.apm/` is the only authoring source). Every path in it
# must be selected, or part of the corpus is silently unprefiltered.
if [[ "$class" == skill ]]; then
corpus="$(printf '%s\n' "$files" | { grep -E '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' || true; })"
else
corpus="$(printf '%s\n' "$files" | { grep -E '^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$' || true; })"
fi
missing="$(comm -23 <(printf '%s\n' "$corpus" | grep . | sort) <(printf '%s\n' "$matched" | grep . | sort) || true)"
nmissing="$(printf '%s\n' "$missing" | grep -c . || true)"
if [[ "$nmissing" -gt 0 ]]; then
bad+="[$id: 'files: $re' misses $nmissing tracked $class file(s) under plugins/*/.apm/, so part of the corpus is never prefiltered while every gate still reports a pass — first: $(printf '%s\n' "$missing" | head -3 | tr '\n' ' ')] "
fi
done <<EOF_RECORDS32
$records
EOF_RECORDS32
@@ -2129,205 +2121,35 @@ else
pass "narrowing either hook's 'files:' regex to match zero files is caught by Part A, which is what makes its pass mean something"
fi
# --- 33. The local and published vale hooks agree on every shared file shape --
#
# The cross-manifest `files:` agreement check of the deleted
# scripts/check-vale-style-sync.sh (ADR-0025), ported. Case 32 does not cover
# it: a local regex narrowed from `^plugins/[^/]+/...` to
# `^plugins/kyberforge/...` still matches tracked files, all of them SKILL.md,
# so it clears both of 32's properties while silently dropping every other
# plugin's skills out of this repo's prefilter. Measured before this case
# existed: that exact narrowing left the whole suite green.
#
# The original's comment on why the two manifests are compared per hook rather
# than unioned, verbatim in substance: "A union here previously let a probe that
# matched only the older, looser .pre-commit-hooks.yaml pattern read as 'in
# scope' even after .pre-commit-config.yaml's copy of the same hook had been
# narrowed away from it -- silently masking exactly the kind of hook-rescoping
# drift this script exists to catch."
#
# What changed in the port is the selector and nothing else. The original found
# each manifest's hook by `entry ~ skill "/scripts/vale-wrap.sh"`, which told the
# two hooks apart only because two skills gave them two entry paths; after the
# merge both hooks share one entry. They are paired by `id:` instead, from an
# explicit table. The pairing is explicit rather than inferred from an id suffix
# so that renaming either id fails here by name instead of quietly dropping a
# class out of the comparison. The probe table and its shared/hooks-only scopes
# are the original's rows, unchanged:
#
# shared -- a shape this repo's own layout has, so both manifests must
# agree on it. This is what catches the narrowing above.
# hooks-only -- a shape only the layout-agnostic published manifest has to
# cover. `.pre-commit-config.yaml` pinning this repo's own
# `plugins/*/.apm/` layout is by design, not drift; bare
# `agents/demo.md` is hooks-only because per ADR-0016 every
# `.apm/agents/` file is `*.agent.md`.
#
# A probe in scope of neither manifest fails too, exactly as in the original:
# the probe path is stale, or both hooks were rescoped away from it.
HOOK_PAIRS33="$(cat <<'EOF_PAIRS33'
skill|kyberforge-vale-audit-skill|vale-audit-prefilter-skill
agent|kyberforge-vale-audit-agent|vale-audit-prefilter-agent
EOF_PAIRS33
)"
PROBES33="$(cat <<'EOF_PROBES33'
skill|plugins/demo/.apm/skills/demo/SKILL.md|shared
skill|.claude/skills/demo/SKILL.md|hooks-only
agent|plugins/demo/.apm/agents/demo.md|hooks-only
agent|plugins/demo/.apm/agents/demo.agent.md|shared
agent|.claude/agents/demo.md|hooks-only
agent|copilot/demo.agent.md|hooks-only
EOF_PROBES33
)"
# Prints the `files:` regex of the hook whose id is exactly $2 in manifest $1,
# or nothing. Records are delimited by their `- id:` line, as in
# hook_records28, but selected by id alone: after the merge `entry:` no longer
# distinguishes them.
hook_regex_by_id33() {
local manifest="$1" want="$2" raw
[[ -f "$manifest" ]] || return 0
raw="$(WANT="$want" awk '
function flush() {
if (id == ENVIRON["WANT"] && files != "") print files
id = ""; files = ""
}
/^[ \t]*-[ \t]*id:/ { flush(); id = $0; sub(/^[ \t]*-[ \t]*id:[ \t]*/, "", id); sub(/[ \t]+$/, "", id) }
/^[ \t]*files:/ { files = $0; sub(/^[ \t]*files:[ \t]*/, "", files) }
END { flush() }
' "$manifest" | head -1)"
raw="${raw%\'}"; raw="${raw#\'}"
raw="${raw%\"}"; raw="${raw#\"}"
printf '%s' "$raw"
}
# Prints one failure token per defect for published manifest $1 against local
# manifest $2; empty output means every probe is in scope of at least one of its
# class's hooks and every shared probe is in scope of both. Manifest paths are
# arguments so Part B runs this exact function against mutated copies.
cross_manifest_failures33() {
local published="$1" local_cfg="$2"
local class pub_id loc_id rel scope pub_re loc_re in_hooks in_config
local bad="" checked=0 shared_classes="" missing_classes=""
while IFS='|' read -r class pub_id loc_id; do
[[ -n "$class" ]] || continue
if [[ -z "$(hook_regex_by_id33 "$published" "$pub_id")" ]]; then
bad+="[no hook with id '$pub_id' and a files: regex in ${published##*/}, so the $class class is compared against nothing] "
missing_classes+="$class "
fi
if [[ -z "$(hook_regex_by_id33 "$local_cfg" "$loc_id")" ]]; then
bad+="[no hook with id '$loc_id' and a files: regex in ${local_cfg##*/}, so the $class class is compared against nothing] "
missing_classes+="$class "
fi
done <<EOF_PAIRCHK33
$HOOK_PAIRS33
EOF_PAIRCHK33
while IFS='|' read -r class rel scope; do
[[ -n "$class" ]] || continue
pub_id="$(printf '%s\n' "$HOOK_PAIRS33" | awk -F'|' -v c="$class" '$1 == c { print $2 }')"
loc_id="$(printf '%s\n' "$HOOK_PAIRS33" | awk -F'|' -v c="$class" '$1 == c { print $3 }')"
if [[ -z "$pub_id" || -z "$loc_id" ]]; then
bad+="[probe $rel names class '$class', which has no row in the hook pair table] "
continue
fi
pub_re="$(hook_regex_by_id33 "$published" "$pub_id")"
loc_re="$(hook_regex_by_id33 "$local_cfg" "$loc_id")"
# A missing hook was reported above; comparing against it would add a
# second message for the same defect.
[[ -n "$pub_re" && -n "$loc_re" ]] || continue
checked=$((checked + 1))
[[ "$scope" != shared ]] || shared_classes+="$class "
in_hooks=false
matches_any_regex28 "$rel" "$pub_re" && in_hooks=true
in_config=false
matches_any_regex28 "$rel" "$loc_re" && in_config=true
if [[ "$in_hooks" == false && "$in_config" == false ]]; then
bad+="[$rel matches neither $pub_id's nor $loc_id's 'files:' regex — the probe path is stale, or the hook was rescoped away from a shape it still needs to lint] "
elif [[ "$scope" == shared && "$in_hooks" != "$in_config" ]]; then
bad+="[$rel is in scope of $pub_id in ${published##*/} (hooks=$in_hooks) but not of $loc_id in ${local_cfg##*/} (config=$in_config), or vice versa — the local and published 'files:' regexes have drifted, and the narrower one silently stops prefiltering that shape] "
fi
done <<EOF_PROBECHK33
$PROBES33
EOF_PROBECHK33
# The original's PROBES_CHECKED floor, plus one it lacked: a class with no
# shared probe is never compared across manifests at all.
if [[ "$checked" -eq 0 && -z "$missing_classes" ]]; then
bad+="[no probe row was checked, so the agreement check verified nothing] "
fi
while IFS='|' read -r class _ _; do
[[ -n "$class" ]] || continue
# A class whose hook is missing was already reported, and its probes were
# skipped for that reason, not for want of a shared row.
[[ " $missing_classes" != *" $class "* ]] || continue
[[ " $shared_classes" == *" $class "* ]] \
|| bad+="[the $class class has no shared probe row, so its two hooks are never compared] "
done <<EOF_CLASSCHK33
$HOOK_PAIRS33
EOF_CLASSCHK33
printf '%s' "$bad"
}
echo ""
echo "--- the local and published vale hooks agree on every shared file shape ---"
PUBLISHED33="$REPO_ROOT/.pre-commit-hooks.yaml"
# Part A: the live assertion, against the two real manifests.
AGREE_FAILS33="$(cross_manifest_failures33 "$PUBLISHED33" "$PC_CONFIG32")"
if [[ -n "$AGREE_FAILS33" ]]; then
fail ".pre-commit-config.yaml and .pre-commit-hooks.yaml disagree on the vale hooks' scope: $AGREE_FAILS33"
else
pass "every probe is in scope of its class's hooks, and both manifests agree on every shared shape"
fi
# Part B: proof that Part A can fail. Each mutation lands in a COPY of
# .pre-commit-config.yaml, never the real file.
#
# skill -- the exact narrowing that slipped through before this case existed.
# agent -- the same narrowing applied to the agent hook.
# id -- the local skill hook renamed, which must fail by name rather than
# drop the skill class out of the comparison.
#
# Replacement is fixed-string, via ENVIRON, so the regex's backslashes reach awk
# literally; `awk -v` would process them as escape sequences.
narrow_config33() {
OLD33="$2" NEW33="$3" awk '
$0 ~ ENVIRON["LINE33"] {
i = index($0, ENVIRON["OLD33"])
if (i) $0 = substr($0, 1, i - 1) ENVIRON["NEW33"] substr($0, i + length(ENVIRON["OLD33"]))
# Part C: proof that property 3 can fail. Each hook's regex is narrowed to one
# plugin in a COPY of the config -- the exact narrowing that once slipped
# through. Replacement is fixed-string, via ENVIRON, so the regex's backslashes
# reach awk literally; `awk -v` would process them as escape sequences.
narrow_config32() {
OLD32="$2" NEW32="$3" awk '
/^[ \t]*files:/ {
i = index($0, ENVIRON["OLD32"])
if (i) $0 = substr($0, 1, i - 1) ENVIRON["NEW32"] substr($0, i + length(ENVIRON["OLD32"]))
}
{ print }
' "$1"
}
MUT33="$(mktemp -d)"
new_fixture "$MUT33"
LINE33='^[ \t]*files:' narrow_config33 "$PC_CONFIG32" \
'^plugins/[^/]+/\.apm/skills/' '^plugins/kyberforge/\.apm/skills/' > "$MUT33/skill.yaml"
LINE33='^[ \t]*files:' narrow_config33 "$PC_CONFIG32" \
'^plugins/[^/]+/\.apm/agents/' '^plugins/kyberforge/\.apm/agents/' > "$MUT33/agent.yaml"
LINE33='^[ \t]*-[ \t]*id:' narrow_config33 "$PC_CONFIG32" \
'vale-audit-prefilter-skill' 'vale-audit-prefilter-skill-renamed' > "$MUT33/id.yaml"
SKILL_FAILS33="$(cross_manifest_failures33 "$PUBLISHED33" "$MUT33/skill.yaml")"
AGENT_FAILS33="$(cross_manifest_failures33 "$PUBLISHED33" "$MUT33/agent.yaml")"
ID_FAILS33="$(cross_manifest_failures33 "$PUBLISHED33" "$MUT33/id.yaml")"
if ! grep -qF '^plugins/kyberforge/\.apm/skills/' "$MUT33/skill.yaml" \
|| ! grep -qF '^plugins/kyberforge/\.apm/agents/' "$MUT33/agent.yaml" \
|| ! grep -qF 'vale-audit-prefilter-skill-renamed' "$MUT33/id.yaml"; then
fail "a mutation never reached its copied config, so Part B mutated nothing and proves nothing about Part A"
elif ! grep -qF "[plugins/demo/.apm/skills/demo/SKILL.md is in scope of kyberforge-vale-audit-skill" <<< "$SKILL_FAILS33"; then
fail "narrowing the local skill hook to ^plugins/kyberforge/ did not fail Part A, so the prefilter can drop every other plugin's skills with every gate green: ${SKILL_FAILS33:-<no failure>}"
elif [[ -z "$AGREE_FAILS33" ]] && grep -qF "kyberforge-vale-audit-agent" <<< "$SKILL_FAILS33"; then
# Only meaningful against a clean base: when Part A already failed, the copy
# inherits that defect, and reporting it again here would be one defect twice.
fail "narrowing only the skill hook also reported an agent-class defect, so the comparison is not confined to its class: $SKILL_FAILS33"
elif ! grep -qF "[plugins/demo/.apm/agents/demo.agent.md is in scope of kyberforge-vale-audit-agent" <<< "$AGENT_FAILS33"; then
fail "narrowing the local agent hook to ^plugins/kyberforge/ did not fail Part A: ${AGENT_FAILS33:-<no failure>}"
elif ! grep -qF "[no hook with id 'vale-audit-prefilter-skill' and a files: regex in id.yaml" <<< "$ID_FAILS33"; then
fail "renaming the local skill hook's id did not fail Part A by name, so the skill class could fall out of the comparison silently: ${ID_FAILS33:-<no failure>}"
narrow_config32 "$PC_CONFIG32" \
'^plugins/[^/]+/\.apm/skills/' '^plugins/kyberforge/\.apm/skills/' > "$MUT32/skill.yaml"
narrow_config32 "$PC_CONFIG32" \
'^plugins/[^/]+/\.apm/agents/' '^plugins/kyberforge/\.apm/agents/' > "$MUT32/agent.yaml"
NARROW_SKILL32="$(prefilter_scope_failures32 "$MUT32/skill.yaml" "$REPO_FILES32")"
NARROW_AGENT32="$(prefilter_scope_failures32 "$MUT32/agent.yaml" "$REPO_FILES32")"
if cmp -s "$PC_CONFIG32" "$MUT32/skill.yaml" || cmp -s "$PC_CONFIG32" "$MUT32/agent.yaml"; then
fail "a one-plugin narrowing left the copied config unchanged, so Part C narrowed nothing and proves nothing about property 3"
elif ! grep -qF "vale-audit-prefilter-skill: 'files: ^plugins/kyberforge/" <<< "$NARROW_SKILL32" \
|| ! grep -qF "tracked skill file(s)" <<< "$NARROW_SKILL32"; then
fail "narrowing the skill hook to ^plugins/kyberforge/ did not fail Part A as an incomplete corpus: ${NARROW_SKILL32:-<no failure>}"
elif ! grep -qF "vale-audit-prefilter-agent: 'files: ^plugins/kyberforge/" <<< "$NARROW_AGENT32" \
|| ! grep -qF "tracked agent file(s)" <<< "$NARROW_AGENT32"; then
fail "narrowing the agent hook to ^plugins/kyberforge/ did not fail Part A as an incomplete corpus: ${NARROW_AGENT32:-<no failure>}"
else
pass "narrowing either local hook to one plugin, or renaming one, is caught by Part A"
pass "narrowing either hook's 'files:' regex to one plugin is caught by Part A as an incomplete corpus"
fi
# --- 34. Every glob section loads a real style, asserted without vale --------
@@ -2342,7 +2164,7 @@ fi
# file the section matches with NO rule, prints `0 errors ... in 1 file` and
# exits 0, and every gate in this repo reads that as a pass. That is the
# silent-pass class ADR-0013 exists to prevent, and `--strict` -- which a
# consumer's clone does not run -- is the only thing standing in front of it.
# consuming repo's install does not run -- is the only thing standing in front of it.
#
# So this case asks the same question of the config TEXT, with no dependency on
# vale being installed. It is a separate case rather than an addition to either
@@ -2354,9 +2176,9 @@ fi
# `[[ "$sp" == /* ]] || sp="$dir/$sp"`, which ACCEPTS an absolute StylesPath --
# a path that resolves on the machine that wrote it and on no other. Relative
# resolution against the config's own directory is the only reason the bundled
# styles are found under a CONSUMING repo's clone prefix, so an absolute one
# passes every check here and hard-fails every external consumer of
# .pre-commit-hooks.yaml. It is asserted here, not in case 0, to keep case 0's
# styles are found under a CONSUMING repo's install of factory-audit, so an
# absolute one passes every check here and hard-fails every repo that installs
# the plugin. It is asserted here, not in case 0, to keep case 0's
# scope the one its comment describes.
VALE_ASSETS34="$FACTORY_AUDIT/assets/vale"
@@ -2383,7 +2205,7 @@ section_styles34() {
# One failure token per defect in the config at $1; empty output means every
# section would load at least one real style for a consumer. The section floor
# is here for the same reason case 28 Part A and case 33 carry theirs: a config
# is here for the same reason case 28 Part A carries its own: a config
# whose sections were all deleted lints nothing at all, and without a floor this
# function would report it clean.
style_load_defects34() {
@@ -2402,7 +2224,7 @@ style_load_defects34() {
return 0
fi
if [[ "$sp" == /* ]]; then
printf '%s' "[${cfg##*/} sets the ABSOLUTE StylesPath '$sp'; it resolves only on the machine that wrote it, and a consuming repo's clone -- which is the only reason .pre-commit-hooks.yaml ships these styles -- gets 'path does not exist'] "
printf '%s' "[${cfg##*/} sets the ABSOLUTE StylesPath '$sp'; it resolves only on the machine that wrote it, and a consuming repo's install of factory-audit -- which is the only reason these styles ship -- gets 'path does not exist'] "
return 0
fi
if [[ ! -d "$dir/$sp" ]]; then
@@ -2490,7 +2312,7 @@ EOF_MUT34
ABS34="$(mktemp -d)"
new_fixture "$ABS34"
cp -r "$VALE_ASSETS34/." "$ABS34/"
# Written through ENVIRON, as in narrow_config33, so the path reaches awk
# Written through ENVIRON, as in narrow_config32, so the path reaches awk
# literally rather than through `-v`'s escape processing.
ABS_SP34="$ABS34/styles" awk '
/^[ \t]*StylesPath[ \t]*=/ { print "StylesPath = " ENVIRON["ABS_SP34"]; next }