#!/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- (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 /*.test.mjs`, NEVER a bare dir (Node 25 gotcha), # or `bash tests/validate-plugin.sh` / `bash validate-plugin.generic.sh ` 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 # 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 |--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"