fix(gates): close the /name fail-open and stop the path guard inventing targets
Two defects in the routing-target resolver, both latent in the corpus but hot for anything written next. The free-standing `/name` sweep sat inside `if boundary:`, so route notation in a sentence carrying no boundary marker was never extracted at all — not an ERROR, not a SUGGESTION, not an INFO. That contradicted ADR-0020's amendment and gates.md, which both promise `/name` blocks unconditionally. The sweep now runs over every sentence. `-> name` and backticked forms stay gated deliberately: an arrow also writes a process chain and a code span cites tools, files and skills alike, so ungating either fires on ordinary prose. The path guard used `\b`, which still holds after a hyphen, so the engine backtracked to a shorter hyphen-terminated prefix whenever the lookahead rejected the full segment. `/api-docs/v2.md` in a boundary clause raised blocking ERRORs for 'api' and 'api-docs' — names no author wrote, with no corroboration escape. `(?![\w-])` forbids the shortened prefix outright; MARKED_TARGET, which had no trailing guard at all, gained one. Zero arguments now exits 2 rather than 0, so a mis-scoped `files:` pattern is no longer indistinguishable from a clean corpus. Both hook manifests pass filenames and pre-commit skips a filename-passing hook when nothing matches, so the hook never sees an empty argv — that contract is now asserted by a test rather than left in prose. Deleting the sweep entirely used to leave every suite green. It now kills eight assertions. The suite also gains its first slash-path and URL fixtures, in both directions. Refs: #107, #110, #124 ADR: 0020
This commit is contained in:
@@ -702,6 +702,175 @@ expect_gate "a fixture with no authoring root reports DID NOT RUN and exits 0" \
|
||||
#
|
||||
# If a real dangling target ever reappears, add its probe back here.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Usage tier: zero arguments is exit 2, not a clean run
|
||||
# ---------------------------------------------------------------------------
|
||||
# The script used to print nothing and exit 0 when handed no paths, which made
|
||||
# a mis-scoped `files:` pattern indistinguishable from a corpus with no
|
||||
# findings — the whole ADR-0020 gate family silently disabled while every hook
|
||||
# reported green. Exit 2 (not 1) is the same split a8cd5e8 made in
|
||||
# provider-adapter-author's validate-adapter.sh and the one vale-wrap.sh already
|
||||
# used: {0,1} are verdicts, 2 is "you invoked this wrong".
|
||||
#
|
||||
# SAFE FOR THE HOOK. Both manifests declare pass_filenames: true and neither
|
||||
# sets always_run, and pre-commit skips a filename-passing hook outright when
|
||||
# its `files:` pattern matches nothing, so pre-commit never invokes this script
|
||||
# with an empty argument list. That claim is asserted below rather than left in
|
||||
# prose, so a config edit that turns it false fails here.
|
||||
echo ""
|
||||
echo "--- zero arguments is a usage error (exit 2), not a silent clean run ---"
|
||||
set +e
|
||||
USAGE_OUT="$("$SCRIPT" 2>&1)"
|
||||
USAGE_RC=$?
|
||||
set -e
|
||||
if [[ $USAGE_RC -eq 2 ]]; then
|
||||
pass "no arguments exits 2"
|
||||
else
|
||||
fail "no arguments exited $USAGE_RC, expected 2 (output: ${USAGE_OUT:-<empty>})"
|
||||
fi
|
||||
if [[ "$USAGE_OUT" == *usage* ]]; then
|
||||
pass "no arguments prints a usage message"
|
||||
else
|
||||
fail "no arguments produced no usage message (output: ${USAGE_OUT:-<empty>})"
|
||||
fi
|
||||
# The exit code must be DISTINCT from both verdicts, or the split buys nothing.
|
||||
# $SMALL is the clean fixture built at the top of this file; $MANY_LINES is over
|
||||
# the line ceiling.
|
||||
set +e
|
||||
"$SCRIPT" "$SMALL" > /dev/null 2>&1
|
||||
CLEAN_RC=$?
|
||||
"$SCRIPT" "$MANY_LINES" > /dev/null 2>&1
|
||||
FINDING_RC=$?
|
||||
set -e
|
||||
if [[ $CLEAN_RC -eq 0 && $FINDING_RC -eq 1 && $USAGE_RC -eq 2 ]]; then
|
||||
pass "the three exit codes are distinct: clean=0, findings=1, usage=2"
|
||||
else
|
||||
fail "exit codes collide — clean=$CLEAN_RC findings=$FINDING_RC usage=$USAGE_RC"
|
||||
fi
|
||||
# The hook contract the usage exit depends on. If either manifest ever stops
|
||||
# passing filenames, or starts always_run, pre-commit could invoke the script
|
||||
# with no paths and exit 2 would break the hook rather than diagnose a caller.
|
||||
HOOK_CONTRACT="$(python3 - "$REPO_ROOT" <<'PYHOOK'
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
root = sys.argv[1]
|
||||
problems = []
|
||||
|
||||
|
||||
def check(label, hook):
|
||||
if hook is None:
|
||||
problems.append('%s declares no such hook' % label)
|
||||
return
|
||||
if hook.get('pass_filenames') is False:
|
||||
problems.append('%s sets pass_filenames: false' % label)
|
||||
if hook.get('always_run'):
|
||||
problems.append('%s sets always_run: true' % label)
|
||||
|
||||
|
||||
with open(os.path.join(root, '.pre-commit-config.yaml'), encoding='utf-8') as fh:
|
||||
cfg = yaml.safe_load(fh) or {}
|
||||
found = None
|
||||
for repo in cfg.get('repos') or []:
|
||||
for hook in (repo.get('hooks') or []):
|
||||
if hook.get('id') == 'skill-size-check':
|
||||
found = hook
|
||||
check('.pre-commit-config.yaml skill-size-check', found)
|
||||
|
||||
with open(os.path.join(root, '.pre-commit-hooks.yaml'), encoding='utf-8') as fh:
|
||||
hooks = yaml.safe_load(fh) or []
|
||||
found = None
|
||||
for hook in hooks:
|
||||
if isinstance(hook, dict) and hook.get('id') == 'kyberforge-skill-size-check':
|
||||
found = hook
|
||||
check('.pre-commit-hooks.yaml kyberforge-skill-size-check', found)
|
||||
|
||||
print('; '.join(problems))
|
||||
PYHOOK
|
||||
)"
|
||||
if [[ -z "$HOOK_CONTRACT" ]]; then
|
||||
pass "both manifests pass filenames and neither is always_run, so pre-commit never invokes the script with no paths"
|
||||
else
|
||||
fail "the usage exit would break the hook: $HOOK_CONTRACT"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# An unreadable path is diagnosed ONCE
|
||||
# ---------------------------------------------------------------------------
|
||||
# The stat dance lived twice — a bash pre-loop and the Python per-file loop —
|
||||
# and both printed the same sentence, so one broken file produced two ERROR
|
||||
# lines with two different "so ... could not be measured" clauses. Duplicated
|
||||
# output on a blocking gate reads as two problems and sends the author hunting
|
||||
# for a second one. The check must still FIRE (silence is the failure this
|
||||
# script forbids itself); it must fire exactly once.
|
||||
echo ""
|
||||
echo "--- an unreadable path produces exactly one ERROR line, not two ---"
|
||||
UNREADABLE_DIR="$TMPDIR/unreadable"
|
||||
mkdir -p "$UNREADABLE_DIR/a-directory.md"
|
||||
ln -sf "$TMPDIR/definitely-not-here.md" "$UNREADABLE_DIR/broken-link.md"
|
||||
|
||||
# unreadable_case <label> <path>
|
||||
unreadable_case() {
|
||||
local label="$1" path="$2" out status=0 count
|
||||
set +e
|
||||
out="$("$SCRIPT" "$path" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
count="$(printf '%s\n' "$out" | grep -cF "ERROR: $path" || true)"
|
||||
if [[ $status -eq 0 ]]; then
|
||||
fail "$label: exited 0 — an unmeasurable path passed in silence (output: ${out:-<empty>})"
|
||||
elif [[ "$count" != "1" ]]; then
|
||||
fail "$label: $count ERROR lines name the path, expected exactly 1 (output: $out)"
|
||||
else
|
||||
pass "$label"
|
||||
fi
|
||||
}
|
||||
unreadable_case "a path that does not exist is reported once" \
|
||||
"$TMPDIR/no-such-file.md"
|
||||
unreadable_case "a DIRECTORY named *.md is reported once" \
|
||||
"$UNREADABLE_DIR/a-directory.md"
|
||||
unreadable_case "a broken symlink is reported once" \
|
||||
"$UNREADABLE_DIR/broken-link.md"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Encoding, write side: under LC_ALL=C the report must still print
|
||||
# ---------------------------------------------------------------------------
|
||||
# read_text() in the shared ADR-0020 resolver block pins the READS to UTF-8.
|
||||
# That moved the crash to the WRITE: this script's own message text carries em
|
||||
# dashes (the boundary SUGGESTION is one), so under LC_ALL=C the streams' ASCII
|
||||
# default raised UnicodeEncodeError while PRINTING -- after every check had
|
||||
# already run, losing the whole report at the last step and turning a
|
||||
# SUGGESTION-only exit 0 into a traceback and an exit 1.
|
||||
echo ""
|
||||
echo "--- under LC_ALL=C the SUGGESTION is printed, not lost to a UnicodeEncodeError ---"
|
||||
LOCALE_SKILL="$TMPDIR/locale-skill"
|
||||
mkdir -p "$LOCALE_SKILL"
|
||||
cat > "$LOCALE_SKILL/SKILL.md" <<'LOCALEEOF'
|
||||
---
|
||||
name: locale-skill
|
||||
description: A valid skill description that is well within the limit.
|
||||
---
|
||||
|
||||
## Step 1
|
||||
|
||||
Do the thing.
|
||||
LOCALEEOF
|
||||
set +e
|
||||
LOCALE_OUT="$(env LC_ALL=C PYTHONUTF8=0 "$SCRIPT" "$LOCALE_SKILL/SKILL.md" 2>&1)"
|
||||
LOCALE_STATUS=$?
|
||||
set -e
|
||||
if [[ $LOCALE_STATUS -ne 0 ]]; then
|
||||
fail "a SUGGESTION-only subject exited $LOCALE_STATUS under LC_ALL=C (output: ${LOCALE_OUT:-<empty>})"
|
||||
elif [[ "$LOCALE_OUT" == *UnicodeEncodeError* || "$LOCALE_OUT" == *Traceback* ]]; then
|
||||
fail "the report died encoding its own message text under LC_ALL=C (output: $LOCALE_OUT)"
|
||||
elif [[ "$LOCALE_OUT" != *"description has no boundary clause"* ]]; then
|
||||
fail "the SUGGESTION never reached stdout under LC_ALL=C (output: ${LOCALE_OUT:-<empty>})"
|
||||
else
|
||||
pass "the SUGGESTION survives LC_ALL=C, streams pinned to UTF-8"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
|
||||
Reference in New Issue
Block a user