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:
parent
63506f7d5c
commit
e2ad190dda
15 changed files with 1723 additions and 14 deletions
|
|
@ -142,10 +142,41 @@ The report contains:
|
|||
- **week**: ISO week identifier
|
||||
- **generatedAt**: ISO timestamp when the report was generated
|
||||
- **summary**: Totals and averages — `totalPosts`, `totalImpressions`, `totalReactions`, `totalComments`, `totalShares`, `totalClicks`, optional `totalSaves` (manual entry only), optional `avgOutOfNetworkPct` (manual entry only — the impressions-weighted out-of-network share), `avgEngagementRate`, `avgImpressionsPerPost`
|
||||
- **baseline**: The operator's own normal, from posts published **before** this week —
|
||||
`window`, `required`, `historyBefore`, plus `impressions` and `engagementRate` (each
|
||||
either `{status: "ok", n, median, mad, band}` or
|
||||
`{status: "insufficient-data", n, required, reason}`), and `period` carrying this
|
||||
week's own median per metric with a `reading` of `above-band` / `within-band` /
|
||||
`below-band` / `no-verdict`. **This is what the report leads with (Step 6).**
|
||||
- **topPerformers** / **underperformers**: Best / weakest posts by engagement rate
|
||||
- **trends**: Week-over-week change — `impressionsTrend`, `engagementTrend`, `comparedTo`, `percentChange`
|
||||
- **alerts**: Anomalies and significant events (intra-week + week-over-week)
|
||||
|
||||
**Week-over-week vs baseline — which to trust.** `trends.percentChange` compares two
|
||||
adjacent weeks, so at two or three posts a week it swings wildly on sample composition
|
||||
alone; `baseline` compares against the operator's own spread. When the two disagree —
|
||||
a +38% week that reads `within-band` — **the baseline wins**, and say so explicitly:
|
||||
the percentage is real arithmetic on a sample too small to carry a conclusion.
|
||||
|
||||
## Step 4b: Per-format and per-pillar baselines (optional, when the queue has labels)
|
||||
|
||||
Format and pillar are not analytics data — they live in the post queue — so a separate
|
||||
surface joins them:
|
||||
|
||||
```bash
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" baseline
|
||||
```
|
||||
|
||||
It prints an overall baseline plus one per format and per pillar, and reports its own
|
||||
coverage (how many posts it could not label). Use it to answer "is carousel actually
|
||||
working for me?" — but respect the per-group refusals: **each group is judged on its
|
||||
own N**, so a format used twice gets no verdict even when the overall history is long.
|
||||
That refusal is the honest answer to a question the operator will still want answered;
|
||||
do not fill the gap with the overall baseline as a proxy for the group.
|
||||
|
||||
Skip this step when the join reports no labels at all — say the grouped view needs
|
||||
queue entries rather than presenting an empty table.
|
||||
|
||||
## Step 5: Run Trend Analysis
|
||||
|
||||
Get additional context with trend analysis:
|
||||
|
|
@ -253,12 +284,55 @@ Your post on [topic] achieved 12,500 impressions — a personal best!
|
|||
|
||||
## Step 6: Present Formatted Report
|
||||
|
||||
**The report LEADS with the operator's own baseline, and the absolute numbers come
|
||||
second.** At a normal publishing cadence a single week holds two or three posts, so
|
||||
an absolute total answers no question on its own — "1,240 impressions" is only
|
||||
information once you know whether that is ordinary for this operator. Present the
|
||||
baseline block first, every time, and never reorder these two sections.
|
||||
|
||||
**Do not compute the verdict yourself.** The report JSON's `baseline` block already
|
||||
carries it, decided in code: `baseline.period.<metric>.reading` is one of
|
||||
`above-band` / `within-band` / `below-band` / `no-verdict`. Print what it says.
|
||||
Judging by eye whether a number sits inside the band is the over-read this block
|
||||
exists to prevent — a value 7% above the median looks like a trend and is not one.
|
||||
|
||||
Format the data into a readable report using this template:
|
||||
|
||||
```
|
||||
# LinkedIn Performance Report
|
||||
## Week {week}
|
||||
|
||||
### 📐 Vs Your Own Baseline
|
||||
|
||||
Baseline = your last {baseline.window} posts published **before** this {week/month}
|
||||
(n={baseline.impressions.n}). Median and normal range, not mean — one viral post
|
||||
must not redefine "normal".
|
||||
|
||||
| Metric | This week (median) | Your baseline | Normal range | Reads as |
|
||||
|--------|--------------------|---------------|--------------|----------|
|
||||
| Impressions/post | {baseline.period.impressions.median} | {baseline.impressions.median} | {band.low}–{band.high} | {reading} |
|
||||
| Engagement rate | {baseline.period.engagementRate.median}% | {baseline.engagementRate.median}% | {band.low}–{band.high}% | {reading} |
|
||||
|
||||
Render `reading` in words, not as the raw token:
|
||||
- `above-band` → **above your normal range** — a real move up, worth explaining
|
||||
- `below-band` → **below your normal range** — a real move down, worth diagnosing
|
||||
- `within-band` → **inside your normal range** — normal variation, not a trend.
|
||||
Say this plainly and do not dress it up as momentum in either direction.
|
||||
- `no-verdict` → see the refusal rule below
|
||||
|
||||
**When `baseline.<metric>.status` is `insufficient-data`: refuse the verdict.** Print
|
||||
`baseline.<metric>.reason` verbatim and state that this {week/month}'s numbers are
|
||||
reported **without** a verdict. Do not substitute a week-over-week percentage as a
|
||||
stand-in, do not call it a trend, and do not soften the refusal into a hedge like
|
||||
"early signs suggest" — with fewer than {baseline.required} prior posts there is no
|
||||
signal to be early about. The absolute numbers below are still shown; only the
|
||||
verdict is withheld. An operator who has just started publishing gets this refusal
|
||||
for several weeks, and that is the correct output, not a failure of the report.
|
||||
|
||||
**If the report JSON has no `baseline` key at all**, it was generated before
|
||||
baselines existed — say so, and offer to re-run Step 3 rather than reading the
|
||||
absolutes as if they had been framed.
|
||||
|
||||
### 📊 Key Metrics
|
||||
|
||||
| Metric | Total | Average per Post | vs. Last Week |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue