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
163 lines
6.8 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|