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

@ -178,4 +178,38 @@ describe("generateMonthlyReport", () => {
const report = generateMonthlyReport(root, "2026-03");
assert.equal(report.month, "2026-03");
});
/** Baseline framing (N17) — same contract as the weekly report. */
test("attaches a baseline built only from earlier months", () => {
const root = setupTestRoot([
createPost("2026-01-05", 100, 4),
createPost("2026-01-12", 110, 4),
createPost("2026-02-02", 120, 4),
createPost("2026-02-09", 130, 4),
createPost("2026-02-16", 140, 4),
// In-month outlier: must not feed the baseline it is compared against.
createPost("2026-03-03", 99999, 9),
]);
const report = generateMonthlyReport(root, "2026-03");
assert.ok(report.baseline, "A generated monthly report must carry a baseline block");
assert.equal(report.baseline!.historyBefore, "2026-03");
assert.equal(report.baseline!.impressions.status, "ok");
if (report.baseline!.impressions.status !== "ok") return;
assert.equal(report.baseline!.impressions.n, 5);
assert.equal(report.baseline!.impressions.median, 120);
});
test("refuses a monthly baseline verdict on thin history", () => {
const root = setupTestRoot([
createPost("2026-02-02", 120, 4),
createPost("2026-03-03", 500, 9),
]);
const report = generateMonthlyReport(root, "2026-03");
assert.ok(report.baseline);
assert.equal(report.baseline!.impressions.status, "insufficient-data");
});
});