From 3dc0414948c0d070d35a1068d2d0f2c3b720f925 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Mon, 18 May 2026 12:31:17 +0200 Subject: [PATCH] feat(claude-design): add tests/validate-plugin.sh foundation validator Co-Authored-By: Claude Opus 4.7 --- .../claude-design/tests/validate-plugin.sh | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100755 plugins/claude-design/tests/validate-plugin.sh diff --git a/plugins/claude-design/tests/validate-plugin.sh b/plugins/claude-design/tests/validate-plugin.sh new file mode 100755 index 0000000..1cac718 --- /dev/null +++ b/plugins/claude-design/tests/validate-plugin.sh @@ -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