ktg-plugin-marketplace/plugins/claude-design/tests/validate-plugin.sh
2026-05-18 12:31:17 +02:00

265 lines
8.1 KiB
Bash
Executable file

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