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:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user