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
|
|
@ -11,7 +11,8 @@
|
|||
* echo '<raw item|batch>' | node --import tsx src/cli.ts normalize
|
||||
* echo '<scored candidates>' | node --import tsx src/cli.ts score [--mode kortform|long-form] [--threshold N]
|
||||
* echo '<raw item|batch>' | node --import tsx src/cli.ts capture [--store <path>] [--json]
|
||||
* node --import tsx src/cli.ts brief [--pillars <a,b>] [--fresh-days N] [--out <dir>] [--no-mark] [--store <path>] [--json]
|
||||
* node --import tsx src/cli.ts brief [--pillars <a,b>] [--fresh-days N] [--first-mover-days N] [--saturation-at N]
|
||||
* [--out <dir>] [--no-mark] [--store <path>] [--json]
|
||||
* node --import tsx src/cli.ts schedule --pillars <a,b> [--at HH:MM] [--fresh-days N]
|
||||
* [--platform auto|launchd|cron] [--install|--uninstall] [--store <path>]
|
||||
*
|
||||
|
|
@ -22,7 +23,9 @@
|
|||
* one owner) first-sight on the record so the morning brief ranks on it — and reasons over
|
||||
* accumulated history via `query`/`list`. `brief` (RE-R2b)
|
||||
* renders a dated, pillar-ranked morning brief over the store to a Markdown file the
|
||||
* SessionStart hook surfaces. `add` is the MANUAL single-trend path (raw flags, no
|
||||
* SessionStart hook surfaces; RE-R3d adds a DERIVED temporal overlay (first-mover up / saturated
|
||||
* down) as a within-tier ranking refinement, tunable via `--first-mover-days`/`--saturation-at`.
|
||||
* `add` is the MANUAL single-trend path (raw flags, no
|
||||
* normalization, publish-date-free). `act`/`skip`/`reset --id` set a trend's lifecycle status
|
||||
* (RE-R3b): the morning brief EXCLUDES acted/skipped and records each surfacing (per-day-idempotent
|
||||
* `surfacedCount`) so the loop stops re-surfacing handled work; a re-capture refreshes the score
|
||||
|
|
@ -107,7 +110,7 @@ function usage(msg: string): never {
|
|||
" normalize < raw-item-or-batch.json\n" +
|
||||
" score [--mode kortform|long-form] [--threshold N] < scored-candidates.json\n" +
|
||||
" capture [--store <path>] [--json] < raw-item-or-batch.json\n" +
|
||||
" brief [--pillars <a,b>] [--fresh-days N] [--out <dir>] [--no-mark] [--store <path>] [--json]\n" +
|
||||
" brief [--pillars <a,b>] [--fresh-days N] [--first-mover-days N] [--saturation-at N] [--out <dir>] [--no-mark] [--store <path>] [--json]\n" +
|
||||
" schedule --pillars <a,b> [--at HH:MM] [--fresh-days N] [--platform auto|launchd|cron] [--install|--uninstall] [--store <path>]",
|
||||
);
|
||||
process.exit(2);
|
||||
|
|
@ -315,12 +318,25 @@ function main(): void {
|
|||
if (Number.isNaN(n) || n < 0) usage("--fresh-days must be a non-negative integer");
|
||||
freshDays = n;
|
||||
}
|
||||
// RE-R3d temporal-overlay thresholds (defaults mirror brief.ts's RankOptions defaults).
|
||||
let firstMoverDays = 2;
|
||||
if (flags["first-mover-days"] && flags["first-mover-days"] !== "true") {
|
||||
const n = Number.parseInt(flags["first-mover-days"], 10);
|
||||
if (Number.isNaN(n) || n < 0) usage("--first-mover-days must be a non-negative integer");
|
||||
firstMoverDays = n;
|
||||
}
|
||||
let saturationAt = 3;
|
||||
if (flags["saturation-at"] && flags["saturation-at"] !== "true") {
|
||||
const n = Number.parseInt(flags["saturation-at"], 10);
|
||||
if (Number.isNaN(n) || n < 1) usage("--saturation-at must be a positive integer");
|
||||
saturationAt = n;
|
||||
}
|
||||
// A bare `--out` yields the string "true" (parseFlags); the guard falls back to
|
||||
// defaultBriefDir() so it never writes to ./true.
|
||||
const outDir = flags.out && flags.out !== "true" ? flags.out : defaultBriefDir();
|
||||
const day = today(); // one wall-clock read for both the ranking and the filename
|
||||
const store = loadStore(storePath); // hoisted: also needed for the surfacing write below
|
||||
const ranking = rankForBrief(store, pillars, day, { freshDays });
|
||||
const ranking = rankForBrief(store, pillars, day, { freshDays, firstMoverDays, saturationAt });
|
||||
const md = renderBrief(ranking);
|
||||
const path = join(outDir, `${day}.md`);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue