diff --git a/plugins/linkedin-studio/scripts/test-runner.sh b/plugins/linkedin-studio/scripts/test-runner.sh index d300f48..12c16ba 100755 --- a/plugins/linkedin-studio/scripts/test-runner.sh +++ b/plugins/linkedin-studio/scripts/test-runner.sh @@ -1,16 +1,28 @@ #!/bin/bash # LinkedIn Studio Plugin — Structure Validator -# Validates file existence, frontmatter format, and router completeness +# Validates the REAL v3.1 layout: registration counts (derived dynamically), +# frontmatter shape, hook drift, and plugin.json fields. Counts are asserted +# against the declared contract below, which is kept in sync with the +# CLAUDE.md "## Agents (N)" / "## Commands (N)" headers (cross-checked here) +# and the STATE.md "Telling" block. Adding or removing an agent, command, +# reference, or skill breaks the count-equality and fails the lint — this is +# the registration guard that gates the remediation plan's later steps. +# +# The stat-consistency grep (one magnitude per algorithm effect across the +# tree) is added in remediation Step 3, after reconciliation makes it pass. +# The version-consistency grep is added in Step 21. +# # Usage: bash scripts/test-runner.sh +# bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`. set -e PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$PLUGIN_ROOT" PASS=0 FAIL=0 WARN=0 -# Color output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' @@ -20,6 +32,14 @@ pass() { echo -e "${GREEN}✓${NC} $1"; PASS=$((PASS + 1)); } fail() { echo -e "${RED}✗${NC} $1"; FAIL=$((FAIL + 1)); } warn() { echo -e "${YELLOW}⚠${NC} $1"; WARN=$((WARN + 1)); } +# --- Declared registration contract (the "Telling" block) --- +# Source of truth: CLAUDE.md headers + STATE.md Telling. Bump these together +# with the files when adding/removing an agent, command, reference, or skill. +EXPECT_AGENTS=19 +EXPECT_COMMANDS=26 +EXPECT_REFS=25 +EXPECT_SKILLS=6 + echo "================================================" echo "LinkedIn Studio Plugin — Structure Validator" echo "Plugin root: $PLUGIN_ROOT" @@ -29,8 +49,8 @@ echo "" # --- Section 1: Core Files --- echo "--- Core Files ---" -for f in ".claude-plugin/plugin.json" "CLAUDE.md" "CHANGELOG.md" "docs/DEVELOPMENT-LOG.md" "README.md" "config/REMEMBER.template.md"; do - if [ -f "$PLUGIN_ROOT/$f" ]; then +for f in ".claude-plugin/plugin.json" "CLAUDE.md" "CHANGELOG.md" "README.md" "config/REMEMBER.template.md"; do + if [ -f "$f" ]; then pass "$f exists" else fail "$f MISSING" @@ -39,200 +59,119 @@ done echo "" -# --- Section 2: Agent Files --- -echo "--- Agent Files ---" +# --- Section 2: Registration Counts (dynamic) --- +echo "--- Registration Counts ---" -EXPECTED_AGENTS=( - "engagement-coach" "content-optimizer" "strategy-advisor" "analytics-interpreter" - "content-planner" "network-builder" "content-repurposer" "trend-spotter" - "voice-trainer" "differentiation-checker" "post-feedback-monitor" "video-scripter" - "fact-checker" "persona-reviewer" -) +AGENTS=$(ls agents/*.md 2>/dev/null | wc -l | tr -d ' ') +COMMANDS=$(ls commands/*.md 2>/dev/null | wc -l | tr -d ' ') +REFS=$(ls references/*.md 2>/dev/null | wc -l | tr -d ' ') +SKILLS=$(ls skills/*/SKILL.md 2>/dev/null | wc -l | tr -d ' ') -for agent in "${EXPECTED_AGENTS[@]}"; do - f="agents/${agent}.md" - if [ -f "$PLUGIN_ROOT/$f" ]; then - # Check for YAML frontmatter - if head -1 "$PLUGIN_ROOT/$f" | grep -q "^---"; then - # Check for required fields - if grep -q "^name:" "$PLUGIN_ROOT/$f" && grep -q "^model:" "$PLUGIN_ROOT/$f" && grep -q "^color:" "$PLUGIN_ROOT/$f"; then - pass "$f (frontmatter OK)" - else - warn "$f (missing frontmatter fields)" - fi +assert_count() { + # $1 label, $2 actual, $3 expected + if [ "$2" -eq "$3" ]; then + pass "$1: $2 (expected $3)" + else + fail "$1: $2 (expected $3) — registration drift" + fi +} + +assert_count "agents/*.md" "$AGENTS" "$EXPECT_AGENTS" +assert_count "commands/*.md" "$COMMANDS" "$EXPECT_COMMANDS" +assert_count "references/*.md" "$REFS" "$EXPECT_REFS" +assert_count "skills/*/SKILL.md" "$SKILLS" "$EXPECT_SKILLS" + +# Cross-check the CLAUDE.md declared headers against the contract (doc-drift guard) +DOC_AGENTS=$(grep -oE '^## Agents \([0-9]+\)' CLAUDE.md | grep -oE '[0-9]+' | head -1) +DOC_COMMANDS=$(grep -oE '^## Commands \([0-9]+\)' CLAUDE.md | grep -oE '[0-9]+' | head -1) +if [ "$DOC_AGENTS" = "$EXPECT_AGENTS" ]; then + pass "CLAUDE.md '## Agents ($DOC_AGENTS)' matches contract" +else + fail "CLAUDE.md agents header ($DOC_AGENTS) != contract ($EXPECT_AGENTS)" +fi +if [ "$DOC_COMMANDS" = "$EXPECT_COMMANDS" ]; then + pass "CLAUDE.md '## Commands ($DOC_COMMANDS)' matches contract" +else + fail "CLAUDE.md commands header ($DOC_COMMANDS) != contract ($EXPECT_COMMANDS)" +fi + +echo "" + +# --- Section 3: Agent Frontmatter --- +echo "--- Agent Frontmatter ---" + +for f in agents/*.md; do + if head -1 "$f" | grep -q "^---"; then + if grep -q "^name:" "$f" && grep -q "^description:" "$f"; then + pass "$f (frontmatter OK)" else - fail "$f (no YAML frontmatter)" + fail "$f (missing name:/description:)" fi else - fail "$f MISSING" + fail "$f (no YAML frontmatter)" fi done echo "" -# --- Section 3: Command Files --- -echo "--- Command Files ---" +# --- Section 4: Command Frontmatter --- +echo "--- Command Frontmatter ---" -EXPECTED_COMMANDS=( - "linkedin" "linkedin:setup" "linkedin:post" "linkedin:quick" "linkedin:templates" - "linkedin:pipeline" "linkedin:batch" "linkedin:analyze" "linkedin:audit" - "linkedin:import" "linkedin:report" "linkedin:strategy" "linkedin:authority" - "linkedin:competitive" "linkedin:monetize" "linkedin:profile" - "linkedin:collab" "linkedin:speaking" "linkedin:multiplatform" - "linkedin:ab-test" -) - -for cmd in "${EXPECTED_COMMANDS[@]}"; do - f="commands/${cmd}.md" - if [ -f "$PLUGIN_ROOT/$f" ]; then - if head -1 "$PLUGIN_ROOT/$f" | grep -q "^---"; then - if grep -q "^name:" "$PLUGIN_ROOT/$f" && grep -q "^description:" "$PLUGIN_ROOT/$f"; then - pass "$f (frontmatter OK)" - else - warn "$f (missing frontmatter fields)" - fi +for f in commands/*.md; do + if head -1 "$f" | grep -q "^---"; then + if grep -q "^name:" "$f" && grep -q "^description:" "$f"; then + pass "$f (frontmatter OK)" else - fail "$f (no YAML frontmatter)" + fail "$f (missing name:/description:)" fi else - fail "$f MISSING" + fail "$f (no YAML frontmatter)" fi done echo "" -# --- Section 4: Reference Files --- -echo "--- Reference Files ---" - -EXPECTED_REFS=( - "engagement-frameworks" "collaborations-guide" "algorithm-signals-reference" - "linkedin-growth-playbook-2025-2026" "opportunity-generation" - "linkedin-formats" "ai-content-framework" "articles-strategy-guide" - "first-comment-strategy" "poll-strategy-guide" "newsletter-strategy-guide" - "linkedin-visual-style" "growth-roadmaps" - "thought-leadership-angles" "low-frequency-posting-strategy" - "url-processing-templates" "linkedin-monetization-strategies" - "troubleshooting-guide" "glossary" "ab-testing-framework" -) - -for ref in "${EXPECTED_REFS[@]}"; do - f="references/${ref}.md" - if [ -f "$PLUGIN_ROOT/$f" ]; then - pass "$f exists" - else - fail "$f MISSING" - fi -done - -echo "" - -# --- Section 5: Skill Files --- -echo "--- Skill Files ---" - -for skill in "linkedin-studio" "linkedin-content-creation" "linkedin-analytics" "linkedin-strategy" "linkedin-networking" "linkedin-voice"; do - f="skills/${skill}.md" - if [ -f "$PLUGIN_ROOT/$f" ]; then - pass "$f exists" - else - fail "$f MISSING" - fi -done - -echo "" - -# --- Section 6: Hook Configuration --- +# --- Section 5: Hook Configuration (drift) --- echo "--- Hook Configuration ---" -HOOKS_FILE="$PLUGIN_ROOT/hooks/hooks.json" -if [ -f "$HOOKS_FILE" ]; then +if [ -f "hooks/hooks.json" ]; then pass "hooks/hooks.json exists" - # Validate JSON - if python3 -c "import json; json.load(open('$HOOKS_FILE'))" 2>/dev/null; then - pass "hooks.json is valid JSON" + if python3 hooks/scripts/compile-hooks.py --check >/dev/null 2>&1; then + pass "hooks.json matches compiled template (no drift)" else - fail "hooks.json is INVALID JSON" + fail "hooks.json DRIFT — run: python3 hooks/scripts/compile-hooks.py" fi else fail "hooks/hooks.json MISSING" fi -# Check hook prompt files -for prompt in "content-quality-gate" "voice-guardian" "state-update-reminder" "post-creation-automation"; do - f="hooks/prompts/${prompt}.md" - if [ -f "$PLUGIN_ROOT/$f" ]; then - pass "$f exists" - else - fail "$f MISSING" - fi -done - echo "" -# --- Section 7: Plugin.json Validation --- +# --- Section 6: Plugin.json Validation --- echo "--- Plugin.json Validation ---" -PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json" if python3 -c " import json, sys -with open('$PLUGIN_JSON') as f: +with open('.claude-plugin/plugin.json') as f: data = json.load(f) -required = ['name', 'version', 'auto_discover', 'description'] -for field in required: - if field not in data: - print(f'Missing field: {field}') - sys.exit(1) -if data.get('auto_discover') != True: - print('auto_discover is not true') +required = ['name', 'version', 'description'] +missing = [field for field in required if field not in data] +if missing: + print('Missing fields:', missing) sys.exit(1) -print(f'Version: {data[\"version\"]}') +print('Version:', data['version']) " 2>/dev/null; then - pass "plugin.json structure valid" + pass "plugin.json structure valid (name/version/description)" else fail "plugin.json structure invalid" fi echo "" -# --- Section 8: Router Completeness --- -echo "--- Router Completeness ---" +# --- Section 7: Analytics Source --- +echo "--- Analytics Source ---" -ROUTER="$PLUGIN_ROOT/commands/linkedin.md" -if [ -f "$ROUTER" ]; then - # Check that key commands are mentioned in router - for cmd in "linkedin:setup" "linkedin:post" "linkedin:quick" "linkedin:report" "linkedin:import" "linkedin:ab-test" "linkedin:collab" "linkedin:pipeline" "linkedin:batch"; do - if grep -q "$cmd" "$ROUTER"; then - pass "Router references $cmd" - else - fail "Router MISSING reference to $cmd" - fi - done - - # Check that key agents are mentioned - for agent in "engagement-coach" "content-optimizer" "network-builder" "post-feedback-monitor" "personalization-scorer"; do - if grep -q "$agent" "$ROUTER"; then - pass "Router references $agent" - else - fail "Router MISSING reference to $agent" - fi - done -else - fail "Router file MISSING" -fi - -echo "" - -# --- Section 9: Analytics Structure --- -echo "--- Analytics Structure ---" - -for d in "scripts/analytics/src" "assets/analytics"; do - if [ -d "$PLUGIN_ROOT/$d" ]; then - pass "$d/ directory exists" - else - fail "$d/ directory MISSING" - fi -done - -if [ -f "$PLUGIN_ROOT/scripts/analytics/src/cli.ts" ]; then +if [ -f "scripts/analytics/src/cli.ts" ]; then pass "scripts/analytics/src/cli.ts exists" else fail "scripts/analytics/src/cli.ts MISSING"