ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh
Kjell Tore Guttormsen 690bbbd68d fix(migration): config-audit SC2 gate (re-seed + back-compat exclusion) + generic validator
50-config-audit-sc2.sh: re-seeds snapshot-default-output in-clone (UPDATE_SNAPSHOT=1), then runs the SC2 gate = full 'find tests -name *.test.mjs' MINUS the 6-file machine-locked v5.0.0 byte-stability surface. 730 tests across 46 files pass standalone.

templates/validate-plugin.generic.sh: ported, parameterized structure validator for the two test-less plugins (okr, human-friendly-style) — STRUCTURE OK on valid plugin.json + non-empty surface + parseable frontmatter; STRUCTURE FAIL otherwise. Reuses the claude-design/tests/validate-plugin.sh pattern.

Brief-correction (operator-ratified 2026-06-17): plan F4 named only json-backcompat + raw-backcompat (2 files). The verified machine-locked surface is 6 — the v5.0.0 fixtures embed the original absolute path AND the claude_md/plugin_hygiene scanners key off a plugins/ ancestor, so findings drift by path (behavioral, not a string rewrite). Dropped clean-room SC2 coverage = config-audit's humanizer/posture-humanizer/scan-orchestrator-humanizer prose-snapshot surface, recorded in the script header + plugin-map.json (not silent). The 3 other v5.0.0-referencing tests (posture, scoring-humanizer, scenario-read-test) are path-independent and stay in the gate. config-audit backlog (out of migration scope): normalize the v5.0.0 fixtures + path-agnostic scanners, then re-include.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:23:38 +02:00

104 lines
3.9 KiB
Bash

#!/usr/bin/env bash
# validate-plugin.generic.sh — generic, parameterized plugin structure validator (Step 7).
#
# A ported, plugin-agnostic copy of plugins/claude-design/tests/validate-plugin.sh, stripped of
# claude-design's bespoke checks (.coverage.md, SKILL description length, forbidden command names,
# operator-private grep). It gives the two test-less plugins — okr + human-friendly-style — a runnable
# SC2 gate for the standalone-extraction harness (40-validate-standalone.sh vendors this file into the
# clean room and runs `bash validate-plugin.generic.sh <key>`).
#
# Checks (all generic):
# (a) .claude-plugin/plugin.json is valid JSON with non-empty name/version/description [HARD]
# (b) at least one user-facing surface dir is present + non-empty
# (commands | agents | skills | output-styles | hooks) [HARD]
# (c) every commands/*.md, agents/*.md, output-styles/*.md and skills/*/SKILL.md parses:
# line 1 is the `---` frontmatter delimiter and the block carries a `name:` key [HARD]
#
# Usage: validate-plugin.generic.sh <key> [plugin_root]
# <key> label used in the verdict line (e.g. "okr: STRUCTURE OK")
# [plugin_root] plugin root to validate; defaults to $PWD (the clean-room cwd)
#
# Exit codes: 0 = STRUCTURE OK; 1 = STRUCTURE FAIL (>=1 hard check failed); 2 = usage error.
set -uo pipefail
KEY="${1:-}"
[ -n "$KEY" ] || { echo "usage: validate-plugin.generic.sh <key> [plugin_root]" >&2; exit 2; }
PLUGIN_ROOT="${2:-$PWD}"
PASS=0
FAIL=0
pass() { printf ' + %s\n' "$1"; PASS=$((PASS + 1)); }
fail() { printf ' - %s\n' "$1"; FAIL=$((FAIL + 1)); }
echo "=== $KEY structure validation (root: $PLUGIN_ROOT) ==="
# --- (a) plugin.json valid JSON + required fields ---
echo "--- (a) plugin.json ---"
PJ=""
for cand in "$PLUGIN_ROOT/.claude-plugin/plugin.json" "$PLUGIN_ROOT/plugin.json"; do
[ -f "$cand" ] && { PJ="$cand"; break; }
done
if [ -z "$PJ" ]; then
fail "plugin.json missing (.claude-plugin/plugin.json)"
elif ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$PJ" 2>/dev/null; then
fail "plugin.json is invalid JSON"
else
pass "plugin.json is valid JSON"
for field in name version description; do
if node -e "const p=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));if(typeof p[process.argv[2]]!=='string'||p[process.argv[2]]==='')process.exit(1)" "$PJ" "$field" 2>/dev/null; then
pass "plugin.json has '$field'"
else
fail "plugin.json missing or empty '$field'"
fi
done
fi
# --- (b) at least one non-empty user-facing surface dir ---
echo "--- (b) user-facing surface ---"
SURFACE=0
for d in commands agents skills output-styles hooks; do
dir="$PLUGIN_ROOT/$d"
[ -d "$dir" ] || continue
if find "$dir" -type f 2>/dev/null | grep -q .; then
pass "surface dir '$d/' present and non-empty"
SURFACE=$((SURFACE + 1))
fi
done
if [ "$SURFACE" -eq 0 ]; then
fail "no non-empty user-facing surface dir (commands/agents/skills/output-styles/hooks)"
fi
# --- (c) frontmatter parses on every surface markdown file ---
echo "--- (c) frontmatter ---"
check_frontmatter() {
local f="$1"
local rel="${f#"$PLUGIN_ROOT"/}"
if [ "$(head -n 1 "$f")" != "---" ]; then
fail "$rel: missing frontmatter delimiter on line 1"
return
fi
local fm
fm="$(awk 'NR==1{next} /^---$/{exit} {print}' "$f")"
if printf '%s\n' "$fm" | grep -qE '^name:'; then
pass "$rel: frontmatter has 'name:'"
else
fail "$rel: frontmatter missing 'name:'"
fi
}
FM_SEEN=0
for f in "$PLUGIN_ROOT"/commands/*.md "$PLUGIN_ROOT"/agents/*.md \
"$PLUGIN_ROOT"/output-styles/*.md "$PLUGIN_ROOT"/skills/*/SKILL.md; do
[ -f "$f" ] || continue
FM_SEEN=$((FM_SEEN + 1))
check_frontmatter "$f"
done
[ "$FM_SEEN" -eq 0 ] && pass "no frontmatter-bearing surface files to check"
echo "=== Summary: pass=$PASS fail=$FAIL ==="
if [ "$FAIL" -gt 0 ]; then
echo "$KEY: STRUCTURE FAIL"
exit 1
fi
echo "$KEY: STRUCTURE OK"
exit 0