ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
Kjell Tore Guttormsen fef4b33c97 fix(migration): remediate 6 MAJOR + 3 MINOR trekreview findings + stale rename test
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>
2026-06-17 16:17:12 +02:00

143 lines
5.6 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"