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:
parent
6bd263144f
commit
8275159aad
4 changed files with 154 additions and 7 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/<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"
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue