MAJOR
- 9e97cd5 40-validate-standalone.sh: route a target's sc2_gate to its dedicated
gate (config-audit → 50-config-audit-sc2.sh), mirroring 99-dryrun.sh, so --all
no longer falsely FAILs config-audit on the machine-locked v5.0.0 tests.
- 1708e90 99-dryrun.sh: assert EXACTLY one tag survives (F5); a partial tag-strip
no longer silently reports the wrong tag via head -1.
- 4e494c8 99-dryrun.sh: capture the SC2 standalone failing set from the dry-run's
own prepped extract ($dest), not the 40-validate side-effect clean room.
- aeb6292 00-preflight.sh: assert every map path is whitespace/glob-free, making
the word-split path handling in 99-dryrun.sh sound.
- 5d112cb extract the SC6 DROP + SC2 regression detectors into sc6-check.sh /
sc2-regression.sh and add sc-checks.test.mjs — a negative test proving each
detector FIRES (force-fresh re-extraction would undo a planted file-drop).
- 9e588ca 10-extract.sh re-asserts git filter-repo before use (self-heal runs
preflight only on a missing mirror); RUNBOOK lists git-filter-repo + python3>=3.6.
MINOR
- bc0f8a7 plugin-map.json: reset ms-ai-architect blob_strip_safe to null
(00-preflight.sh populates it per run).
- 8d649e9 99-dryrun.sh: gate SC6 behind extract success; a failed extract is
labelled (extract failed), not a content DROP.
- 4044c49 99-dryrun.sh: guard mktemp — an empty capture is an error, not a
false zero-regression PASS.
Also: 00-preflight.test.mjs asserted all 3 'renamed' plugins carry >=2 paths, but
llm-security became single-path in 836b8e9 (copilot was a coexisting plugin, not a
rename) — a stale pre-existing failure. Aligned the test to the ratified map and
added a positive single-path lock against re-introducing the 87-file-drop defect.
Verified: full dry-run 11/11, 0 pushes; sc-checks/99-dryrun/40-validate/00-preflight/
60-rewrite suites green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
143 lines
5.6 KiB
Bash
143 lines
5.6 KiB
Bash
#!/usr/bin/env bash
|
||
# Step 6 — Standalone validation harness (SC2 + SC7).
|
||
# For each target: ensures the extract is prepped (extract -> rehome -> fix-references, all idempotent),
|
||
# copies it to a clean room /tmp/claude-<key> (no marketplace parent), then:
|
||
# SC2: runs the runner the plugin itself declares in plugin-map.json (test_cmd) — always the glob form
|
||
# `node --test 'tests/**/*.test.mjs'` or `node --test <dir>/*.test.mjs`, NEVER a bare dir (Node 25 gotcha),
|
||
# or `bash tests/validate-plugin.sh` / `bash validate-plugin.generic.sh <key>` for the test-less plugins.
|
||
# linkedin-studio is two-tier (M11): the .mjs core is the HARD gate; its TS analytics suite is
|
||
# reported as ADVISORY (npm/network — not a clean-room gate).
|
||
# SC7: no tracked STATE.md / *.local.md (except okr/templates/okr.local.md.template), no ../../README.md,
|
||
# .gitignore ignores .claude/.
|
||
# voyage: .forgejo/ISSUE_TEMPLATE/ survives the extraction and holds no monorepo-relative path (M17).
|
||
# Emits a per-repo PASS/FAIL table; exits non-zero if any target FAILs (escalate — never mask). NULL push (D8).
|
||
#
|
||
# Usage: 40-validate-standalone.sh <target-key>
|
||
# 40-validate-standalone.sh --all
|
||
set -uo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
MAP="$SCRIPT_DIR/plugin-map.json"
|
||
GENERIC_VALIDATOR="$SCRIPT_DIR/templates/validate-plugin.generic.sh"
|
||
WORK="${WORK:-/tmp/polyrepo-migration}"
|
||
|
||
[ -f "$MAP" ] || { echo "plugin-map.json missing at $MAP" >&2; exit 1; }
|
||
|
||
ARG="${1:-}"
|
||
[ -n "$ARG" ] || { echo "usage: 40-validate-standalone.sh <target-key>|--all" >&2; exit 2; }
|
||
|
||
if [ "$ARG" = "--all" ]; then
|
||
KEYS="$(python3 -c "import json; print('\n'.join(sorted(json.load(open('$MAP'))['targets'])))")"
|
||
else
|
||
KEYS="$ARG"
|
||
fi
|
||
|
||
FAILS=0
|
||
|
||
prep_target() {
|
||
local key="$1"
|
||
local dest="$WORK/$key"
|
||
if [ ! -d "$dest/.git" ]; then
|
||
WORK="$WORK" bash "$SCRIPT_DIR/10-extract.sh" "$key" >/dev/null || return 1
|
||
fi
|
||
WORK="$WORK" bash "$SCRIPT_DIR/20-rehome-config.sh" "$key" >/dev/null || return 1
|
||
WORK="$WORK" node "$SCRIPT_DIR/30-fix-references.mjs" "$key" >/dev/null || return 1
|
||
return 0
|
||
}
|
||
|
||
validate_target() {
|
||
local key="$1"
|
||
local dest="$WORK/$key"
|
||
local cr="/tmp/claude-$key"
|
||
local problems=""
|
||
|
||
rm -rf "$cr"
|
||
cp -R "$dest" "$cr"
|
||
|
||
# --- SC7: no tracked STATE.md / *.local.md (except the okr template) ---
|
||
local leaks
|
||
leaks="$(git -C "$cr" ls-files | grep -E 'STATE\.md|\.local\.md$' | grep -v 'templates/okr\.local\.md\.template' || true)"
|
||
[ -z "$leaks" ] || problems="$problems; tracked state-file leak: $(echo "$leaks" | tr '\n' ' ')"
|
||
|
||
# --- SC7: no ../../README.md remains ---
|
||
if grep -rn '\.\./\.\./README\.md' "$cr" --include='*.md' >/dev/null 2>&1; then
|
||
problems="$problems; ../../README.md reference remains"
|
||
fi
|
||
|
||
# --- SC7: .gitignore ignores .claude/ ---
|
||
if ! git -C "$cr" check-ignore .claude/x >/dev/null 2>&1; then
|
||
problems="$problems; .gitignore does not ignore .claude/"
|
||
fi
|
||
|
||
# --- voyage: .forgejo survival (M17) ---
|
||
if [ "$key" = "voyage" ]; then
|
||
if [ ! -d "$cr/.forgejo/ISSUE_TEMPLATE" ]; then
|
||
problems="$problems; .forgejo/ISSUE_TEMPLATE missing"
|
||
elif grep -rn '\.\./\.\.' "$cr/.forgejo" >/dev/null 2>&1; then
|
||
problems="$problems; .forgejo holds a monorepo-relative path"
|
||
fi
|
||
fi
|
||
|
||
# --- SC2: route to the target's dedicated gate if it declares one, else run the declared runner in the
|
||
# clean room. config-audit's full `node --test 'tests/**/*.test.mjs'` FAILs at a fresh-clone path on
|
||
# the 6 machine-locked v5.0.0 byte-stability tests — the exact blocker its Step-7 gate resolves — so
|
||
# `--all` MUST delegate to that gate (mirroring 99-dryrun.sh), not run the raw test_cmd. ---
|
||
local test_cmd advisory sc2_gate
|
||
test_cmd="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('test_cmd',''))")"
|
||
advisory="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('test_cmd_advisory',''))")"
|
||
sc2_gate="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('sc2_gate',''))")"
|
||
|
||
local sc2_label="standalone-safe"
|
||
|
||
if [ -n "$sc2_gate" ]; then
|
||
if WORK="$WORK" bash "$SCRIPT_DIR/$sc2_gate" >/dev/null 2>&1; then
|
||
sc2_label="$sc2_gate, standalone-safe"
|
||
else
|
||
problems="$problems; SC2 gate failed ($sc2_gate)"
|
||
fi
|
||
else
|
||
# The generic structure validator (Step 7) is referenced by test-less plugins — vendor it into the clean room.
|
||
case "$test_cmd" in
|
||
*validate-plugin.generic.sh*)
|
||
if [ -f "$GENERIC_VALIDATOR" ]; then cp "$GENERIC_VALIDATOR" "$cr/validate-plugin.generic.sh"; fi
|
||
;;
|
||
esac
|
||
|
||
local out status tests
|
||
out="$(cd "$cr" && eval "$test_cmd" 2>&1)"; status=$?
|
||
if [ $status -ne 0 ]; then
|
||
problems="$problems; SC2 runner failed (exit $status)"
|
||
fi
|
||
case "$test_cmd" in
|
||
*node\ --test*)
|
||
# Node 25's default reporter prints "ℹ tests N"; older/TAP prints "# tests N". Match either.
|
||
tests="$(printf '%s\n' "$out" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)"
|
||
[ -n "$tests" ] && sc2_label="$tests tests, standalone-safe"
|
||
;;
|
||
*validate-plugin*) sc2_label="structure, standalone-safe" ;;
|
||
esac
|
||
fi
|
||
|
||
if [ -n "$problems" ]; then
|
||
echo "$key: FAIL${problems}"
|
||
FAILS=$((FAILS+1))
|
||
else
|
||
echo "$key: PASS ($sc2_label)"
|
||
[ -n "$advisory" ] && echo " advisory (not a clean-room gate): $advisory"
|
||
fi
|
||
}
|
||
|
||
for key in $KEYS; do
|
||
if ! prep_target "$key"; then
|
||
echo "$key: FAIL (prep/extract error)"
|
||
FAILS=$((FAILS+1))
|
||
continue
|
||
fi
|
||
validate_target "$key"
|
||
done
|
||
|
||
if [ "$FAILS" -ne 0 ]; then
|
||
echo "VALIDATE: $FAILS target(s) FAILED"
|
||
exit 1
|
||
fi
|
||
echo "VALIDATE OK"
|