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>
104 lines
3.9 KiB
Bash
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
|