265 lines
8.1 KiB
Bash
Executable file
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
|