feat(linkedin-studio): N17 — baseline-motor (median + variansbånd + minimum-N-refusal) [skip-docs]

Every reading now leads with "vs your own baseline", and no verdict is given
when N is too small to carry one.

- stats.ts: median + medianAbsoluteDeviation (robust pair; mean/stddev stay for
  the alert engine, which wants outlier sensitivity), rollingBaseline with a
  10-post positional window, median ± 1·MAD band floored at 0, and a typed
  insufficient-data refusal below MIN_BASELINE_N=5. readAgainstBaseline returns
  above/within/below-band, or no-verdict when the baseline was refused.
- baselineByGroup + buildBaselineBlock: per-format/per-pillar baselines, each
  judged on its own N; the reported period is excluded from its own baseline and
  compared on its median, not its mean.
- queue-join.ts (new): read-only date join supplying format/pillar from the post
  queue. Every ambiguity resolves to unlabelled, an entry labels at most one
  post, and a missing/broken queue degrades to no labels.
- weekly/monthly reports attach the block unconditionally (refusal included);
  optional in the types, so pre-N17 reports load unchanged.
- CLI: report output leads with the baseline; new `baseline [--by format|pillar]`
  verb with coverage reporting.
- report.md leads with baseline framing and prints the code's reading rather
  than judging the band by eye; WoW loses to the baseline on disagreement.
  analyze.md Step 2a tests whether the drop is real before diagnosing it.

TDD: 58 analytics tests written red first (144 -> 202). test-runner Section 16x,
23 unconditional checks + self-test (247 -> 270; anti-erosion floor 228 -> 251).
tsc clean. All suites green: trends 300, brain 134, editions 72,
specifics-bank 45, contract-gate 33, hooks 191, tests 35, render 60.

Also closes the OKF phase-4 scope follow-up in docs/okf-ingestion/plan.md §8
(coord round 2026-07-25): phase 4 tracks the contract, parse is in scope, and
our claim on read_concept/navigate_bundle is withdrawn as unnecessary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 20:50:45 +02:00
commit e2ad190dda
15 changed files with 1723 additions and 14 deletions

View file

@ -38,7 +38,34 @@ Use AskUserQuestion to understand the situation:
## Step 2: Gather Data
If imported analytics data exists (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/`), delegate audience-pattern discovery to the `analytics-interpreter` agent (interpret mode) — invoke it via `Task` with `subagent_type: linkedin-studio:analytics-interpreter` (foreground, from this command layer) — to ground the diagnosis in what the data actually shows before relying on self-report.
**Step 2a — first, establish whether there is a problem at all.** This command is
invoked because something *feels* wrong, and the operator's own framing ("my reach
dropped") is a hypothesis, not a measurement. Diagnosing a drop that never happened
sends them chasing a phantom and teaches them to distrust their own numbers.
If imported analytics exist, generate the current report and read its `baseline`
block (see `/linkedin:report` Step 4):
```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" report
```
Read `baseline.period.<metric>.reading` and lead the diagnosis with it:
- **`within-band`** — the numbers are inside the operator's own normal range. Say so
before going further: *"Your last posts are inside your normal range (median X,
normal range YZ). This looks like ordinary variation rather than a drop."* Then ask
whether they still want the diagnostic pass. Often the honest answer is that nothing
is wrong and the fix is to keep publishing — do not manufacture a diagnosis to
justify the command having been run.
- **`below-band`** — the drop is real and measured. Proceed, and use the band figures
as the size of the problem instead of a remembered percentage.
- **`no-verdict`** — print `baseline.<metric>.reason` and say plainly that there is not
enough history to tell a drop from variation. Continue on **self-report only**, and
label the diagnosis as provisional. Never upgrade a refusal into a verdict because
the operator sounds worried.
If imported analytics exist (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/`), delegate audience-pattern discovery to the `analytics-interpreter` agent (interpret mode) — invoke it via `Task` with `subagent_type: linkedin-studio:analytics-interpreter` (foreground, from this command layer) — to ground the diagnosis in what the data actually shows before relying on self-report.
Based on their answer, ask relevant follow-up questions:
@ -150,7 +177,11 @@ Run through this checklist:
## Step 5: Reach Drop Severity Assessment
Based on how much reach dropped:
Based on how much reach dropped. **Measure the drop against the baseline band from
Step 2a, not against the single best week the operator remembers** — a percentage
computed from a remembered peak is a comparison with an outlier, and it will class
ordinary variation as suppression. When the band says `within-band`, the severity is
"normal fluctuation" regardless of what the percentage says.
### Down <25%
**Diagnosis:** Normal fluctuation