feat(claude-design): add tests/validate-plugin.sh foundation validator
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fd04793ee5
commit
3dc0414948
1 changed files with 265 additions and 0 deletions
265
plugins/claude-design/tests/validate-plugin.sh
Executable file
265
plugins/claude-design/tests/validate-plugin.sh
Executable file
|
|
@ -0,0 +1,265 @@
|
|||
#!/usr/bin/env bash
|
||||
# validate-plugin.sh — Foundation plugin structure validator for claude-design
|
||||
# Usage: bash tests/validate-plugin.sh
|
||||
# Exit codes: 0 = all checks pass; 1 = at least one FAIL
|
||||
#
|
||||
# Forked from plugins/ms-ai-architect/tests/validate-plugin.sh:
|
||||
# keep: helpers (pass/fail/warn), counters, PLUGIN_ROOT, JSON-validity check,
|
||||
# README/CLAUDE.md existence checks
|
||||
# strip: agent frontmatter loop, commands frontmatter loop, KB-staleness checks,
|
||||
# architect:* command-name assertions, references-count assertions
|
||||
# add: SKILL.md frontmatter + description-length, LICENSE content, GOVERNANCE
|
||||
# existence, .coverage.md existence, forbidden-command-name regex (h),
|
||||
# operator-private-context grep (i), Norwegian-leakage grep (j)
|
||||
|
||||
set -euo pipefail
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
WARN=0
|
||||
|
||||
pass() { printf "${GREEN} ✓ %s${NC}\n" "$1"; PASS=$((PASS + 1)); }
|
||||
fail() { printf "${RED} ✗ %s${NC}\n" "$1"; FAIL=$((FAIL + 1)); }
|
||||
warn() { printf "${YELLOW} ⚠ %s${NC}\n" "$1"; WARN=$((WARN + 1)); }
|
||||
|
||||
echo "=== claude-design Plugin Validation ==="
|
||||
echo "Plugin root: $PLUGIN_ROOT"
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (a): plugin.json valid + required fields
|
||||
# -------------------------------------------------------
|
||||
echo "--- (a) plugin.json structure ---"
|
||||
|
||||
PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json"
|
||||
|
||||
if [ ! -f "$PLUGIN_JSON" ]; then
|
||||
fail ".claude-plugin/plugin.json missing"
|
||||
else
|
||||
if node -e "JSON.parse(require('fs').readFileSync('$PLUGIN_JSON'))" 2>/dev/null; then
|
||||
pass ".claude-plugin/plugin.json is valid JSON"
|
||||
else
|
||||
fail ".claude-plugin/plugin.json is invalid JSON"
|
||||
fi
|
||||
|
||||
for field in name version description; do
|
||||
if node -e "const p = JSON.parse(require('fs').readFileSync('$PLUGIN_JSON')); if (typeof p['$field'] !== 'string' || p['$field'] === '') process.exit(1)" 2>/dev/null; then
|
||||
pass "plugin.json has '$field'"
|
||||
else
|
||||
fail "plugin.json missing or empty '$field'"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (b): at least one SKILL.md under skills/*/
|
||||
# -------------------------------------------------------
|
||||
echo "--- (b) SKILL.md presence ---"
|
||||
|
||||
SKILL_COUNT=0
|
||||
for skill_file in "$PLUGIN_ROOT"/skills/*/SKILL.md; do
|
||||
[ -f "$skill_file" ] || continue
|
||||
SKILL_COUNT=$((SKILL_COUNT + 1))
|
||||
done
|
||||
|
||||
if [ "$SKILL_COUNT" -ge 1 ]; then
|
||||
pass "found $SKILL_COUNT SKILL.md file(s) under skills/*/"
|
||||
else
|
||||
fail "no SKILL.md found under skills/*/"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (c): SKILL.md frontmatter has name+description, description >=400 chars
|
||||
# -------------------------------------------------------
|
||||
echo "--- (c) SKILL.md frontmatter quality ---"
|
||||
|
||||
for skill_file in "$PLUGIN_ROOT"/skills/*/SKILL.md; do
|
||||
[ -f "$skill_file" ] || continue
|
||||
basename_skill="$(basename "$(dirname "$skill_file")")/SKILL.md"
|
||||
|
||||
first_line="$(head -n 1 "$skill_file")"
|
||||
if [ "$first_line" != "---" ]; then
|
||||
fail "$basename_skill: missing frontmatter delimiter on line 1"
|
||||
continue
|
||||
fi
|
||||
|
||||
frontmatter="$(awk 'NR==1{next} /^---$/{exit} {print}' "$skill_file")"
|
||||
|
||||
if echo "$frontmatter" | grep -qE '^name:'; then
|
||||
pass "$basename_skill: has 'name:'"
|
||||
else
|
||||
fail "$basename_skill: missing 'name:'"
|
||||
fi
|
||||
|
||||
if echo "$frontmatter" | grep -qE '^description:'; then
|
||||
pass "$basename_skill: has 'description:'"
|
||||
else
|
||||
fail "$basename_skill: missing 'description:'"
|
||||
fi
|
||||
|
||||
desc_len="$(awk '/^description: \|/,/^---$/' "$skill_file" | wc -c | tr -d '[:space:]')"
|
||||
if [ -z "$desc_len" ]; then desc_len=0; fi
|
||||
if [ "$desc_len" -ge 400 ]; then
|
||||
pass "$basename_skill: description block is $desc_len chars (>=400)"
|
||||
else
|
||||
fail "$basename_skill: description block is $desc_len chars (<400)"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (d): LICENSE exists, non-empty, contains "MIT License"
|
||||
# -------------------------------------------------------
|
||||
echo "--- (d) LICENSE ---"
|
||||
|
||||
LICENSE_FILE="$PLUGIN_ROOT/LICENSE"
|
||||
|
||||
if [ ! -f "$LICENSE_FILE" ]; then
|
||||
fail "LICENSE missing"
|
||||
elif [ ! -s "$LICENSE_FILE" ]; then
|
||||
fail "LICENSE is empty"
|
||||
elif ! grep -q "MIT License" "$LICENSE_FILE"; then
|
||||
fail "LICENSE does not contain 'MIT License'"
|
||||
else
|
||||
pass "LICENSE exists, non-empty, MIT License"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (e): GOVERNANCE.md exists, non-empty
|
||||
# -------------------------------------------------------
|
||||
echo "--- (e) GOVERNANCE.md ---"
|
||||
|
||||
GOVERNANCE_FILE="$PLUGIN_ROOT/GOVERNANCE.md"
|
||||
|
||||
if [ ! -f "$GOVERNANCE_FILE" ]; then
|
||||
fail "GOVERNANCE.md missing"
|
||||
elif [ ! -s "$GOVERNANCE_FILE" ]; then
|
||||
fail "GOVERNANCE.md is empty"
|
||||
else
|
||||
pass "GOVERNANCE.md exists, non-empty"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (f): README.md + CLAUDE.md exist, non-empty
|
||||
# -------------------------------------------------------
|
||||
echo "--- (f) README.md and CLAUDE.md ---"
|
||||
|
||||
for f in README.md CLAUDE.md; do
|
||||
fpath="$PLUGIN_ROOT/$f"
|
||||
if [ ! -f "$fpath" ]; then
|
||||
fail "$f missing"
|
||||
elif [ ! -s "$fpath" ]; then
|
||||
fail "$f is empty"
|
||||
else
|
||||
pass "$f exists, non-empty"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (g): .coverage.md exists at plugin root
|
||||
# -------------------------------------------------------
|
||||
echo "--- (g) .coverage.md ---"
|
||||
|
||||
COVERAGE_FILE="$PLUGIN_ROOT/.coverage.md"
|
||||
|
||||
if [ ! -f "$COVERAGE_FILE" ]; then
|
||||
fail ".coverage.md missing"
|
||||
elif [ ! -s "$COVERAGE_FILE" ]; then
|
||||
fail ".coverage.md is empty"
|
||||
else
|
||||
pass ".coverage.md exists, non-empty"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (h): forbidden command-name regex (scope fence vs
|
||||
# Anthropic's knowledge-work-plugins/design)
|
||||
# -------------------------------------------------------
|
||||
echo "--- (h) forbidden command-name regex ---"
|
||||
|
||||
FORBIDDEN_REGEX='^name:[[:space:]]*(claude-design:)?(critique|accessibility|ux-copy|research-synthesis|design-system|handoff)[[:space:]]*$'
|
||||
|
||||
H_HIT=0
|
||||
|
||||
for cmd_file in "$PLUGIN_ROOT"/commands/*.md "$PLUGIN_ROOT"/skills/*/SKILL.md; do
|
||||
[ -f "$cmd_file" ] || continue
|
||||
if grep -qE "$FORBIDDEN_REGEX" "$cmd_file"; then
|
||||
fail "command-name collision with Anthropic's official knowledge-work-plugins/design plugin: $cmd_file"
|
||||
H_HIT=$((H_HIT + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$H_HIT" -eq 0 ]; then
|
||||
pass "no forbidden command-name collisions"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (i): operator-private-context grep
|
||||
# -------------------------------------------------------
|
||||
echo "--- (i) operator-private-context grep ---"
|
||||
|
||||
I_HITS="$(grep -rnE '(kjell|vegvesen|NEXT-SESSION-PROMPT|REMEMBER\.md content from)' \
|
||||
"$PLUGIN_ROOT" \
|
||||
--include='*.md' \
|
||||
--exclude-dir='.claude' \
|
||||
--exclude-dir='tests' \
|
||||
--exclude='REMEMBER.md' \
|
||||
--exclude='TODO.md' \
|
||||
--exclude='NEXT-SESSION-PROMPT.local.md' \
|
||||
2>/dev/null || true)"
|
||||
|
||||
if [ -z "$I_HITS" ]; then
|
||||
pass "no operator-private context leaks in shipped content"
|
||||
else
|
||||
while IFS= read -r hit; do
|
||||
fail "operator-private context leak in shipped content (brief NFR): $hit"
|
||||
done < <(printf '%s\n' "$I_HITS")
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Check (j): Norwegian-leakage grep (WARN, not FAIL)
|
||||
# -------------------------------------------------------
|
||||
echo "--- (j) Norwegian-leakage grep ---"
|
||||
|
||||
J_HITS="$(grep -rnE '[æøåÆØÅ]' \
|
||||
"$PLUGIN_ROOT" \
|
||||
--include='*.md' \
|
||||
--exclude-dir='.claude' \
|
||||
--exclude='REMEMBER.md' \
|
||||
--exclude='TODO.md' \
|
||||
--exclude='NEXT-SESSION-PROMPT.local.md' \
|
||||
2>/dev/null || true)"
|
||||
|
||||
if [ -z "$J_HITS" ]; then
|
||||
pass "no Norwegian diacritics in shipped content"
|
||||
else
|
||||
while IFS= read -r hit; do
|
||||
warn "Norwegian diacritic in shipped content (review case-by-case): $hit"
|
||||
done < <(printf '%s\n' "$J_HITS")
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Summary
|
||||
# -------------------------------------------------------
|
||||
echo "=== Summary ==="
|
||||
printf "Pass: %d Fail: %d Warn: %d\n" "$PASS" "$FAIL" "$WARN"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue