diff --git a/agents/analytics-interpreter.md b/agents/analytics-interpreter.md index f2c2d47..8fa1c01 100644 --- a/agents/analytics-interpreter.md +++ b/agents/analytics-interpreter.md @@ -51,11 +51,11 @@ The plugin has a built-in analytics pipeline. Always check for imported data fir 4. **Load audience context:** Read `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/audience-insights/demographics.md` for audience composition. 5. **Run trend analysis:** ```bash - ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period month --metric impressions + node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period month --metric impressions ``` 6. **Generate fresh report (report mode):** ```bash - ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week + node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week ``` 7. **If no imported data exists:** Guide the user to run `/linkedin:import` first. Fall back to the manual data sources below. diff --git a/docs/m0/log.md b/docs/m0/log.md index ef78263..9316e2b 100644 --- a/docs/m0/log.md +++ b/docs/m0/log.md @@ -71,6 +71,24 @@ left are the three shipped exclusions; full lint Failed:1 = EXPECT_REFS only. *migrated* data classes only, so it does not flag personas. Track as a follow-up if a full personas externalization is wanted later. +### Step 16 — lint extended 74→81 checks; the no-pin guard caught 5 real survivors + +Added to `test-runner.sh`: `EXPECT_REFS` 25→26 + "delta = exactly +1 (data-path-convention.md)" +assert (m3/m11); **Section 13** (no command/agent/skill/hook prose references a migrated +user-data path in-plugin — bare or `${CLAUDE_PLUGIN_ROOT}`-pinned — R1; + the no-`ANALYTICS_ROOT`- +pin sibling M3; + the SC2 `git status --porcelain --ignored` dry-run), each with a +non-vacuity self-test mirroring Section 8; **Section 14** assertion-count anti-erosion +floor (pre-M0 baseline 74, count may only grow — SC6 / assumption 3). + +**The no-pin guard earned its keep on first run:** it flagged **5 surviving in-plugin +`ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics"` pins** that Step 11 missed — +`agents/analytics-interpreter.md` (×2) + `skills/linkedin-analytics/SKILL.md` (×3). Step 11 +scoped the pin-drop to the *commands* (`import.md`/`report.md`); the agent + skill invoke the +CLI directly and kept the pin, which would force the analytics CLI to read in-plugin even +after the default flipped external (the exact M3 failure). Dropped all 5 (surgical: removed +only the `ANALYTICS_ROOT=… ` prefix, kept the `node --import tsx` form — no CLI-style change), +matching how Step 11 left the commands. Lint now **81/0**. + ## Session 3 — Steps 11–13 (2026-06-18) ### Environment reality vs. plan assumptions diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index 3fd252d..16c3a21 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -16,7 +16,11 @@ # command was cleaned of survives in the reference it renders from) in S12; the # `$`-safety guard (no untrusted value reaches a String.replace replacement STRING # in state-updater.mjs — proven behaviorally, coverage-complete, self-testing) in -# S13. All five are live below (Sections 8, 9, 10, 11 and 12). +# S13. The external data-dir convention guard (M0: no command/agent/skill/hook prose +# references a migrated user-data path in-plugin — bare or ${CLAUDE_PLUGIN_ROOT}-pinned; +# + no ANALYTICS_ROOT in-plugin pin; + an SC2 dry-run that the tree holds no in-plugin +# user data) in Section 13, each with a non-vacuity self-test; the assertion-count +# anti-erosion floor (SC6) in Section 14. All are live below (Sections 8–14). # # Usage: bash scripts/test-runner.sh # bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`. @@ -43,8 +47,12 @@ warn() { echo -e "${YELLOW}⚠${NC} $1"; WARN=$((WARN + 1)); } # with the files when adding/removing an agent, command, reference, or skill. EXPECT_AGENTS=19 EXPECT_COMMANDS=29 -EXPECT_REFS=25 +EXPECT_REFS=26 EXPECT_SKILLS=6 +# Pre-M0 references/ baseline was 25; M0 adds exactly one ref doc +# (references/data-path-convention.md). The "delta = +1" assert below proves the +# bump was a single intended addition, not a larger jump masking an extra doc (m3/m11). +REFS_BASELINE_PRE_M0=25 echo "================================================" echo "LinkedIn Studio Plugin — Structure Validator" @@ -87,6 +95,14 @@ assert_count "commands/*.md" "$COMMANDS" "$EXPECT_COMMANDS" assert_count "references/*.md" "$REFS" "$EXPECT_REFS" assert_count "skills/*/SKILL.md" "$SKILLS" "$EXPECT_SKILLS" +# M0 references/ delta = exactly +1, and the one added doc is the D3 convention. +# Guards against the bump silently absorbing an incidental extra ref doc (m3/m11). +if [ "$EXPECT_REFS" -eq $((REFS_BASELINE_PRE_M0 + 1)) ] && [ -f references/data-path-convention.md ]; then + pass "references/ delta = exactly +1 (M0 added data-path-convention.md; ${REFS_BASELINE_PRE_M0}→${EXPECT_REFS})" +else + fail "references/ delta != +1 — M0 must add exactly one ref doc (the D3 convention), not $((EXPECT_REFS - REFS_BASELINE_PRE_M0))" +fi + # 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) @@ -458,6 +474,119 @@ fi echo "" +# --- Section 13: External Data-Dir Convention (R1 — M0) --- +echo "--- External Data-Dir Convention ---" + +# After the M0 migration (user data lives under ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/ +# linkedin-studio}/), no command/agent/skill/hook prose may reference a MIGRATED +# user-data path in-plugin — neither bare (assets//) nor ${CLAUDE_PLUGIN_ROOT}- +# prefixed — or a command writes the external dir while Claude follows stale in-plugin +# prose (R1, brief §12.6). Shipped read-only assets keep ${CLAUDE_PLUGIN_ROOT} and are +# exempt: the analytics README, every *-template.md seed, assets/checklists/, +# assets/quick-post-resources.md, the shipped assets/templates/*, config/*.template.*. +# config/personas.local.md is a deliberate in-plugin fallback library (un-migrated, out +# of M0 scope) and is NOT in the class. Scope = the actionable prose dirs (commands/ +# agents/skills/hooks-prompts); references/ is documentation (the convention doc +# legitimately names scaffold paths) and is excluded. +BARE_DATA='(\$\{CLAUDE_PLUGIN_ROOT\}/)?assets/(voice-samples|analytics|drafts|plans|audience-insights|examples|frameworks|case-studies|network)/|(\$\{CLAUDE_PLUGIN_ROOT\}/)?assets/repurposing-tracker\.md|(\$\{CLAUDE_PLUGIN_ROOT\}/)?assets/templates/my-post-templates\.md|(\$\{CLAUDE_PLUGIN_ROOT\}/)?config/user-profile\.local\.md' +BARE_EXEMPT='README\.md|-template\.md' + +# Non-vacuity self-test (mirrors Section 8): the full criterion is regex-match AND +# not-exempt. Positives MUST be violations; negatives (external token + every shipped +# exemption + the out-of-scope personas lib) MUST NOT be. +B13_OK=1 +while IFS= read -r probe; do + [ -z "$probe" ] && continue + if echo "$probe" | grep -qE "$BARE_DATA" && ! echo "$probe" | grep -qE "$BARE_EXEMPT"; then :; else + B13_OK=0; echo " non-vacuity FAIL: in-plugin data path not flagged -> $probe" + fi +done <<'POSITIVE13' +assets/voice-samples/authentic-voice-samples.md +${CLAUDE_PLUGIN_ROOT}/assets/analytics/posts/ +assets/drafts/queue.json +assets/templates/my-post-templates.md +assets/frameworks/ai-maturity-model.md +assets/repurposing-tracker.md +config/user-profile.local.md +POSITIVE13 +while IFS= read -r probe; do + [ -z "$probe" ] && continue + if echo "$probe" | grep -qE "$BARE_DATA" && ! echo "$probe" | grep -qE "$BARE_EXEMPT"; then + B13_OK=0; echo " false-positive FAIL: legitimate path flagged -> $probe" + fi +done <<'NEGATIVE13' +${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/ +assets/analytics/README.md +assets/frameworks/framework-template.md +assets/case-studies/case-study-template.md +assets/checklists/quality-scorecard.md +assets/quick-post-resources.md +config/user-profile.template.md +config/personas.local.md +NEGATIVE13 +if [ "$B13_OK" -eq 1 ]; then + pass "no-bare-path self-test: 7 in-plugin data paths caught, 8 legitimate/exempt forms ignored" +else + fail "no-bare-path self-test failed — the R1 criterion no longer enforces in-plugin data-path detection" +fi + +BARE_HITS=$(grep -rnE "$BARE_DATA" commands/ agents/ skills/ hooks/prompts/ 2>/dev/null | grep -vE "$BARE_EXEMPT" || true) +if [ -z "$BARE_HITS" ]; then + pass "no command/agent/skill/hook prose references a migrated user-data path in-plugin (R1)" +else + fail "in-plugin user-data path(s) in prose — route via \${LINKEDIN_STUDIO_DATA:-\$HOME/.claude/linkedin-studio}/ (see references/data-path-convention.md):" + echo "$BARE_HITS" +fi + +# Sibling no-pin guard (M3): the no-bare-path grep cannot catch an ANALYTICS_ROOT alias +# pinned to the in-plugin analytics dir — Step 11 dropped these; forbid their return. +PIN='ANALYTICS_ROOT=.{0,2}\$\{CLAUDE_PLUGIN_ROOT\}/assets/analytics' +PIN_OK=1 +echo 'ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics"' | grep -qE "$PIN" || { PIN_OK=0; echo " non-vacuity FAIL: in-plugin pin not caught"; } +if echo 'ANALYTICS_ROOT="${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics"' | grep -qE "$PIN"; then PIN_OK=0; echo " false-positive FAIL: external alias caught"; fi +if [ "$PIN_OK" -eq 1 ]; then + pass "no-pin self-test: in-plugin ANALYTICS_ROOT pin caught, external alias ignored" +else + fail "no-pin self-test failed — the M3 pin criterion is not enforced" +fi +PIN_HITS=$(grep -rnE "$PIN" commands/ agents/ skills/ hooks/prompts/ 2>/dev/null || true) +if [ -z "$PIN_HITS" ]; then + pass "no prose pins ANALYTICS_ROOT to the in-plugin analytics dir (M3)" +else + fail "ANALYTICS_ROOT pinned to in-plugin analytics — drop the pin (external default applies):" + echo "$PIN_HITS" +fi + +# SC2 dry-run: the migrated in-plugin user-data classes (.gitignore) must hold NO data +# files — after M0 they live external. --ignored surfaces gitignored stragglers a plain +# porcelain would hide; filtered to the data classes, it must be empty. Catches a flow +# (or a stray file) that wrote user data back into the plugin tree (SC2). personas is +# excluded (un-migrated, out of M0 scope — consistent with Section 13's class). +SC2_CLASSES='assets/analytics/(exports|posts|weekly-reports|monthly-reports)/|assets/analytics/content-history\.md|assets/drafts/queue\.json|assets/drafts/week-|assets/voice-samples/[^ ]*\.local\.md|config/user-profile\.local\.md' +SC2_DIRT=$(git status --porcelain --ignored 2>/dev/null | grep -E "$SC2_CLASSES" || true) +if [ -z "$SC2_DIRT" ]; then + pass "SC2 dry-run: no in-plugin user-data files (analytics/drafts/voice/profile) in the tree" +else + fail "SC2 violation — migrated user data sits in the plugin tree (should be external):" + echo "$SC2_DIRT" +fi + +echo "" + +# --- Section 14: Assertion-Count Anti-Erosion (SC6) --- +# The lint self-modifies its own checks, so a green run could mask a silently dropped +# assertion. Pin the pre-M0 total (74 pass()+fail() invocations) as a floor; the count +# may only grow (brief-reviewer assumption 3). Runs last so TOTAL_CHECKS sees every prior check. +ASSERT_BASELINE_PRE_M0=74 +TOTAL_CHECKS=$((PASS + FAIL)) +if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_PRE_M0" ]; then + pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= pre-M0 baseline $ASSERT_BASELINE_PRE_M0" +else + fail "assertion count $TOTAL_CHECKS < pre-M0 baseline $ASSERT_BASELINE_PRE_M0 — a check was silently removed" +fi + +echo "" + # --- Summary --- echo "================================================" echo "RESULTS" diff --git a/skills/linkedin-analytics/SKILL.md b/skills/linkedin-analytics/SKILL.md index 3d6e7ea..3574699 100644 --- a/skills/linkedin-analytics/SKILL.md +++ b/skills/linkedin-analytics/SKILL.md @@ -53,9 +53,9 @@ Node.js CLI tool for parsing LinkedIn CSV exports into structured JSON. **CLI usage:** ```bash -ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" import -ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week -ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period

--metric +node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" import +node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week +node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period

--metric ``` **Storage structure:**