From a413279c58ba10d287f122448f7abc1d39654106 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Thu, 18 Jun 2026 20:56:47 +0200 Subject: [PATCH] =?UTF-8?q?fix(linkedin-studio):=20S14=20harden=20import?= =?UTF-8?q?=20=E2=80=94=20drop=20baseline/metadata=20over-promise,=20prose?= =?UTF-8?q?=20now=20matches=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit import.md repeatedly promised baseline comparison, rolling 4-week averages, and baselines.json/metadata.json the importer "creates" — but no code anywhere writes either file; detectAlerts flags intra-batch std-dev from the batch's own mean, not a stored baseline. Step 5b's `cat baselines.json` was always empty, so the command fell forever to "first import — baselines will be established" on every import. Rewrote 5 sections (Step 5, 5b, 7, State Tracking, Reference Files) to the truth: real CLI fields + YYYY-MM-DD- filename, honest intra-batch surfacing, cross-week analysis deferred to /linkedin:report. Verified with a grounded run against a throwaway fixture. Axes a/b/c/d all PASS post-fix; lint 81/0/0, counts 29/19 unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE --- commands/import.md | 127 +++++++++++++++--------------------------- docs/hardening/log.md | 56 +++++++++++++++++++ 2 files changed, 101 insertions(+), 82 deletions(-) diff --git a/commands/import.md b/commands/import.md index 9580a0c..3a892e2 100644 --- a/commands/import.md +++ b/commands/import.md @@ -130,94 +130,59 @@ If importing multiple files, run the command for each file sequentially. ## Step 5: Capture and Present Results -The CLI will output: -- Number of posts imported -- Date range covered (earliest to latest post) -- Any duplicate posts detected -- Anomalies or alerts detected +The CLI prints (see `cli.ts` `handleImport`): +- `Posts imported:` — count of valid rows (rows with an empty title or an unparseable date are skipped, each with a `Warning:` line) +- `Date range:` — earliest to latest post in the batch +- `Batch ID:` and `Saved to: posts/` — the batch file written +- `Saves entered:` — only when the CSV carried a `Saves` column (manual entry) +- An anomaly block — either `Immediate alerts detected:` with 🔴/⚠️/ℹ️ spike/drop lines, or `No anomalies detected in imported data.` -**Parse the output** and present a summary: +**Surface the CLI's output to the user** — for example: ``` -Import completed successfully! +Import successful! +───────────────────────────────────── +Posts imported: 42 +Date range: 2025-12-01 to 2026-01-29 +Saved to: posts/2025-12-01-batch-a1b2c3d4.json +Saves entered: 1,204 across 18 post(s) (manual) -Summary: -- Posts imported: 42 -- Date range: 2025-12-01 to 2026-01-29 -- Duplicates skipped: 3 -- Anomalies detected: 2 posts with unusually high engagement - -Alerts: -- Post "AI agents are eating..." (2026-01-15): 340% above baseline impressions -- Post "The future of no-code..." (2026-01-22): Viral threshold reached (10k+ impressions) - -Data saved to: -- ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/YYYY-WXX.json +Immediate alerts detected: +───────────────────────────────────── +ℹ️ [INFO] Post "AI agents are eating..." has unusually high impressions: 21,400 (2.4 std deviations above mean) ``` -### Step 5b: Import Analysis & Anomaly Detection +The saved file is named `posts/YYYY-MM-DD-.json` (the batch's earliest post date + a short batch id), not by ISO week. -After successful import, automatically analyze the imported data for anomalies and patterns. +### Step 5b: Surface the Anomalies the Importer Detected -**Anomaly Detection:** -Compare the imported week's data against existing baselines (if available from previous imports): +The import CLI runs **intra-batch** anomaly detection during Step 4 (`detectAlerts` +in `cli.ts`): for the just-imported batch it flags any post whose impressions +deviate sharply — by standard deviation — from *that batch's own mean*, printing +either `Immediate alerts detected:` (🔴/⚠️/ℹ️ spike/drop lines) or +`No anomalies detected in imported data.` -1. **Engagement anomalies:** - - Any post with >3x average impressions -> flag as "breakout post" - - Any post with <0.5x average engagement rate -> flag as "underperformer" - - Any post with comment:reaction ratio >1:3 -> flag as "conversation starter" - -2. **Pattern recognition:** - - Most successful day of week (by average impressions) - - Most successful format (if detectable from post content) - - Posting frequency vs. previous weeks - -**Read baselines for comparison:** -```bash -cat ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/baselines.json 2>/dev/null -``` - -**If baselines exist**, compare each imported post's metrics against baseline means. If no baselines exist yet, note that this is the first import and baselines will be established. +Surface those lines as-is, and state the scope honestly: a flagged post stands out +**among the posts in this export**, not against a stored historical baseline — the +importer keeps no baseline file. Cross-week comparison is Step 6's job. **Present as:** ``` -### Import Analysis — YYYY-WXX +### Import Summary — -X posts imported (Y new, Z updated) +X posts imported (Y skipped: empty title or unparseable date) -#### Standout Posts -Breakout: "[hook text...]" — X impressions (3.2x your average) -Conversation Starter: "[hook text...]" — X comments (ratio 1:2.5) +#### Standout in this batch +ℹ️ "[hook text...]" — 21,400 impressions (2.4 std dev above this batch's mean) +⚠️ "[hook text...]" — 180 impressions (2.1 std dev below this batch's mean) -#### Patterns Detected -- Best day: Tuesday (avg 2,100 impressions vs. 1,400 other days) -- Best time: Posts before 8 AM outperformed by 35% -- Format winner: Listicles averaged 40% more engagement - -#### Baseline Update -Your rolling 4-week averages have been updated: -- Impressions: X -> Y (change Z%) -- Engagement rate: X% -> Y% (change Z%) +(or: "No standout deviations within this batch.") ``` -**If this is the first import (no baselines):** -``` -### Import Analysis — YYYY-WXX - -X posts imported (first import — baselines will be established) - -#### Initial Observations -Top post: "[hook text...]" — X impressions -Most discussed: "[hook text...]" — X comments - -#### Baselines Established -Your initial baselines are now set: -- Avg impressions per post: X -- Avg engagement rate: X% -- Avg comments per post: X - -Import 2-3 more weeks of data for meaningful trend analysis. -``` +Cross-week analysis — best day of week, format performance, week-over-week trend, +rolling averages — is **not** computed here; it is produced by `/linkedin:report` +(Step 6), which reads the full post history via the `trends`/`heatmap` CLI. Defer +that analysis to Step 6 rather than restating it. ## Step 6: Analytics-to-Strategy Feedback Loop @@ -266,12 +231,12 @@ Write the updated state file Present next steps using AskUserQuestion based on the analysis results: -**If data shows declining engagement** (current < baseline by >15%): +**If the report's trend is down** (impressions or engagement trending DOWN): - "Run /linkedin:report for full weekly breakdown" - "Run content audit to review strategy" - "Analyze your top post to understand what worked" -**If data shows strong performance** (current > baseline by >15%): +**If the report's trend is up** (impressions or engagement trending UP): - "Run /linkedin:report for the full numbers" - "Create more content in your top format" - "Draft your next post while insights are fresh" @@ -318,15 +283,13 @@ If the import fails: ## Reference Files The import system creates: -- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/YYYY-WXX.json` - Weekly post data -- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/metadata.json` - Import tracking and baseline metrics -- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/baselines.json` - Statistical baselines for anomaly detection +- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/YYYY-MM-DD-.json` - one JSON batch per import (earliest post date + short batch id) + +Weekly and monthly report files (under `weekly-reports/` and `monthly-reports/`) are created separately by `/linkedin:report`, not by import. ## State Tracking -After import, the system automatically: -- Updates baseline metrics (mean, median, std dev for each metric) -- Detects and flags anomalies (posts >2 sigma from baseline) -- Organizes posts by ISO week for trend analysis -- Preserves historical data (never overwrites existing weeks) -- Updates `last_import_date` and `last_import_week` in state file +After import: +- A new batch file `posts/YYYY-MM-DD-.json` is written, one per import — existing batch files are never overwritten; `loadAllPosts` deduplicates by post id at read time (latest import wins) +- Intra-batch spike/drop alerts are computed and surfaced (std deviation from the batch's own mean — no persisted baseline) +- `last_import_date` and `last_import_week` are updated in the state file (`~/.claude/linkedin-studio.local.md`, see Step 6b) diff --git a/docs/hardening/log.md b/docs/hardening/log.md index 6d74c78..57d05ae 100644 --- a/docs/hardening/log.md +++ b/docs/hardening/log.md @@ -1124,3 +1124,59 @@ functionally safe but mis-labelled "(Step 3b)". Root cause: conflating "the phas - Disposition: **FIXED** (1 edit in `commands/calendar.md`) · 0 deferrals · axes a/b/c/d all PASS post-fix. --- + +## Session 14 — Hardening (`import`) + +> S14 status: read-and-show of the analytics CLI (`csv-parser.ts`, `cli.ts handleImport`, +> `storage.ts`, `alerts.ts`) BEFORE simulate, then a GROUNDED run against a throwaway fixture +> (`ANALYTICS_ROOT=/private/tmp/…`, 5-row CSV, discarded after). Class agreed steps 1–2: +> **analytics ingest** — CSV export → structured weekly JSON via `cli.ts import`, delegating all +> analysis to `/linkedin:report` (Step 6). **NOT post-emitting** → the feed-format predicates +> (hook 110–140, length band, no-body-link, buzzwords, rotation) do NOT apply. Axis-a/d heavy +> (path-seam consistency + graceful degradation + the over-promise). + +### /linkedin:import — CSV export → structured weekly JSON, intra-batch anomaly surface, analysis delegated to report + +**WHAT VERIFIED CLEAN (no change).** +- Path-seam (axis-a): `getAnalyticsRoot()` (`storage.ts:67–72`) → `getDataRoot("analytics")` + (`:54–59`) = `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics` = the exact path + the prose tells the user to use (`import.md:37,62,91`); identical `LINKEDIN_STUDIO_DATA` override. + `ANALYTICS_ROOT` back-compat override (`storage.ts:68`) confirmed (validates the throwaway isolation). +- saves/dwell honesty (axis-c): grounded run → EU `"5.234"`→5234, US `"8,710"`→8710; + `parseOptionalCount` (`csv-parser.ts:71–85`) kept saves 42/128 on data rows, **blank → no `saves` + field** (unknown, not 0); `engagementRate` excludes saves (`:203–208`); **0 dwell field** anywhere. + README boundary blockquote (`import.md:26–30`) intact. +- Wiring (axis-d): `/linkedin:report` → `commands/report.md` present; **0** `subagent_type` + (CLI-delegating, correct); state-file Step 6b = `~/.claude/linkedin-studio.local.md`. +- Degradation (axis-d): `npm install` pre-step (`:114–121`) covers fresh-clone `ERR_MODULE_NOT_FOUND`; + file-not-found exit 1 (`cli.ts:67–71`) ↔ Error Handling; empty title (file line 5) + bad date + (line 6) **skipped** in the run (`csv-parser.ts:186–195`). + +**THE FIX (1 finding, 5 surgical edits, all in `commands/import.md`).** +1. [REWORK · axis-a/d] **Over-promise: baseline / metadata infrastructure the code never had.** + import.md repeatedly promised baseline comparison + rolling 4-week averages + a `baselines.json` / + `metadata.json` the importer "creates" + ">2 sigma from baseline" alerts. Grep over the whole repo + (`scripts/analytics/src` + all `*.mjs/ts/py/js`) = **NONE writes either file**; `saveBatch` writes + only `posts/*.json` (`storage.ts:157`); `detectAlerts` (`alerts.ts:18–69`) flags **intra-batch** + std-dev from the batch's own mean, not a stored baseline. Concrete bug: Step 5b's + `cat baselines.json 2>/dev/null` is always empty → the command falls forever to "first import — + baselines will be established", on every import. Fix: rewrote 5 sections to the truth — Step 5 + (real CLI fields + `YYYY-MM-DD-.json` filename; dropped "Duplicates skipped" / "340% above + baseline" / "Viral threshold"), Step 5b (honest intra-batch surfacing; cross-week analysis deferred + to `/linkedin:report`), Step 7 (Next-Steps conditioned on the report's trend, not a local baseline), + State Tracking + Reference Files (no `baselines.json` / `metadata.json`; correct batch filename). + +**VERIFY.** +- Re-grepped the final file: `metadata.json` / `YYYY-WXX.json` (filename) / "Duplicates skipped" / + "340%" / "Viral threshold" all GONE; the only `YYYY-WXX` left is `:226` (`last_import_week` ISO-week + state field — legitimate); the 3 remaining "baseline" hits are the new honest negations ("keeps no + baseline file", "no persisted baseline"). +- `bash scripts/test-runner.sh` → `Passed: 81 · Failed: 0 · Warnings: 0`, **exit 0**; counts **29/19** + unchanged (.md-only edit, node/analytics tests not triggered). +- Grounded simulation passes the failing axis: the rewritten prose matches the observed CLI output + verbatim (filename `2026-06-02-batch-17.json`, "No anomalies detected in imported data.", + "Saves entered: 170 across 2 post(s) (manual)"). +- Disposition: **FIXED** (5 edits, all in `commands/import.md`) · 0 deferrals · axes a/b/c/d all PASS + post-fix. + +---