feat(linkedin-studio): RE-R3e — brief history + day-over-day diff (surfaced: frontmatter + Nytt siden sist) [skip-docs]

Closes hull #7 ("ingen brief-historikk"). Each morning brief now records the
trend ids it showed into its own YAML frontmatter (surfaced: <id-csv> =
surfacedIds(ranking)) and renders a day-over-day diff against the most recent
prior brief — a "## Nytt siden sist (<prior-date>)" section that leads the
ranked list, plus a " N nye siden sist." marker on the one-line summary the
SessionStart hook surfaces (no hook change).

- brief.ts: BRIEF_SCHEMA_VERSION 1->2 (artifact frontmatter gained surfaced:;
  the store's SCHEMA_VERSION stays 4 — no store field). Three PURE helpers
  (diffSurfaced / parseSurfacedFrontmatter / selectPriorBriefFile) + the
  surfaced: emit + the section + the summary marker. No fs/clock in brief.ts.
- cli.ts: the brief handler discovers the prior dated file (existsSync-guarded
  readdirSync -> selectPriorBriefFile, strict < today so a same-day re-run is
  byte-identical), parses its surfaced: line, computes the diff, threads it into
  renderBrief AND the shared briefSummary(ranking, diff) (one-source: file
  frontmatter == --json summary, cli.test one-source invariant). --json gains a
  diff:{priorDate,added,carried,dropped} counts object; the console line appends
  the delta. Any fs error degrades to the empty-prior (first-brief) path.

TDD two-phase: stubs -> 17 value-RED (no module-not-found) -> GREEN. Trends suite
216 -> 245 (brief +27, cli +2), 0 fail. New unconditional gate Section 16n (6
checks); ASSERT_BASELINE_FLOOR 117 -> 123; TRENDS_TESTS_FLOOR -> 245. Full gate
FAIL=0; hook suite 139/139 + R3c schedule/run-daily green untouched. Behavioural:
real two-day rename-real-write diff + same-day byte-identity confirmed. Counts
29/19/27 unchanged; no version bump (additive, v0.5.2 dev).

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:
Kjell Tore Guttormsen 2026-06-26 14:40:09 +02:00
commit 5b51b4baeb
7 changed files with 462 additions and 18 deletions

View file

@ -48,7 +48,7 @@
* `schedule` adds no new exit code: an autonomy install never RUNS the system mutation.
*/
import { readFileSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
import { readFileSync, readdirSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
import { fileURLToPath } from "node:url";
@ -68,7 +68,16 @@ import type { TrendStatus } from "./types.js";
import { normalizeItem, normalizeItems, itemToInput } from "./item.js";
import { triage } from "./score.js";
import type { ScoreMode } from "./score.js";
import { rankForBrief, renderBrief, briefSummary, defaultBriefDir, surfacedIds } from "./brief.js";
import {
rankForBrief,
renderBrief,
briefSummary,
defaultBriefDir,
surfacedIds,
diffSurfaced,
parseSurfacedFrontmatter,
selectPriorBriefFile,
} from "./brief.js";
import { launchdPlist, crontabLine, installInstructions, uninstallInstructions, defaultLabel } from "./schedule.js";
import type { ScheduleSpec } from "./schedule.js";
@ -337,7 +346,26 @@ function main(): void {
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, firstMoverDays, saturationAt });
const md = renderBrief(ranking);
// RE-R3e: discover the most recent prior brief in outDir and diff today's surfaced cohort
// against its `surfaced:` line. Any fs error (no dir, unreadable file) degrades to the
// empty-prior (first-brief) path. The dir read lives HERE (the edge) — brief.ts stays fs-free.
const todayIds = surfacedIds(ranking);
let priorIds: string[] = [];
let priorDate: string | null = null;
try {
if (existsSync(outDir)) {
const priorFile = selectPriorBriefFile(readdirSync(outDir), day);
if (priorFile) {
priorIds = parseSurfacedFrontmatter(readFileSync(join(outDir, priorFile), "utf8"));
priorDate = priorFile.slice(0, 10);
}
}
} catch {
priorIds = [];
priorDate = null;
}
const diff = diffSurfaced(todayIds, priorIds, priorDate);
const md = renderBrief(ranking, diff);
const path = join(outDir, `${day}.md`);
mkdirSync(outDir, { recursive: true });
writeFileSync(path, md, "utf8");
@ -347,12 +375,33 @@ function main(): void {
const mark = flags["no-mark"] !== "true";
const marked = mark ? markSurfaced(store, surfacedIds(ranking), day).marked : 0;
if (mark) saveStore(storePath, store);
const summary = briefSummary(ranking); // SAME source the frontmatter carries
const summary = briefSummary(ranking, diff); // SAME source the frontmatter carries (RE-R3e: incl. the delta marker)
if (asJson) {
console.log(JSON.stringify({ path, date: ranking.today, totals: ranking.totals, summary, marked }, null, 2));
console.log(
JSON.stringify(
{
path,
date: ranking.today,
totals: ranking.totals,
summary,
marked,
diff: {
priorDate: diff.priorDate,
added: diff.added.length,
carried: diff.carried.length,
dropped: diff.dropped.length,
},
},
null,
2,
),
);
return;
}
console.log(`Wrote brief: ${path} (${ranking.totals.matched} matched, ${ranking.totals.fresh} fresh, ${marked} surfaced)`);
const deltaNote = diff.added.length > 0 && diff.priorDate !== null ? `, ${diff.added.length} nye siden sist` : "";
console.log(
`Wrote brief: ${path} (${ranking.totals.matched} matched, ${ranking.totals.fresh} fresh, ${marked} surfaced)${deltaNote}`,
);
return;
}