refactor(gates): source the boundary resolver into skill-size-check

Why: scripts/skill-size-check.sh embedded a byte-identical 1,061-line copy
of the ADR-0020 boundary resolver only because it was also exported
through .pre-commit-hooks.yaml, whose consumers could not reach a file
inside the plugin. 4de5b6b retired that export, so the hook now runs only
in this repo and can source factory-audit's lib-boundary-resolver.sh like
validate.sh does. One copy removes the edit-one-paste-the-other hazard.

Implementation Notes:
- The hook's Python program is assembled from its own preamble, the
  library's resolver and its own checks, read from quoted here-docs. The
  assembled program matches the old one line for line except one comment,
  and the hook's stdout, stderr and exit code are identical over every
  corpus SKILL.md and the 26 differential-suite fixtures.
- The hook fails closed, naming the library, when it is missing or
  defines no resolver.
- test-adr0020-contract.sh assertion 1 now pins the single copy: one
  marker pair in the library, none in the hook, fail-closed on a missing
  or gutted library, and a sentinel planted in a copied library that must
  appear in the hook's output. 1a expects exactly one authority. 27 -> 29
  passes.
- ADR-0020 and ADR-0025 carry dated amendments; gates.md and the
  library, hook and mode-library comments no longer describe two copies.
- factory-audit is new on this branch, so the version-bump gate exempts
  it; kyberforge is already at 2.0.0 against main's 1.6.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 13:25:16 +00:00
parent adaa978d20
commit ef27c9751a
9 changed files with 239 additions and 1196 deletions

View File

@@ -27,6 +27,14 @@ applies the body tiers on the skill path only. The two copies must still stay by
plugin script cannot source the root one, which is why a second copy exists at all. Read every plugin script cannot source the root one, which is why a second copy exists at all. Read every
"three" below as the count at the time of writing. "three" below as the count at the time of writing.
**Amended again (2026-09-16): one copy.** `scripts/skill-size-check.sh` no longer embeds the
resolver. It sources `factory-audit/scripts/lib-boundary-resolver.sh` by path and fails closed if the
library is missing or defines no resolver. The embedded copy had been kept only because the hook was
also exported through `.pre-commit-hooks.yaml`, whose consumers could not reach a file inside the
plugin; `4de5b6b` retired that export (ADR-0014), so the hook runs only inside this repo. The
"byte-identical" sentence above is superseded: there is nothing left to keep identical, and
`tests/test-adr0020-contract.sh` assertion 1 now pins the single copy instead of hashing a pair.
## Context ## Context
Every `file:line` citation in this ADR is against the base commit the decision was taken on, Every `file:line` citation in this ADR is against the base commit the decision was taken on,

View File

