feat(linkedin-studio): RE-R3d — temporal overlay (first-mover + saturation) [skip-docs]
R3 slice (b): the rest of hull #3. The morning brief now reads the temporal axis the R3b seen-log records but the ranking ignored. Two DERIVED signals, computed at brief time from already-persisted fields (publishedAt/capturedAt -> ageDays, surfacedCount), never stored: - first-mover: recent (ageDays <= --first-mover-days, default 2) AND never surfaced on a prior day -> ranked up, badge "first ute". Future-dated (ageDays<0) excluded. - saturation: surfaced on >= --saturation-at (default 3) prior days -> ranked down, badge "mettet (Nx)". Self-surfacing (our seen-log), not market coverage. - warming (1..at-1) keeps the R3b "sett Nx" badge but only at >=2 (contract intact); neutral carries no badge. SB1 derived (no schema bump: SCHEMA_VERSION 4 / BRIEF_SCHEMA_VERSION 1 untouched). SB2 the R3a relevance composite stays the PRIMARY sort key; the temporal rank is a new cmp key after pillar-overlap, before effectiveDate -> re-orders only WITHIN a (composite, overlap) tier. temporalSignal is pure (saturationAt clamped >=1). Prior-day surfacings exclude today (via lastSurfacedAt), so a same-day re-render is byte-identical (caught by the R3c run-daily SC7 regression; fixes a latent R3b prior-day imprecision too). brief CLI gains --first-mover-days / --saturation-at; schedule untouched (nightly uses defaults). Wiring: trend-spotter.md (prose), trend-scoring-modes.md (one-line consumer note), README (## Temporal overlay), gate Section 16m (+6 unconditional -> ASSERT floor 111->117), TRENDS_TESTS_FLOOR 192->216. Counts 29/19/27 unchanged. Zero new files. Gate: Passed 132 / Failed 0; trends 216/216; hook suite 139/139 untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
This commit is contained in:
parent
5aa7187243
commit
2a8459c674
8 changed files with 474 additions and 43 deletions
|
|
@ -600,3 +600,72 @@ describe("trends CLI — schedule subcommand (RE-R3c / autonomous trigger, print
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("trends CLI — brief temporal flags (RE-R3d / SC6)", () => {
|
||||
// brief is flag-driven; spawn into a temp store/out so the real per-user data dir is never touched.
|
||||
function runBrief(args: string[], env: Record<string, string> = {}): { status: number | null; stdout: string } {
|
||||
const res = spawnSync("node", ["--import", "tsx", "src/cli.ts", "brief", ...args], {
|
||||
input: "",
|
||||
encoding: "utf8",
|
||||
cwd: trendsDir,
|
||||
env: { ...process.env, ...env },
|
||||
});
|
||||
return { status: res.status, stdout: res.stdout };
|
||||
}
|
||||
const freshIso = new Date(Date.now() - 2 * 86400000).toISOString().slice(0, 10);
|
||||
function seedStore(trends: unknown[]): string {
|
||||
const store = join(mkdtempSync(join(tmpdir(), "trends-r3d-")), "trends.json");
|
||||
writeFileSync(store, JSON.stringify({ schemaVersion: 2, trends }));
|
||||
return store;
|
||||
}
|
||||
// --no-mark so sequential runs on the same store do NOT accumulate surfacedCount (which would
|
||||
// confound the threshold assertions); tier badges live in the .md body, not in --json.
|
||||
function briefMd(args: string[]): { status: number | null; md: string } {
|
||||
const out = mkdtempSync(join(tmpdir(), "r3d-out-"));
|
||||
const { status, stdout } = runBrief([...args, "--out", out, "--no-mark", "--json"]);
|
||||
const path = status === 0 ? (JSON.parse(stdout).path as string) : "";
|
||||
return { status, md: path ? readFileSync(path, "utf8") : "" };
|
||||
}
|
||||
|
||||
test("--saturation-at 2 escalates a surfacedCount-2 trend from warming (sett 2x) to saturated (mettet 2x)", () => {
|
||||
const store = seedStore([
|
||||
{ id: "s", title: "Seen", url: "https://e/s", source: "tavily", capturedAt: freshIso, publishedAt: freshIso, topics: ["ai"], surfacedCount: 2 },
|
||||
]);
|
||||
try {
|
||||
const dflt = briefMd(["--pillars", "ai", "--store", store]);
|
||||
assert.equal(dflt.status, 0);
|
||||
assert.ok(dflt.md.includes("· sett 2x"), "default saturationAt 3 -> surfacedCount 2 is warming");
|
||||
const tuned = briefMd(["--pillars", "ai", "--store", store, "--saturation-at", "2"]);
|
||||
assert.equal(tuned.status, 0);
|
||||
assert.ok(tuned.md.includes("· 🔁 mettet (2x)"), "--saturation-at 2 -> surfacedCount 2 is saturated");
|
||||
} finally {
|
||||
rmSync(join(store, ".."), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("--first-mover-days 1 drops a 2-day-old unsurfaced trend out of first-mover", () => {
|
||||
const store = seedStore([
|
||||
{ id: "f", title: "Fresh", url: "https://e/f", source: "tavily", capturedAt: freshIso, publishedAt: freshIso, topics: ["ai"] },
|
||||
]);
|
||||
try {
|
||||
const dflt = briefMd(["--pillars", "ai", "--store", store]);
|
||||
assert.ok(dflt.md.includes("· 🥇 først ute"), "default firstMoverDays 2 -> a 2-day unsurfaced trend is first-mover");
|
||||
const tuned = briefMd(["--pillars", "ai", "--store", store, "--first-mover-days", "1"]);
|
||||
assert.ok(!tuned.md.includes("først ute"), "--first-mover-days 1 -> a 2-day trend is neutral");
|
||||
} finally {
|
||||
rmSync(join(store, ".."), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid threshold flags -> exit 2", () => {
|
||||
const store = seedStore([]);
|
||||
try {
|
||||
assert.equal(runBrief(["--pillars", "ai", "--store", store, "--first-mover-days", "x"]).status, 2, "--first-mover-days x");
|
||||
assert.equal(runBrief(["--pillars", "ai", "--store", store, "--first-mover-days", "-1"]).status, 2, "--first-mover-days -1");
|
||||
assert.equal(runBrief(["--pillars", "ai", "--store", store, "--saturation-at", "0"]).status, 2, "--saturation-at 0");
|
||||
assert.equal(runBrief(["--pillars", "ai", "--store", store, "--saturation-at", "x"]).status, 2, "--saturation-at x");
|
||||
} finally {
|
||||
rmSync(join(store, ".."), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue