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
|
|
@ -9,7 +9,7 @@ import {
|
|||
getPostsForWeek,
|
||||
generateWeeklyReport,
|
||||
} from "../src/reports/weekly.js";
|
||||
import { saveBatch, saveWeeklyReport } from "../src/utils/storage.js";
|
||||
import { saveBatch, saveWeeklyReport, loadWeeklyReport } from "../src/utils/storage.js";
|
||||
import type { PostAnalytics, AnalyticsBatch } from "../src/models/types.js";
|
||||
|
||||
// Helper function to create test post data
|
||||
|
|
@ -662,4 +662,97 @@ describe("weekly", () => {
|
|||
assert.match(report.week, /^\d{4}-W\d{2}$/);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Baseline framing (N17). The report's job is to answer "is this different
|
||||
* from my own normal?" — which requires a baseline built from history BEFORE
|
||||
* the reported week, and an explicit refusal when that history is too thin.
|
||||
*/
|
||||
describe("generateWeeklyReport — baseline", () => {
|
||||
// 2026-W03 is Mon 2026-01-12 .. Sun 2026-01-18.
|
||||
function priorPost(day: string, impressions: number): PostAnalytics {
|
||||
return createTestPost({
|
||||
id: `prior-${day}`,
|
||||
publishedDate: day,
|
||||
metrics: { impressions, reactions: 5, comments: 1, shares: 0, clicks: 2, engagementRate: 4 },
|
||||
});
|
||||
}
|
||||
|
||||
test("should attach a baseline built only from earlier weeks", () => {
|
||||
tempDir = setupTempDir();
|
||||
const posts = [
|
||||
priorPost("2025-12-15", 100),
|
||||
priorPost("2025-12-17", 110),
|
||||
priorPost("2025-12-29", 120),
|
||||
priorPost("2025-12-31", 130),
|
||||
priorPost("2026-01-05", 140),
|
||||
// In-week post, deliberately extreme: if it leaked into the baseline the
|
||||
// median would move and the week would be compared against itself.
|
||||
createTestPost({
|
||||
id: "in-week",
|
||||
publishedDate: "2026-01-14",
|
||||
metrics: { impressions: 99999, reactions: 5, comments: 1, shares: 0, clicks: 2, engagementRate: 4 },
|
||||
}),
|
||||
];
|
||||
saveBatch(tempDir, createTestBatch({ posts }));
|
||||
|
||||
const report = generateWeeklyReport(tempDir, "2026-W03");
|
||||
|
||||
assert.ok(report.baseline, "A generated report must carry a baseline block");
|
||||
assert.equal(report.baseline!.historyBefore, "2026-W03");
|
||||
assert.equal(report.baseline!.impressions.status, "ok");
|
||||
if (report.baseline!.impressions.status !== "ok") return;
|
||||
assert.equal(report.baseline!.impressions.n, 5, "Only the five earlier posts feed it");
|
||||
assert.equal(report.baseline!.impressions.median, 120, "The in-week outlier must not leak in");
|
||||
});
|
||||
|
||||
test("should refuse a baseline verdict when prior history is too thin", () => {
|
||||
tempDir = setupTempDir();
|
||||
const posts = [
|
||||
priorPost("2025-12-15", 100),
|
||||
priorPost("2025-12-17", 110),
|
||||
createTestPost({ id: "in-week", publishedDate: "2026-01-14" }),
|
||||
];
|
||||
saveBatch(tempDir, createTestBatch({ posts }));
|
||||
|
||||
const report = generateWeeklyReport(tempDir, "2026-W03");
|
||||
|
||||
assert.ok(report.baseline);
|
||||
assert.equal(report.baseline!.impressions.status, "insufficient-data");
|
||||
if (report.baseline!.impressions.status !== "insufficient-data") return;
|
||||
assert.equal(report.baseline!.impressions.n, 2);
|
||||
assert.equal(report.baseline!.impressions.required, 5);
|
||||
});
|
||||
|
||||
test("should refuse rather than omit the block when there is no history at all", () => {
|
||||
tempDir = setupTempDir();
|
||||
saveBatch(tempDir, createTestBatch({
|
||||
posts: [createTestPost({ id: "in-week", publishedDate: "2026-01-14" })],
|
||||
}));
|
||||
|
||||
const report = generateWeeklyReport(tempDir, "2026-W03");
|
||||
|
||||
assert.ok(report.baseline, "An absent block is indistinguishable from a pre-N17 report");
|
||||
assert.equal(report.baseline!.impressions.status, "insufficient-data");
|
||||
});
|
||||
|
||||
test("should carry the baseline through to the saved report on disk", () => {
|
||||
tempDir = setupTempDir();
|
||||
const posts = [
|
||||
priorPost("2025-12-15", 100),
|
||||
priorPost("2025-12-17", 110),
|
||||
priorPost("2025-12-29", 120),
|
||||
priorPost("2025-12-31", 130),
|
||||
priorPost("2026-01-05", 140),
|
||||
createTestPost({ id: "in-week", publishedDate: "2026-01-14" }),
|
||||
];
|
||||
saveBatch(tempDir, createTestBatch({ posts }));
|
||||
|
||||
generateWeeklyReport(tempDir, "2026-W03");
|
||||
const reloaded = loadWeeklyReport(tempDir, "2026-W03");
|
||||
|
||||
assert.ok(reloaded?.baseline, "The block must survive the round-trip to disk");
|
||||
assert.equal(reloaded!.baseline!.impressions.status, "ok");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue