test(linkedin-studio): M0-16 — lint: EXPECT_REFS=26 + no-bare-path + no-pin + SC2 dry-run (R1) + drop 5 ANALYTICS_ROOT pins it caught

This commit is contained in:
Kjell Tore Guttormsen 2026-06-18 13:11:50 +02:00
commit 8275159aad
4 changed files with 154 additions and 7 deletions

View file

@ -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 <YYYY-WXX>
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week <YYYY-WXX>
```
7. **If no imported data exists:** Guide the user to run `/linkedin:import` first. Fall back to the manual data sources below.

View file

@ -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 1113 (2026-06-18)
### Environment reality vs. plan assumptions

View file

@ -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 814).
#
# 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/<x>/) 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"

View file

@ -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 <file>
ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week <W>
ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period <P> --metric <M>
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" import <file>
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report --week <W>
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" trends --period <P> --metric <M>
```
**Storage structure:**