#!/bin/bash # LinkedIn Studio Plugin — Structure Validator # 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) was added in remediation Step 3; the version-consistency grep in # Step 21. Both are live below (Sections 8 and 9). # # 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 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color 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=27 EXPECT_REFS=25 EXPECT_SKILLS=6 echo "================================================" echo "LinkedIn Studio Plugin — Structure Validator" echo "Plugin root: $PLUGIN_ROOT" echo "================================================" echo "" # --- Section 1: Core Files --- echo "--- Core Files ---" 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" fi done echo "" # --- Section 2: Registration Counts (dynamic) --- echo "--- Registration Counts ---" 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 ' ') 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 (missing name:/description:)" fi else fail "$f (no YAML frontmatter)" fi done echo "" # --- Section 4: Command Frontmatter --- echo "--- Command Frontmatter ---" 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 (missing name:/description:)" fi else fail "$f (no YAML frontmatter)" fi done echo "" # --- Section 5: Hook Configuration (drift) --- echo "--- Hook Configuration ---" if [ -f "hooks/hooks.json" ]; then pass "hooks/hooks.json exists" 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 DRIFT — run: python3 hooks/scripts/compile-hooks.py" fi else fail "hooks/hooks.json MISSING" fi echo "" # --- Section 6: Plugin.json Validation --- echo "--- Plugin.json Validation ---" if python3 -c " import json, sys with open('.claude-plugin/plugin.json') as f: data = json.load(f) 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('Version:', data['version']) " 2>/dev/null; then pass "plugin.json structure valid (name/version/description)" else fail "plugin.json structure invalid" fi echo "" # --- Section 7: Analytics Source --- echo "--- Analytics Source ---" if [ -f "scripts/analytics/src/cli.ts" ]; then pass "scripts/analytics/src/cli.ts exists" else fail "scripts/analytics/src/cli.ts MISSING" fi echo "" # --- Section 8: Algorithm-Stat Consistency --- echo "--- Algorithm-Stat Consistency ---" # The single source of truth for algorithm magnitudes is # references/algorithm-signals-reference.md. After the Phase-0 reconciliation, # stale/competing magnitudes — the retired engagement-coefficient folklore, the # unpublishable model params/brand, and the deployment date — must not reappear # anywhere else (cite the reference, do not restate). This enforces "one magnitude # per algorithm effect" by forbidding EVERY retired-class value from returning, so # the same grep that defines the Phase-0 Success Criterion fails on any survivor. # # S9 rebuild: the S8 list forbade only the two S7-named strings and went green # over six more survivors (the coefficient system in analytics-interpreter/ # content-optimizer/pipeline/glossary, the playbook 15x/5x, the 150B model). This # list is rebuilt to the FULL criterion. Forbidden classes (each maps to a # canonical statement in the reference): # - Carousel-rate folklore: 6.6% / 6.60% / 1.92% → reference: "~7% top format" # - Link-penalty folklore: 40-50% / 25-40% / -40-60% → reference: one ~38% correlational band # - Comment-multiplier folklore: "15x more reach/algorithmic", "5x more effective/ # less valuable/reach than" → reference: order only, comment ≈ 2x a like # - Video-multiplier folklore: "5x more conversations" → reference: video declining, no multiplier # - Engagement-coefficient system: 7-9x, 2.5x, 0.2x, (10x), (8x), "10x weight" # → reference: "never hard coefficients to optimize against" # - Model params/brand/date: the PATTERN CLASS [0-9]+[ -]?(B|billion)?[ -]?param # (covers 150-parameter / 150B param / 150 billion param) / 360Brew / January 2026 # → reference: "Not publishable as fact" # # S10: the model-precision token is now the pattern CLASS, not a literal-token # list. S9 forbade only "150 ?B param|150 billion param"; a hyphenated # "150-parameter" (no "B") slipped both the discovery grep and the lint, surviving # in glossary.md:10. The criterion is "no asserted model precision in ANY surface # form", so the lint now enforces the shape (a number adjacent to "param"), not an # enumeration. An adjacent digit is REQUIRED, so legitimate "param" uses with no # leading number — "Language parameter", "parameterized", "different parameters", # "«parametere»", "175-milliarders parametermodell" — do not match. # Bare "10x"/"15x"/"5x" are deliberately NOT forbidden — they carry legitimate # uses (collaboration "10x your reach" hyperbole, "5x5x5", posting cadence, pixel # dims like 1080x1350), so each token targets the retired *phrasing*, not the bare # number. # # Scope covers every dir the criterion's grep covers, including assets/checklists/ # (the 360Brew survivor lived there, outside the S8 scan), assets/templates/, and # CHANGELOG.md (S10: the 360Brew/January-2026 survivor lived there, outside the S9 # scope). assets/{templates,checklists}/ — not all of assets/ — keeps the scan off # gitignored runtime data (assets/analytics/, assets/drafts/, voice-samples/). STALE_STATS='40-50%|25-40%|6\.6%|6\.60%|1\.92%|15x more reach|15x more algorithmic|5x more effective|5x less valuable|5x more reach than|5x more conversations|7-9x|2\.5x|0\.2x|\(10x\)|\(8x\)|10x weight|-40-60%|[0-9]+[ -]?(B|billion)?[ -]?param|360Brew|January 2026' # Non-vacuity self-test (S10). A grep criterion is only meaningful if it actually # MATCHES the forbidden forms and does NOT match legitimate ones. S7→S9 each # shipped a lint that passed green while a survivor slipped, because the proof was # run once by hand and never committed — so a hyphenated "150-parameter" form was # never re-checked. This makes the proof PERMANENT: it runs on every invocation # BEFORE the real scan, so narrowing STALE_STATS back to a literal-token list fails # the suite instead of silently certifying an unenforced criterion. The positive # set covers all three model-precision surface forms (incl. the exact S10 # "150-parameter" survivor); the negative set covers the legitimate "param"/x uses # that live in the tree today. SELFTEST_OK=1 while IFS= read -r probe; do [ -z "$probe" ] && continue if ! echo "$probe" | grep -qE "$STALE_STATS"; then SELFTEST_OK=0; echo " non-vacuity FAIL: forbidden form not caught -> $probe" fi done <<'POSITIVE' 40-50% link penalty 6.6% carousel rate 1.92% reach 15x more reach 5x more conversations 7-9x weight (10x) coefficient 10x weight 150-parameter foundation model 150B parameter foundation model 150 billion parameter model 360Brew January 2026 algorithm update POSITIVE while IFS= read -r probe; do [ -z "$probe" ] && continue if echo "$probe" | grep -qE "$STALE_STATS"; then SELFTEST_OK=0; echo " false-positive FAIL: legitimate form caught -> $probe" fi done <<'NEGATIVE' 5x5x5 pre-posting method post 3x per week 1080x1350 pixels 10x your reach Language parameter (configurable) parameterized content-gatekeeper Start over with different parameters 175-milliarders parametermodell NEGATIVE if [ "$SELFTEST_OK" -eq 1 ]; then pass "STALE_STATS self-test: 13 forbidden forms caught, 8 legitimate forms ignored" else fail "STALE_STATS self-test failed — the lint no longer enforces the full criterion" fi STAT_HITS=$(grep -rnE "$STALE_STATS" references/ commands/ skills/ hooks/prompts/ agents/ assets/templates/ assets/checklists/ CLAUDE.md README.md CHANGELOG.md .claude-plugin/plugin.json 2>/dev/null | grep -v 'algorithm-signals-reference' || true) if [ -z "$STAT_HITS" ]; then pass "no stale algorithm magnitudes / model brand outside the canonical reference" else fail "stale algorithm stat(s) reintroduced — cite algorithm-signals-reference.md instead:" echo "$STAT_HITS" fi echo "" # --- Section 9: Version Consistency --- echo "--- Version Consistency ---" # Single source of truth for the plugin version: .claude-plugin/plugin.json. # Its value must be declared identically in the README badge, the CLAUDE.md # header, and the CHANGELOG top entry. Historical references to older versions # (CHANGELOG history, the README version-history table, "vX added Y" prose) are # NOT checked here — only the current-version DECLARATIONS must agree. VERSION=$(python3 -c "import json; print(json.load(open('.claude-plugin/plugin.json'))['version'])" 2>/dev/null) if [ -z "$VERSION" ]; then fail "could not read version from plugin.json" else pass "plugin.json version: $VERSION" if grep -q "version-${VERSION}-blue" README.md; then pass "README badge declares v$VERSION" else fail "README badge does not declare v$VERSION (expected version-${VERSION}-blue)" fi if grep -q "LinkedIn Studio Plugin (v${VERSION})" CLAUDE.md; then pass "CLAUDE.md header declares v$VERSION" else fail "CLAUDE.md header does not declare (v$VERSION)" fi if grep -q "^## \[${VERSION}\]" CHANGELOG.md; then pass "CHANGELOG has a [$VERSION] entry" else fail "CHANGELOG missing a [$VERSION] entry" fi fi echo "" # --- Summary --- echo "================================================" echo "RESULTS" echo "================================================" echo -e "${GREEN}Passed: $PASS${NC}" echo -e "${RED}Failed: $FAIL${NC}" echo -e "${YELLOW}Warnings: $WARN${NC}" echo "" if [ $FAIL -eq 0 ]; then echo -e "${GREEN}All structural checks passed!${NC}" exit 0 else echo -e "${RED}$FAIL check(s) failed. Review above.${NC}" exit 1 fi