linkedin-studio/scripts/analytics/tests/queue-join.test.ts
Kjell Tore Guttormsen e2ad190dda 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
2026-07-25 20:50:45 +02:00

163 lines
6.8 KiB
TypeScript

import { describe, test, afterEach } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { loadQueueEntries, labelPosts } from "../src/utils/queue-join.js";
import type { QueueEntry } from "../src/utils/queue-join.js";
/**
* Format and pillar are NOT analytics data — they live in the post queue, which
* the operator's create surfaces write. The per-format / per-pillar baseline
* therefore rests on a JOIN, and a join that guesses is worse than no labels at
* all: a post filed under the wrong format would poison the very baseline it is
* being measured against. Every ambiguous case here resolves to UNLABELLED.
*/
describe("queue-join", () => {
let tempDir: string;
afterEach(() => {
if (tempDir && existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
});
const post = (publishedDate: string, title = "Some post") => ({
publishedDate,
title,
metrics: { impressions: 1000, engagementRate: 5 },
});
const entry = (overrides: Partial<QueueEntry> & { scheduled_date: string }): QueueEntry => ({
id: overrides.id ?? `q-${overrides.scheduled_date}`,
scheduled_date: overrides.scheduled_date,
pillar: overrides.pillar ?? "ai-agents",
format: overrides.format ?? "text",
hook_preview: overrides.hook_preview,
});
describe("loadQueueEntries", () => {
test("should return an empty list when there is no queue file", () => {
tempDir = mkdtempSync(join(tmpdir(), "queue-join-"));
const result = loadQueueEntries(join(tempDir, "queue.json"));
assert.deepEqual(result, [], "A missing queue is unlabelled data, not an error");
});
test("should return an empty list for malformed JSON rather than throwing", () => {
tempDir = mkdtempSync(join(tmpdir(), "queue-join-"));
const file = join(tempDir, "queue.json");
writeFileSync(file, "{ not json", "utf8");
assert.deepEqual(loadQueueEntries(file), []);
});
test("should return an empty list when the file is not an array", () => {
tempDir = mkdtempSync(join(tmpdir(), "queue-join-"));
const file = join(tempDir, "queue.json");
writeFileSync(file, JSON.stringify({ entries: [] }), "utf8");
assert.deepEqual(loadQueueEntries(file), []);
});
test("should read entries and keep those missing pillar or format", () => {
tempDir = mkdtempSync(join(tmpdir(), "queue-join-"));
const file = join(tempDir, "queue.json");
writeFileSync(
file,
JSON.stringify([
{ id: "a", scheduled_date: "2026-07-01", pillar: "ai", format: "text" },
{ id: "b", scheduled_date: "2026-07-03" },
]),
"utf8"
);
const result = loadQueueEntries(file);
assert.equal(result.length, 2);
assert.equal(result[1].pillar, undefined, "A partial entry is data; dropping it would hide posts");
});
test("should skip rows with no scheduled_date, which cannot join on anything", () => {
tempDir = mkdtempSync(join(tmpdir(), "queue-join-"));
const file = join(tempDir, "queue.json");
writeFileSync(
file,
JSON.stringify([{ id: "a", pillar: "ai", format: "text" }, { id: "b", scheduled_date: "2026-07-03" }]),
"utf8"
);
assert.equal(loadQueueEntries(file).length, 1);
});
});
describe("labelPosts", () => {
test("should label a post whose publish date matches a queue entry exactly", () => {
const result = labelPosts([post("2026-07-08")], [entry({ scheduled_date: "2026-07-08", format: "carousel" })]);
assert.equal(result[0].format, "carousel");
assert.equal(result[0].pillar, "ai-agents");
});
test("should label through a small reschedule gap", () => {
// A queue entry is a PLAN; the operator publishing a day late is normal.
const result = labelPosts([post("2026-07-09")], [entry({ scheduled_date: "2026-07-08" })]);
assert.equal(result[0].format, "text");
});
test("should not label across a gap wider than two days", () => {
const result = labelPosts([post("2026-07-15")], [entry({ scheduled_date: "2026-07-08" })]);
assert.equal(result[0].format, undefined, "A week apart is a different post, not a reschedule");
assert.equal(result[0].pillar, undefined);
});
test("should prefer an exact date match over a near one", () => {
const result = labelPosts(
[post("2026-07-08")],
[entry({ scheduled_date: "2026-07-07", format: "video" }), entry({ scheduled_date: "2026-07-08", format: "text" })]
);
assert.equal(result[0].format, "text");
});
test("should leave a post unlabelled when two entries tie on the same date", () => {
const result = labelPosts(
[post("2026-07-08")],
[
entry({ id: "x", scheduled_date: "2026-07-08", format: "text" }),
entry({ id: "y", scheduled_date: "2026-07-08", format: "carousel" }),
]
);
assert.equal(result[0].format, undefined, "A coin-flip label would poison the format baseline");
});
test("should disambiguate same-date entries on the hook preview", () => {
const result = labelPosts(
[post("2026-07-08", "The thing nobody tells you about agent evals")],
[
entry({ id: "x", scheduled_date: "2026-07-08", format: "text", hook_preview: "Three ways to lose a week" }),
entry({
id: "y",
scheduled_date: "2026-07-08",
format: "carousel",
hook_preview: "The thing nobody tells you",
}),
]
);
assert.equal(result[0].format, "carousel", "The hook is the only honest tiebreaker available");
});
test("should leave the post unlabelled when the queue is empty", () => {
const result = labelPosts([post("2026-07-08")], []);
assert.equal(result[0].format, undefined);
});
test("should never consume one queue entry for two different posts", () => {
// Two posts, one entry: labelling both would double-count the entry's
// format and invent a second post of that format that never existed.
const result = labelPosts([post("2026-07-08"), post("2026-07-09")], [entry({ scheduled_date: "2026-07-08" })]);
const labelled = result.filter((r) => r.format !== undefined);
assert.equal(labelled.length, 1, "An entry labels at most one post");
assert.equal(labelled[0].publishedDate, "2026-07-08", "The exact match wins the entry");
});
test("should preserve every post, labelled or not", () => {
const posts = [post("2026-07-08"), post("2026-07-30")];
const result = labelPosts(posts, [entry({ scheduled_date: "2026-07-08" })]);
assert.equal(result.length, 2, "Unlabelled posts stay in the list; they are just not grouped");
});
});
});