@@ -10,6 +10,13 @@ below (both exported hook IDs survive) and the case 33 port no longer describe t
deleted, and its one-plugin narrowing guard is now a property of case 32. See deleted, and its one-plugin narrowing guard is now a property of case 32. See
[ADR-0014's amendment](0014-vale-prefilter-ships-from-the-plugin.md#amendment-2026-09-16-the-external-hook-contract-is-retired). [ADR-0014's amendment](0014-vale-prefilter-ships-from-the-plugin.md#amendment-2026-09-16-the-external-hook-contract-is-retired).
**Amended (2026-09-16): the root hook sources the resolver.** Point 6 below records sourcing the
resolver into `scripts/skill-size-check.sh` as refuted, because the hook was consumed through
`.pre-commit-hooks.yaml`. That manifest is retired (above), so the reason no longer holds: the hook now
sources `lib-boundary-resolver.sh`, the repo holds one resolver copy, and the contract test's
assertion 1 pins that copy rather than hashing two. Assertion 1a's "exactly those two files" is now
exactly one. Point 6 is left as the record of the decision at the time.
## Context ## Context
Every figure below was measured against the worktree on 2026-09-15. Re-derive rather than quote; the Every figure below was measured against the worktree on 2026-09-15. Re-derive rather than quote; the

View File

@@ -410,14 +410,17 @@ skills. They are copied rather than imported because a cache-installed plugin's
files outside their own plugin directory. `tests/test-skill-size-check.sh` asserts the copies agree, files outside their own plugin directory. `tests/test-skill-size-check.sh` asserts the copies agree,
so drift fails CI rather than silently letting an audit bless a skill the commit hook then rejects. so drift fails CI rather than silently letting an audit bless a skill the commit hook then rejects.
**The shared boundary resolver is now two copies, not three** (ADR-0025). `scripts/skill-size-check.sh` **The shared boundary resolver is one copy** (ADR-0025, then 2026-09-16). It lives between the
still carries it embedded between `BEGIN`/`END ADR-0020 SHARED BOUNDARY RESOLVER` markers; the two `BEGIN`/`END ADR-0020 SHARED BOUNDARY RESOLVER` markers in `factory-audit/scripts/lib-boundary-resolver.sh`.
plugin copies that used to sit inside `skill-audit`'s and `agent-audit`'s `validate.sh` collapsed ADR-0025 collapsed the two copies inside `skill-audit`'s and `agent-audit`'s `validate.sh` into that
into the single `factory-audit/scripts/lib-boundary-resolver.sh`, sourced by that skill's scripts. file. `scripts/skill-size-check.sh` kept an embedded, byte-identical third copy while it was also
The two remaining copies must still stay byte-identical — a plugin script cannot source the root exported through `.pre-commit-hooks.yaml`, whose consumers could not reach a file inside the plugin.
one, which is the constraint that forces a copy to exist at all. `4de5b6b` retired that export (ADR-0014), so the hook now sources the library by path and fails closed
if the library is missing or defines no resolver.
`tests/test-adr0020-contract.sh` pins that arrangement, and one of its assertions was green on a `tests/test-adr0020-contract.sh` pins that arrangement: the library carries the only marker pair,
the hook carries none, the hook fails closed without the library, and a sentinel planted in a copied
library proves the hook executes the library's text. One of its assertions was green on a
defect it named. "`validate.sh` sources the resolver in **both mode branches**" was implemented as a defect it named. "`validate.sh` sources the resolver in **both mode branches**" was implemented as a
file-wide `grep -Ec … -ge 2`, which cannot see a branch at all: delete the `agent)` arm's source line file-wide `grep -Ec … -ge 2`, which cannot see a branch at all: delete the `agent)` arm's source line
and duplicate the `skill)` arm's, and the file-wide count is still 2 and the assertion still passes, and duplicate the `skill)` arm's, and the file-wide count is still 2 and the assertion still passes,
@@ -425,7 +428,8 @@ with the agent path running no resolver or some other one. It is now a **per-arm
each arm of `validate.sh`'s `case "$MODE" in` block must carry exactly one `source` line inside its each arm of `validate.sh`'s `case "$MODE" in` block must carry exactly one `source` line inside its
own body, and the file must carry exactly those two — with a mutation self-test that performs that own body, and the file must carry exactly those two — with a mutation self-test that performs that
exact count-preserving edit on a copy and requires the check to fail on it. The suite went 25 → 28 exact count-preserving edit on a copy and requires the check to fail on it. The suite went 25 → 28
cases. cases. It stood at 27 before the 2026-09-16 change and 29 after it, which replaced the two-copy
hash and its line-count floor with the six one-copy assertions above.
### `python3` and PyYAML are hard requirements ### `python3` and PyYAML are hard requirements

View File

@@ -1,19 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# lib-boundary-resolver.sh — SOURCED, never executed. # lib-boundary-resolver.sh — SOURCED, never executed.
# #
# The ADR-0020 shared boundary resolver, as ONE copy for this skill. Both of # The ADR-0020 shared boundary resolver — the ONE copy in the repo. Both of
# validate.sh's modes compose it into the Python program they run, so the # validate.sh's modes compose it into the Python program they run, and so does
# skill-mode and agent-mode check suites resolve boundary targets through the # the repo-root hook scripts/skill-size-check.sh, so the audit and the commit
# same code rather than through two copies that can drift apart. # hook resolve boundary targets through the same code rather than through
# copies that can drift apart.
# #
# The resolver is Python, and bash cannot source Python, so the block is held # The resolver is Python, and bash cannot source Python, so the block is held
# in a shell variable filled from a QUOTED here-doc: nothing inside it is # in a shell variable filled from a QUOTED here-doc: nothing inside it is
# expanded, substituted or rewritten, and the text between the two markers # expanded, substituted or rewritten, and every consumer runs exactly the text
# below is therefore byte-identical to the copy in scripts/skill-size-check.sh # between the two markers below. The markers stay on lines of their own, at
# that tests/test-adr0020-contract.sh hashes. The markers stay on lines of # column 0, exactly once each: tests/test-adr0020-contract.sh extracts the span
# their own, at column 0, exactly once each, so `sed -n '/^BEGIN$/,/^END$/p'` # with `sed -n '/^BEGIN$/,/^END$/p'`, and asserts no other file carries them.
# extracts the same span here as it does from the scripts the test already
# reads. Edit one copy, then paste it over the others.
# #
# The here-doc is consumed by the `read` BUILTIN rather than by `$(cat <<...)`. # The here-doc is consumed by the `read` BUILTIN rather than by `$(cat <<...)`.
# This file is sourced by validate.sh before the mode-specific python3/PyYAML # This file is sourced by validate.sh before the mode-specific python3/PyYAML
@@ -26,7 +25,7 @@
# exactly ONE newline, never a run: blank lines at the end of a chunk are part # exactly ONE newline, never a run: blank lines at the end of a chunk are part
# of the program text the entry script reassembles, and stripping every # of the program text the entry script reassembles, and stripping every
# trailing newline deleted them. The here-doc itself is unchanged: still # trailing newline deleted them. The here-doc itself is unchanged: still
# QUOTED, still byte-identical between its markers. # QUOTED, still verbatim between its markers.
# #
# Self-containment (agentskills.io, skill-author/references/deployment-modes.md) # Self-containment (agentskills.io, skill-author/references/deployment-modes.md)
# binds BETWEEN skills, not within one: a cache-installed plugin copies each # binds BETWEEN skills, not within one: a cache-installed plugin copies each
@@ -34,21 +33,22 @@
# travels with the skill and is always readable. That is why this is sourced # travels with the skill and is always readable. That is why this is sourced
# here and duplicated across skill boundaries elsewhere. # here and duplicated across skill boundaries elsewhere.
# #
# Consumed by: validate.sh (both modes), via $KYBERFORGE_RESOLVER_PY. # Consumed by: validate.sh (both modes) and scripts/skill-size-check.sh, via
# $KYBERFORGE_RESOLVER_PY. The root hook reaches into this plugin by path, which
# is safe only because it runs solely inside this repo — 4de5b6b retired the
# published hook manifest that once made it run elsewhere (ADR-0014).
# shellcheck shell=bash # shellcheck shell=bash
# shellcheck disable=SC2034 # shellcheck disable=SC2034
IFS='' read -r -d '' KYBERFORGE_RESOLVER_PY <<'KYBERFORGE_ADR0020_RESOLVER_PY' || true IFS='' read -r -d '' KYBERFORGE_RESOLVER_PY <<'KYBERFORGE_ADR0020_RESOLVER_PY' || true
# ===== BEGIN ADR-0020 SHARED BOUNDARY RESOLVER ===== # ===== BEGIN ADR-0020 SHARED BOUNDARY RESOLVER =====
# ONE resolver, embedded VERBATIM in two scripts (ADR-0025 retired the third): # ONE resolver, and this is its only copy. Sourced from this file by:
# scripts/skill-size-check.sh # plugins/kyberforge/.apm/skills/factory-audit/scripts/validate.sh (both modes)
# plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-boundary-resolver.sh # scripts/skill-size-check.sh (the repo-root commit hook)
# The block between these markers must stay byte-identical in both. It is copied # ADR-0025 retired the copies in the two pre-merge audit skills, and the
# rather than imported because a cache-installed plugin's scripts cannot read # 2026-09-16 change retired the copy embedded in the root hook, which had been
# files outside their own plugin directory, and this repo-root hook is kept fit for # kept only while that hook was also exported through a published hook manifest
# a published hook manifest (retired; ADR-0014), where only entry[0] is rewritten -- # (retired by 4de5b6b; ADR-0014). Edit it here; there is nothing to paste over.
# so no single file is reachable by both (the same constraint that duplicates the
# ADR-0020 constants). Edit one copy, then paste it over the other.
# #
# Requires: glob, os, re, yaml (imported by the host script; PyYAML is a hard # Requires: glob, os, re, yaml (imported by the host script; PyYAML is a hard
# dependency, preflighted in bash before the interpreter starts). # dependency, preflighted in bash before the interpreter starts).

View File

@@ -69,9 +69,9 @@ import yaml
# resolver block below pins the reads; this pins the writes. # resolver block below pins the reads; this pins the writes.
# #
# Deliberately OUTSIDE the ADR-0020 shared boundary resolver block: the two # Deliberately OUTSIDE the ADR-0020 shared boundary resolver block: the two
# validate.sh copies print findings, skill-size-check.sh has its own top-level # validate.sh modes print findings, skill-size-check.sh has its own top-level
# equivalent, and tests/test-adr0020-contract.sh hashes that block for # equivalent, and the block is one sourced copy all three share, so each
# byte-identity across all three. # consumer's own startup stays in its own preamble.
for _stream in (sys.stdout, sys.stderr): for _stream in (sys.stdout, sys.stderr):
try: try:
_stream.reconfigure(encoding='utf-8') _stream.reconfigure(encoding='utf-8')
@@ -145,9 +145,11 @@ COPILOT_BODY_LIMIT = 30000
# every session exactly like a skill's, so agents take the SAME description # every session exactly like a skill's, so agents take the SAME description
# gates. These two constants are DUPLICATED in three places: # gates. These two constants are DUPLICATED in three places:
# scripts/skill-size-check.sh, lib-checks-skill.sh beside this file, and here. # scripts/skill-size-check.sh, lib-checks-skill.sh beside this file, and here.
# The repo-root hook's copy cannot be shared with this skill — a cache-installed # The repo-root hook's copy cannot be sourced FROM this skill — a cache-installed
# plugin's scripts cannot read files outside their own plugin directory, and the # plugin's scripts cannot read files outside their own plugin directory. (The
# hook cannot reach inside the plugin. The two copies INSIDE this skill could be # hook could now read these from the plugin, as it already sources
# lib-boundary-resolver.sh, but they sit in its Python preamble; hoisting them
# is a separate change.) The two copies INSIDE this skill could be
# shared (ADR-0025: two files in one skill may source a third), and are not only # shared (ADR-0025: two files in one skill may source a third), and are not only
# because each mode library is a verbatim lift of the pre-merge suite whose # because each mode library is a verbatim lift of the pre-merge suite whose
# constants sit in its Python preamble; hoisting them is a separate change. # constants sit in its Python preamble; hoisting them is a separate change.

View File

@@ -68,9 +68,9 @@ import yaml
# resolver block below pins the reads; this pins the writes. # resolver block below pins the reads; this pins the writes.
# #
# Deliberately OUTSIDE the ADR-0020 shared boundary resolver block: the two # Deliberately OUTSIDE the ADR-0020 shared boundary resolver block: the two
# validate.sh copies print findings, skill-size-check.sh has its own top-level # validate.sh modes print findings, skill-size-check.sh has its own top-level
# equivalent, and tests/test-adr0020-contract.sh hashes that block for # equivalent, and the block is one sourced copy all three share, so each
# byte-identity across all three. # consumer's own startup stays in its own preamble.
for _stream in (sys.stdout, sys.stderr): for _stream in (sys.stdout, sys.stderr):
try: try:
_stream.reconfigure(encoding='utf-8') _stream.reconfigure(encoding='utf-8')

File diff suppressed because it is too large Load Diff

View File

@@ -3,22 +3,20 @@
# itself. None of them was pinned anywhere before this file, and each one fails # itself. None of them was pinned anywhere before this file, and each one fails
# silently — which is the whole reason they need a test rather than a comment: # silently — which is the whole reason they need a test rather than a comment:
# #
# 1. "ONE resolver, embedded VERBATIM in two scripts." The block between the # 1. "ONE resolver, and both of its consumers run it." The ADR-0020 boundary
# BEGIN/END markers is copied, not imported, because a cache-installed # resolver has exactly one copy, factory-audit's sourced
# plugin's scripts cannot read files outside their own plugin directory. # lib-boundary-resolver.sh. scripts/skill-size-check.sh used to embed a
# ADR-0025 merged skill-audit and agent-audit, which dropped the count from # second, byte-identical copy, because it was also exported through a
# three copies to two: factory-audit now holds ONE copy in a sourced # published hook manifest whose consumers could not reach a file inside the
# lib-boundary-resolver.sh, and scripts/skill-size-check.sh keeps its # plugin; 4de5b6b retired that export (ADR-0014), so the hook now sources
# embedded copy because it is a repo-root hook kept fit for a published # the library too. What must not fail silently: the hook growing its own
# hook manifest (retired; ADR-0014), where entry[0] is the only token # copy back, the library being gutted, or the hook no longer running the
# pre-commit rewrites — it could not reach a file inside the plugin at a # library's text at all. So this asserts the library is real content, the
# path any consumer has. Nothing but this file asserts the two copies are still identical, and # hook carries no marker pair, the hook fails closed without the library,
# a one-line edit to a single copy is invisible: every constant-agreement # and — by a sentinel planted in a copied library — that the text the hook
# assertion in tests/test-skill-size-check.sh still passes, because the # executes IS the library's.
# CONSTANTS are not what drifted. # 1a. The library is the ONLY authority, and validate.sh sources it in both
# 1a. The resolver's two copies are the ONLY two, and validate.sh sources # mode branches — the same authority checks 1b makes for the parser.
# factory-audit's in both mode branches — the same authority checks 1b
# makes for the parser, which byte-identity alone cannot make.
# 1b. The same claim, one directory over, for the Contributing-files parser. # 1b. The same claim, one directory over, for the Contributing-files parser.
# That one was worse: it was embedded in both validate-provenance.sh copies, # That one was worse: it was embedded in both validate-provenance.sh copies,
# the agent-audit copy's docstring ASSERTED it was kept behaviourally # the agent-audit copy's docstring ASSERTED it was kept behaviourally
@@ -49,8 +47,8 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOOK="$REPO_ROOT/scripts/skill-size-check.sh" HOOK="$REPO_ROOT/scripts/skill-size-check.sh"
FACTORY_AUDIT="$REPO_ROOT/plugins/kyberforge/.apm/skills/factory-audit" FACTORY_AUDIT="$REPO_ROOT/plugins/kyberforge/.apm/skills/factory-audit"
# ADR-0025: one auto-detecting entry point, and one sourced copy of the resolver # ADR-0025: one auto-detecting entry point, and one sourced copy of the resolver
# behind it. The entry point is what the preflight assertions run; the library is # behind it, which the root hook sources as well. The entry point is what the
# what the byte-identity assertion hashes. # preflight assertions run; the library is what assertion 1 inspects.
FACTORY_VALIDATE="$FACTORY_AUDIT/scripts/validate.sh" FACTORY_VALIDATE="$FACTORY_AUDIT/scripts/validate.sh"
FACTORY_RESOLVER="$FACTORY_AUDIT/scripts/lib-boundary-resolver.sh" FACTORY_RESOLVER="$FACTORY_AUDIT/scripts/lib-boundary-resolver.sh"
PASS=0 PASS=0
@@ -66,70 +64,111 @@ BEGIN_MARKER='# ===== BEGIN ADR-0020 SHARED BOUNDARY RESOLVER ====='
END_MARKER='# ===== END ADR-0020 SHARED BOUNDARY RESOLVER =====' END_MARKER='# ===== END ADR-0020 SHARED BOUNDARY RESOLVER ====='
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 1. The shared resolver block is byte-identical in both scripts # 1. The resolver has one copy, and the root hook runs it
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Two copies, not three, since ADR-0025 merged skill-audit and agent-audit: # One copy since the 2026-09-16 change: factory-audit's lib-boundary-resolver.sh.
# scripts/skill-size-check.sh (embedded — sourcing the plugin's copy was # scripts/skill-size-check.sh sources it rather than embedding a second copy, so
# considered and refuted, see the header) and factory-audit's single sourced # there is no pair left to hash. What replaces the hash is the set of ways a
# lib-boundary-resolver.sh. The claim is unchanged and so is the check: the # single sourced copy can still fail quietly.
# span between the markers must be byte-identical wherever it appears.
echo "" echo ""
echo "--- the ADR-0020 shared resolver block is byte-identical in both scripts ---" echo "--- the ADR-0020 resolver has one copy, and skill-size-check.sh runs it ---"
# Marker discipline first. An unbalanced or duplicated marker pair makes the # (i) The library carries exactly one well-formed marker pair around real
# extraction below silently measure the wrong span — a sed range that never # content. An unbalanced pair makes every extraction measure the wrong span, and
# closes swallows the rest of the file, and one that opens twice concatenates # an emptied-out block would still "load" while enforcing nothing. The resolver
# two spans. Both would still compare "equal" if both were mangled the # is ~1,060 lines; 100 is a floor low enough never to need maintenance and high
# same way, so the shape is asserted before the contents. # enough that a gutted block cannot sneak past.
MARKERS_OK=true if [[ ! -f "$FACTORY_RESOLVER" ]]; then
for f in "$HOOK" "$FACTORY_RESOLVER"; do fail "resolver library not found: ${FACTORY_RESOLVER#"$REPO_ROOT/"}"
if [[ ! -f "$f" ]]; then
fail "script not found: $f"
MARKERS_OK=false
continue
fi
b="$(grep -cFx "$BEGIN_MARKER" "$f" || true)"
e="$(grep -cFx "$END_MARKER" "$f" || true)"
if [[ "$b" == "1" && "$e" == "1" ]]; then
pass "${f#"$REPO_ROOT/"} carries exactly one BEGIN and one END marker"
else
fail "${f#"$REPO_ROOT/"} has $b BEGIN and $e END markers, expected 1 and 1"
MARKERS_OK=false
fi
done
if ! $MARKERS_OK; then
fail "skipping the byte-identity comparison — the marker pairs are not well-formed, so any extraction would measure the wrong span"
else else
HASHES=() b="$(grep -cFx "$BEGIN_MARKER" "$FACTORY_RESOLVER" || true)"
LINECOUNTS=() e="$(grep -cFx "$END_MARKER" "$FACTORY_RESOLVER" || true)"
for f in "$HOOK" "$FACTORY_RESOLVER"; do if [[ "$b" == "1" && "$e" == "1" ]]; then
out="$TMPDIR_T/block-$(echo "$f" | md5sum | cut -c1-8).txt" pass "${FACTORY_RESOLVER#"$REPO_ROOT/"} carries exactly one BEGIN and one END marker"
sed -n "/^${BEGIN_MARKER}\$/,/^${END_MARKER}\$/p" "$f" > "$out" span="$(sed -n "/^${BEGIN_MARKER}\$/,/^${END_MARKER}\$/p" "$FACTORY_RESOLVER" | wc -l | tr -d ' ')"
HASHES+=("$(md5sum < "$out" | cut -d' ' -f1)") if [[ "$span" -gt 100 ]]; then
LINECOUNTS+=("$(wc -l < "$out" | tr -d ' ')") pass "the resolver span is $span lines — real content, not an empty block"
done else
if [[ "${HASHES[0]}" == "${HASHES[1]}" ]]; then fail "the resolver span is only $span lines — a gutted block would load and resolve nothing"
pass "both copies hash to ${HASHES[0]} (${LINECOUNTS[0]} lines) — agreement by construction, not by coincidence" fi
else else
fail "the shared resolver has DRIFTED: skill-size-check=${HASHES[0]} (${LINECOUNTS[0]} lines), factory-audit/scripts/lib-boundary-resolver.sh=${HASHES[1]} (${LINECOUNTS[1]} lines). Edit one copy, then paste it over the other." fail "${FACTORY_RESOLVER#"$REPO_ROOT/"} has $b BEGIN and $e END markers, expected 1 and 1"
fi fi
# A block that has been emptied out would hash equal in both and pass the fi
# comparison above while enforcing nothing. The resolver is ~1,060 lines; 100
# is a floor low enough never to need maintenance and high enough that a # (ii) The hook carries no copy of its own. A column-0 marker line in the hook
# gutted block cannot sneak past. # is the shape the old embedded copy had, and the shape a paste-back would have.
if [[ "${LINECOUNTS[0]}" -gt 100 ]]; then if [[ ! -f "$HOOK" ]]; then
pass "the extracted block is ${LINECOUNTS[0]} lines — the comparison is over real content, not an empty span" fail "hook not found: ${HOOK#"$REPO_ROOT/"}"
else
hb="$(grep -cFx "$BEGIN_MARKER" "$HOOK" || true)"
he="$(grep -cFx "$END_MARKER" "$HOOK" || true)"
if [[ "$hb" == "0" && "$he" == "0" ]]; then
pass "${HOOK#"$REPO_ROOT/"} carries no resolver marker lines — it has not grown its own copy back"
else else
fail "the extracted shared block is only ${LINECOUNTS[0]} lines — two identical empty spans would compare equal and assert nothing" fail "${HOOK#"$REPO_ROOT/"} carries $hb BEGIN and $he END marker lines — a second copy of the resolver is back in the hook"
fi
fi
# (iii) and (iv) run the hook from a scratch tree that mirrors the two paths it
# depends on, so the real library is never touched. The scratch hook is a copy
# of the real one; its library is either absent, gutted, or the real library
# with a sentinel planted inside the resolver block.
SSC_TREE="$TMPDIR_T/ssc-tree"
SSC_LIB_DIR="$SSC_TREE/plugins/kyberforge/.apm/skills/factory-audit/scripts"
mkdir -p "$SSC_TREE/scripts" "$SSC_LIB_DIR" "$TMPDIR_T/ssc-skill/probe-skill"
cp "$HOOK" "$SSC_TREE/scripts/skill-size-check.sh"
printf -- '---\nname: probe-skill\ndescription: Use when probing the resolver wiring.\nmetadata:\n version: "0.1.0"\n---\n\n## Step 1\n\nDo the thing.\n' \
> "$TMPDIR_T/ssc-skill/probe-skill/SKILL.md"
PROBE="$TMPDIR_T/ssc-skill/probe-skill/SKILL.md"
run_scratch_hook() {
local rc=0
SSC_OUT="$(bash "$SSC_TREE/scripts/skill-size-check.sh" "$PROBE" 2>&1)" || rc=$?
SSC_RC=$rc
}
# (iii) Fail closed: no library, then a library that defines nothing.
rm -f "$SSC_LIB_DIR/lib-boundary-resolver.sh"
run_scratch_hook
if [[ "$SSC_RC" -ne 0 && "$SSC_OUT" == *"boundary resolver library was not found"* ]]; then
pass "with the library missing, the hook exits $SSC_RC and names the missing library — not a vacuous pass"
else
fail "with the library missing, the hook exited $SSC_RC without naming it: $SSC_OUT"
fi
printf '# gutted\n' > "$SSC_LIB_DIR/lib-boundary-resolver.sh"
run_scratch_hook
if [[ "$SSC_RC" -ne 0 && "$SSC_OUT" == *"did not define the ADR-0020 boundary resolver"* ]]; then
pass "with a library that defines no resolver, the hook exits $SSC_RC and says so"
else
fail "with a gutted library, the hook exited $SSC_RC without saying so: $SSC_OUT"
fi
# (iv) The text the hook executes IS the library's. A sentinel print planted
# just after the BEGIN marker of a copied library must appear in the hook's
# output. Without this, a hook that sourced the library but ran some other
# program would pass (i)-(iii).
SENTINEL="ADR0020-RESOLVER-SENTINEL-$$"
if [[ -f "$FACTORY_RESOLVER" ]]; then
awk -v m="$BEGIN_MARKER" -v s="$SENTINEL" '{ print } $0 == m { print "print(\"" s "\")" }' \
"$FACTORY_RESOLVER" > "$SSC_LIB_DIR/lib-boundary-resolver.sh"
if [[ "$(grep -cF "$SENTINEL" "$SSC_LIB_DIR/lib-boundary-resolver.sh" || true)" -ne 1 ]]; then
fail "fixture check: the sentinel was not planted exactly once in the copied library — the case below would prove nothing"
else
run_scratch_hook
if [[ "$SSC_RC" -eq 0 && "$SSC_OUT" == *"$SENTINEL"* ]]; then
pass "a sentinel planted in the library's resolver block runs inside the hook — the hook executes the library's text"
else
fail "the hook did not run the library's resolver text (rc=$SSC_RC, sentinel absent from output): $SSC_OUT"
fi
fi fi
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 1a. The resolver copies are the ONLY two, and validate.sh sources its one # 1a. The resolver library is the ONLY authority, and validate.sh sources it
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Byte-identity between two named files says nothing about a THIRD copy, and # Assertion 1 says nothing about a copy somewhere else in the tree, and nothing
# nothing about whether factory-audit's copy is the one that runs. Assertion 1b # about whether validate.sh runs the library. Assertion 1b
# pins both of those for the Contributing-files parser; the resolver is the same # pins both of those for the Contributing-files parser; the resolver is the same
# defect class and gets the same two checks: # defect class and gets the same two checks:
# #
@@ -137,16 +176,14 @@ fi
# branches — asserted inside each arm of `case "$MODE" in`, not by counting # branches — asserted inside each arm of `case "$MODE" in`, not by counting
# source lines file-wide, because a count cannot see a branch. A library # source lines file-wide, because a count cannot see a branch. A library
# that is identical, unique and never sourced is a copy that has quietly # that is identical, unique and never sourced is a copy that has quietly
# been replaced by an inline one — and the byte-identity check above would # been replaced by an inline one — and assertion 1 would stay green over it.
# stay green over it.
# b. Nothing has re-inlined it. The BEGIN marker and a def unique to the # b. Nothing has re-inlined it. The BEGIN marker and a def unique to the
# resolver (`_authoring_root`) appear in exactly the two authorities — # resolver (`_authoring_root`) appear in exactly one file,
# scripts/skill-size-check.sh and lib-boundary-resolver.sh — and nowhere # lib-boundary-resolver.sh, and nowhere else under the tree. A mode library
# else under the tree. A mode library that grows a "just this once" copy # or a root script that grows a "just this once" copy would otherwise
# would otherwise escape assertion 1 entirely, because 1 hashes only the # escape assertion 1 entirely, because 1 inspects only the files it names.
# two files it names.
echo "" echo ""
echo "--- the ADR-0020 resolver has exactly two authorities, and validate.sh sources factory-audit's ---" echo "--- the ADR-0020 resolver has exactly one authority, and validate.sh sources it ---"
# Deployed and vendored trees are generated copies, not authorities: .claude/ is # Deployed and vendored trees are generated copies, not authorities: .claude/ is
# apm install output, apm_modules/ is resolved dependencies, build/ is release # apm install output, apm_modules/ is resolved dependencies, build/ is release
@@ -303,18 +340,18 @@ PY
fi fi
fi fi
# (b) Exactly the two authorities, for both spellings of a copy. # (b) Exactly the one authority, for both spellings of a copy.
EXPECTED_RESOLVERS="$(printf '%s\n' "$HOOK" "$FACTORY_RESOLVER" | sort)" EXPECTED_RESOLVERS="$FACTORY_RESOLVER"
check_resolver_authorities() { check_resolver_authorities() {
local label="$1" needle="$2" local label="$1" needle="$2"
local found local found
found="$(tree_scan "$needle")" found="$(tree_scan "$needle")"
if [[ "$found" == "$EXPECTED_RESOLVERS" ]]; then if [[ "$found" == "$EXPECTED_RESOLVERS" ]]; then
pass "$label appears in exactly the two resolver authorities and nowhere else" pass "$label appears in exactly the one resolver authority and nowhere else"
elif [[ -z "$found" ]]; then elif [[ -z "$found" ]]; then
fail "$label was found in NO file at all — the scan is looking for the wrong text" fail "$label was found in NO file at all — the scan is looking for the wrong text"
else else
fail "$label appears in an unexpected set of files, so the resolver has been re-inlined or lost: $(echo "$found" | tr '\n' ' ')— expected exactly ${HOOK#"$REPO_ROOT/"} and ${FACTORY_RESOLVER#"$REPO_ROOT/"}" fail "$label appears in an unexpected set of files, so the resolver has been re-inlined or lost: $(echo "$found" | tr '\n' ' ')— expected exactly ${FACTORY_RESOLVER#"$REPO_ROOT/"}"
fi fi
} }
check_resolver_authorities "the resolver's BEGIN marker" "$BEGIN_MARKER" check_resolver_authorities "the resolver's BEGIN marker" "$BEGIN_MARKER"
@@ -325,7 +362,7 @@ check_resolver_authorities "a 'def _authoring_root' definition" "def _authoring_
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Same defect class, one directory over. parse_contributing_files() used to be # Same defect class, one directory over. parse_contributing_files() used to be
# embedded in both validate-provenance.sh copies for the same reason the resolver # embedded in both validate-provenance.sh copies for the same reason the resolver
# is embedded twice, and until this assertion existed the agent-audit copy's # was once embedded in several scripts, and until this assertion existed the agent-audit copy's
# docstring merely CLAIMED it was "kept behaviourally identical to skill-audit's # docstring merely CLAIMED it was "kept behaviourally identical to skill-audit's
# copy" — an invariant nothing checked, and the two did drift into different # copy" — an invariant nothing checked, and the two did drift into different
# spellings of the bullet loop at 484357a. That drift happened to be # spellings of the bullet loop at 484357a. That drift happened to be

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Regression test for the two ways an ADR-0020 gate can be made to check NOTHING # Regression test for the two ways an ADR-0020 gate can be made to check NOTHING
# while still exiting 0. Both were live defects, both were silent, and both sit # while still exiting 0. Both were live defects, both were silent, and both sit
# in the shared resolver block that all three scripts embed verbatim — so every # in the shared resolver block that all three entry points run — one sourced
# case below runs against all three. # copy since 2026-09-16 — so every case below runs against all three.
# #
# 1. THE FRONTMATTER BLOCKER. The frontmatter matcher used to be `^---\n`. A # 1. THE FRONTMATTER BLOCKER. The frontmatter matcher used to be `^---\n`. A
# UTF-8 BOM, a leading blank line, a trailing space after either marker, or # UTF-8 BOM, a leading blank line, a trailing space after either marker, or