The shipped render dates the section header (## 🆕 Nytt siden sist (<prior-date>)) when a prior brief exists — the form SC9, Phase-B, and the behavioural step already specify. The plan Step 3 snippet showed a bare header and the brief S-history prose was silent on the date; both are corrected to match the shipped code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
39 KiB
Brief — RE-R3e: brief history + day-over-day diff (R3 slice d)
Slice: RE-R3e (research-engine rung-2, R3 slice (d) in the operator's
(a)→(c)→(b)→(d)→(e)sequence — the doc is numbered r3e by creation order, the concept is slice (d)). It closes hull #7 (substrate §1 hull list: "ingen brief-historikk"): the dated morning brief already writes one Markdown file per day (morning-brief/YYYY-MM-DD.md), but the file is prose-only — nothing records which trends a brief showed in a machine-readable form, and no run reads yesterday's brief, so the engine cannot answer the one question a daily motor exists to answer: "what is new since I last looked?" Predecessor: RE-R2b (the dated brief artifact +surfacedIds(ranking)— the exact set a brief shows:brief.ts:305) + RE-R3b (the per-day-idempotent seen-logsurfacedCount/lastSurfacedAt, which already records that a trend was surfaced but not with which cohort) + RE-R3c (the autonomous trigger that makes the dated files accumulate day-over-day on their own — the dependency that makes a day-over-day diff a real signal, not a once-in-a-while comparison) + RE-R3d (the temporal overlay — the within-brief recency/saturation class the diff is orthogonal to). Substrate:docs/research-engine-concepts.local.md§1 hull #7 ("ingen brief-historikk") + §B3 ("Dated-digest som flat plain-text-artefakt … diffbar, grep-bar, lenkbar … Senere sesjon laster «gårsdagens brief» trivielt" — the dated file is explicitly designed to be diffed, R3e is the diff B3 anticipated) + §B4 ("append-only seen-log → ikke re-overflate samme sak" — R3e is the per-cohort complement: not "have I seen this ever" but "was this in the PRIOR brief"). The core decisions (operator-confirmed, AskUserQuestion 2026-06-26 — baked):
- SD1 — persist membership in the brief's own frontmatter, NOT a sidecar. Each brief writes a single
surfaced: <id-csv>line into its YAML frontmatter — the ids it actually showed (surfacedIds(ranking)). This keeps one self-describing, grep-bar artifact per day (B3), mirrors the existingstore: { … }frontmatter idiom, and is hook-safe (the SessionStartextractYamlis^summary:-anchored and line-scoped — a newsurfaced:line cannot perturb it). The diff reads the prior brief'ssurfaced:line via one pure regex. No sidecar.json; no second artifact.BRIEF_SCHEMA_VERSIONbumps 1 → 2 (the frontmatter gained a field — the first bump since R2b; the store'sSCHEMA_VERSIONstays 4).- SD2 —
addedwith titles +droppedas a count;brief.tsstays store-free. The diff is the symmetric set difference of today's surfaced ids against the prior brief's: added (in today, not prior — the headline "what's new", rendered with titles resolved from the ranking the brief already holds), carried (in both), dropped (in prior, not today).addedis the value;dropped/carriedrender as a one-line tally (counts). The dropped ids are not resolved against the store for an acted/skipped/aged reason — that would require injecting store records into the render and is the one explicit follow-up (§4). The render needs only the ranking it already has →brief.tsstays pure (no store, no fs). The framing is honest: "ikke vist i dag" (not shown today), never "resolved" (which the count cannot prove). TDD-order (two-phase RED, light-Voyage discipline, inherited from R3c/R3d): Phase A —diffSurfaced,parseSurfacedFrontmatter,selectPriorBriefFile(+ theBriefDifftype) are NEW named exports of the EXISTINGbrief.ts; under Node16 ESM a missing named import throws at module-load (everybrief.test.tstest would error, not assert), so land non-throwing stubs FIRST (diffSurfaced → {priorDate:null,added:[], carried:[],dropped:[]},parseSurfacedFrontmatter → [],selectPriorBriefFile → null;renderBriefgains an optionaldiffparam it ignores in the stub), then record value-assertion RED against them (the stubs' constant returns fail the diff/parse/select/section/marker assertions — true assertion-RED). Phase B — the CLI two-day diff test is value-RED against the existingbriefhandler (today it writes nosurfaced:line, reads no prior brief, and its--jsoncarries nodiffkey → the day-2 diff assertions fail). See plan Step 1.
1. Operator decision context (2026-06-26)
The research engine is Tier-1 (operator, 2026-06-23). R1→R3d built the deterministic spine, the trend's life after capture, the autonomy that runs it, and the within-brief temporal overlay: item-schema + triage (R1) → capture bridge (R2a) → dated morning brief + surfacing (R2b) → persisted relevance + composite ranking (R3a) → status lifecycle + seen-log + re-score (R3b) → autonomous trigger + headless entry (R3c) → temporal overlay (first-mover + saturation, R3d). The brief now regenerates itself every morning (R3c) and ranks each trend by its frozen relevance composite, its pillar overlap, and a live first-mover/saturation class (R3d). But every morning's brief is a standalone snapshot: it cannot say "these three are new since yesterday; the two you saw yesterday are gone." The accumulated dated files are a pile of snapshots, not a history with a diff.
R3e closes hull #7 — the brief history + day-over-day diff — which the operator chose as slice (d) of the full-R3 build-out (2026-06-24, "ALLE gjenstående R3-slices … i rekkefølge (a) → (c) → (b) → (d) → (e)"). It is sequenced after R3c for a load-bearing reason: a day-over-day diff is only meaningful when a brief is produced every day on its own — R3c's nightly trigger is what makes "yesterday's brief" reliably exist. (c) makes (d) a real signal; without the daily trigger, "since last brief" could mean "since whenever the operator last happened to ask."
The concrete value — turning a pile of snapshots into a feed. A daily motor's job is to surface the delta:
the operator does not want to re-read the full ranked list every morning and diff it in their head — they want
the engine to say "3 nye siden i går" at the top of the brief (and on the one-line SessionStart surfacing,
for free). R3e makes the dated file a genuine history rung: each brief records what it showed
(surfaced: frontmatter), and the next brief reads the most recent prior one and renders "Nytt siden sist."
This is the smallest honest step from "a brief is written daily" (R3c) to "the brief tells me what changed"
(the point of a feed) — and it is exactly the diffable dated-digest §B3 said the artifact was designed to be.
2. The gap — grounded in code
- The dated brief is prose-only; membership is not machine-readable.
cli.ts:341-343writesmorning-brief/${day}.mdfromrenderBrief(ranking); the body embeds each trend'sidinside a rendered bullet (brief.ts:242,:251—`${e.trend.id}`), but there is no structured record of the set a brief showed.surfacedIds(ranking)(brief.ts:305) computes that set and feeds it tomarkSurfaced(cli.ts:348), but it is never persisted to the artifact — so a later run that wants "what did yesterday's brief show" would have to scrape prose. The seen-log (surfacedCount/lastSurfacedAt, R3b) records that and how many days a trend was surfaced, but not which cohort it appeared with — it cannot reconstruct "yesterday's brief contained {A, B, C}." - No run reads a prior brief. The only consumer of the dated files is the SessionStart hook's
latestMorningBrief(session-start.mjs:60-77), which reads the single newest file'sdate+summaryand surfaces it verbatim. Nothing reads the second-newest to compare. There is no diff, anywhere. - "New since last" is not derivable from the store alone. The seen-log gives "never surfaced ever"
(
surfacedCountabsent ⇒ a first-ever sighting) — but that is not "new since the last brief": a trend surfaced once three days ago, absent from yesterday's brief, reappearing today is new to yesterday's reader yet hassurfacedCount 1(not 0). Only a per-brief membership record (the priorsurfaced:set) answers "was this in the immediately-prior brief," and only it can compute dropped (in the prior cohort, gone today) — which the store cannot express at all. This is precisely the gap §B3's "diffbar … dated-digest" and §1 hull #7 name. - The brief's own summary cannot signal a delta.
briefSummary(brief.ts:204-216) describes today's top match in isolation; the SessionStart surfacing (session-start.mjs:534-536) shows that line verbatim. There is no "N nye siden sist" the operator could see without opening the file — the one number a feed leads with.
3. Scope — what is IN (RE-R3e)
Zero new source/test files (the two tracked slice docs aside). R3e is pure EDITs: the diff lives beside
surfacedIds in brief.ts (the module that already owns the brief's pure read logic — surgical, no new module
to wire into the gate), its unit tests in brief.test.ts, its CLI wiring (prior-file discovery) in
cli.ts/cli.test.ts, plus the wiring docs + gate.
S-history — scripts/trends/src/brief.ts (EDIT) — the pure diff + the persisted membership
BRIEF_SCHEMA_VERSIONbumps 1 → 2 (brief.ts:23) — the frontmatter gained thesurfaced:field. (The store'sSCHEMA_VERSIONis untouched at 4 — R3e adds no store field; the membership lives in the artifact, the diff is derived.)export interface BriefDiff { priorDate: string | null; added: string[]; carried: string[]; dropped: string[] }—priorDate= the date of the brief diffed against (null⇒ no prior brief, i.e. the first ever / a fresh data dir);added/carried/dropped= the three partitions of the set difference, each order-stable (added/carried preserve today'ssurfacedIdsorder; dropped preserves the prior set's order).export function diffSurfaced(currentIds: string[], priorIds: string[], priorDate: string | null): BriefDiff— pure (no clock/fs/env; both id lists + the prior date are injected by the CLI edge).added=currentIdsnot inpriorIds;carried=currentIdsinpriorIds;dropped=priorIdsnot incurrentIds. Uses aSetfor membership; preserves input order in the output arrays. WhenpriorIdsis empty (first brief),added === currentIdsanddropped === [].export function parseSurfacedFrontmatter(md: string): string[]— pure; extracts thesurfaced:value from a brief's full text via a single line-anchored regex (mirrors the hook'sextractYamlidiom:/^surfaced: *([^\n]*)/m), splits on,, trims, drops empties. Returns[]when the line is absent, blank, or malformed (a pre-R3e brief, or a hand-edited file) — degrades to "empty prior," never throws. Real ids are comma-free hex (store.ts:69-72), so the CSV is unambiguous.export function selectPriorBriefFile(filenames: string[], today: string): string | null— pure; from a directory listing, returns the lexicographically greatest filename matching^\d{4}-\d{2}-\d{2}\.md$whose date is strictly less thantoday(ISO dates sort lexicographically, so string compare = date compare), elsenull. Mirrors the hook'slatestMorningBrieffilter+sort (session-start.mjs:63-66) but excludes today and any future-dated file — so a same-day re-run diffs against the true previous day, not its own just-written file (the byte-determinism guarantee, SC8).renderBriefgains an optionaldiffparam (brief.ts:259):renderBrief(ranking: BriefRanking, diff?: BriefDiff). Two additive emissions:- Frontmatter
surfaced:line —surfaced: ${surfacedIds(ranking).join(",")}inserted beforeschemaVersion:(always emitted, even for an empty store →surfaced:blank; this is the record the next day's diff reads, independent of whether today had a prior).schemaVersion:now renders 2. - A
## 🆕 Nytt siden sistsection (the header gains(<prior-date>)when a prior brief exists, per SC9), placed after the intro line and before## 🎯 Topp-treff(the delta leads, then the full ranked list). Branches (all deterministic):- no diff arg /
priorDate === nullwith added →_Første brief — alt nedenfor er nytt._ priorDate === nullwith no added (empty first brief) →_Første brief._priorDate !== null,addednon-empty → one bullet per added id, its entry resolved from the ranking (title + matched pillars + date + link + id, reusing the bullet idiom), then a tally line_${carried.length} båret over, ${dropped.length} ikke vist i dag._priorDate !== null,addedempty →_Ingenting nytt siden ${priorDate}._(+ the same tally line)
- no diff arg /
- When
diffis omitted (a barerenderBrief(ranking)call, e.g. a unit test that does not exercise the diff), it defaults to the empty diff ({priorDate:null,added:[],carried:[],dropped:[]}) → the_Første brief._section branch (priorDate===null,addedempty). Thesurfaced:frontmatter line is independent of the diff — alwayssurfacedIds(ranking).join(",")(blank only for an empty store), so a non-empty ranking still emits its real surfaced ids. (This keeps existing single-arg call sites compiling and semantically valid.)
- Frontmatter
briefSummarygains an optionaldiffparam (brief.ts:204):briefSummary(ranking, diff?). Whendiffis present,priorDate !== null, andadded.length > 0, it appends${added.length} nye siden sist.to the one-line headline — so the SessionStart hook surfaces the delta for free (it already shows thesummary:line verbatim; no hook edit). The marker is suppressed on the first brief (priorDate === null) and when nothing is new (no noise). It carries no double-quote and no newline (the^summary: *"?([^"\n]*)"?hook-regex invariant,brief.ts:200-203).renderBriefpasses itsdiffthrough tobriefSummaryso the frontmattersummary:and the--json summaryagree.
S-cli — scripts/trends/src/cli.ts (EDIT) — prior-brief discovery + the diff in --json
- The
briefhandler discovers the prior brief and computes the diff (between the ranking atcli.ts:339and the render at:340):readdirSync(outDir)(guarded byexistsSync— a first run has no dir) →selectPriorBriefFile(files, day)→ if found,readFileSyncit andparseSurfacedFrontmatter→ builddiffSurfaced(surfacedIds(ranking), priorIds, priorDate); on any fs error, degrade to the empty-prior diff (priorDate: null). Pass the diff intorenderBrief(ranking, diff). AddsreaddirSyncto the existingnode:fsimport (cli.ts:51). --jsongains adiffobject (cli.ts:352):diff: { priorDate, added: added.length, carried: carried.length, dropped: dropped.length }— counts, not id lists (the headlessrun-daily.shcollapses--jsonto one cron-log line). The non-JSON console line (cli.ts:355) appends, N nye siden sistwhenadded > 0 && priorDate !== null.--no-markis unchanged in meaning — it still governs only the store seen-log write (cli.ts:347-349). The artifact'ssurfaced:frontmatter records what the brief showed regardless of--no-mark(it is a property of the rendered brief, not of the store mutation). No new flag.
Wiring (D-default — WIRE, mirrors R3a/R3b/R3c/R3d)
agents/trend-spotter.md(EDIT, prose-only, minimal): one line — the morning brief now records which trends it showed (frontmattersurfaced:) and renders a day-over-day diff ("Nytt siden sist") against the most recent prior brief — no new capture step; the agent's polling/capture path is unchanged. Domain-general (no vendor/sector token).scripts/trends/README.md(EDIT): add a## Brief history + diff (RE-R3e)section between the R3d temporal- overlay section and## Tests: thesurfaced:frontmatter record, theselectPriorBriefFileprior-discovery (strict< today, same-day re-run determinism), thediffSurfacedpartitions, the section + the summary marker, and theBRIEF_SCHEMA_VERSION 1→2boundary (artifact-only; storeSCHEMA_VERSIONstays 4).scripts/test-runner.sh(EDIT): bumpTRENDS_TESTS_FLOOR(live:716, currently 216) to thetests Nline reported after the suite runs — recounted live, append `+ RE-R3e: brief +N, cli +N (brief history- diff)
to the inline breakdown comment. Add **Section 16n** ("Trends Brief History / Diff", RE-R3e) **between Section 16m's closingecho ""and the Section 18 header** (anti-erosion must stay last). Mirror 16m's shape: **unconditional**, deps-absent-safe (grep -qF+ a non-vacuity self-test emitting **one** pass/fail). **6 emitters** (all on tracked source, notsx, all literals **ASCII** — the section header emoji🆕is **never** grepped; the shell stays ASCII-clean for bash 3.2set -u): (1) self-test; (2)export function diffSurfacedinbrief.ts; (3)parseSurfacedFrontmatterinbrief.ts; (4) the section header literalNytt siden sistinbrief.ts; (5)selectPriorBriefFileincli.ts(the diff wiring); (6) the frontmatter emitsurfaced:inbrief.ts. **6 unconditional emitters → bumpASSERT_BASELINE_FLOOR117 → exactly 123** (**live:1473**; "live recount" is the safety net; the expected value is the pinned 117 + 6). Insert the 16n clause into the **header-enumeration prose chain (:53-64`)** before "…the assertion-count anti-erosion floor (SC6) in Section 18," and append the R3e (→123) narration to the Section-18 floor-history comment (which ends "= 117").
- diff)
4. Non-goals — what is OUT (deferred)
- A sidecar
.jsonmembership manifest — OUT (SD1). Membership lives in the brief's own frontmatter (surfaced:), keeping one self-describing artifact (B3). No second file per day. droppedresolved to an acted/skipped/aged reason — OUT (SD2), and the one explicit follow-up.droppedrenders as a count ("N ikke vist i dag"). Labelling why each dropped id left (acted/skipped viastatus, or aged pastfreshDays) would require injecting the store records into the render —brief.tswould no longer be store-free. Honest framing for R3e: "ikke vist i dag," never "resolved." A small, clearly-scoped follow-up (CLI resolves dropped ids →{title, status}and passes them to a richer render) if the loop-closing signal proves worth the coupling.- A browsable history INDEX file (e.g. a rolling
history.mdof all past briefs) — OUT. The dated files + thesurfaced:frontmatter are the history (grep-bar, lenkbar — B3); an index is a presentation nicety, not a capability gap. - A SessionStart hook change to render the diff — OUT. The "N nye siden sist" marker rides the existing
summary:surfacing (session-start.mjs:534-536) — no hook edit, no hook test, no new frontmatter field the hook must learn. (The hook still reads onlydate+summary.) schedule/run-daily.shchanges — OUT. The nightly run callsbrief(run-daily.sh:33), which now computes the diff internally → the scheduled brief gets "Nytt siden sist" automatically, with no scheduler edit (no R3c regression surface).- A new store field / schema bump / store mutation for the diff — OUT.
SCHEMA_VERSIONstays 4;types.ts/store.tsare untouched. The membership is an artifact property; the diff is derived at the CLI edge. - "New" defined as first-ever-sighting (
surfacedCount === 0) — OUT (rejected as less correct). R3e's "new" is relative to the immediately-prior brief (artifact diff), which also flags a trend re-emerging after a gap — the honest meaning of "siden sist." (surfacedCountstays the R3d saturation input, a different question.) - Diffing against an arbitrary historical brief (
--since <date>) — OUT. R3e diffs against the most recent prior brief only (the "since last" a daily feed needs). An arbitrary baseline is a later nicety. - A new module / new test file — none. The diff lives in
brief.ts; its tests inbrief.test.ts. (No pathguard surface — all EDITs.) - New agent / new command / new reference doc — none. R3e EDITs
brief.ts+cli.ts+ their tests + one agent (prose) + README + gate. Counts stay 29/19/27. (The SSOTtrend-scoring-modes.mdis not touched — the diff is not a scoring concern; scope fence.)
5. Boundaries / invariants (must hold)
- TDD iron law (two-phase RED): failing tests land BEFORE implementation. Phase A — the new named exports
(
diffSurfaced/parseSurfacedFrontmatter/selectPriorBriefFile/BriefDiff) need non-throwing stubs first (Node16 ESM throws a missing named import at module-load), THEN value-assertion RED against the constant stubs. Phase B — the CLI two-day diff test is value-RED against the existing handler (nosurfaced:write, no prior read, nodiffin--jsontoday). The plan does not claim a single "everything fails before any code" run. brief.tsstays pure (no clock, no fs, no env, no AI):diffSurfaced/parseSurfacedFrontmatter/selectPriorBriefFileall take strings/arrays and return values — the directory read + file read live incli.ts(the edge), exactly liketoday/pillarsare injected. The module's "No fs, no clock, no AI, no network" header claim is preserved.- Determinism of the brief: given
(store, pillars, today, freshDays, firstMoverDays, saturationAt, diff)the rendered.mdis byte-identical (the diff is now an injected input, liketoday). Critically, a same-day re-run is byte-identical:selectPriorBriefFileexcludes${today}.md(strict<), so the re-run diffs against the same previous day's brief and re-writes the samesurfaced:line (R3c SC7 preserved). surfaced:records the shown set,--no-mark-independent: the frontmatter line issurfacedIds(ranking)joined — what the brief showed — regardless of whether the store seen-log was written (--no-markgoverns the store mutation only). The artifact is always self-consistent.- Frozen composite + temporal overlay untouched (R3a + R3d): R3e adds no
cmpkey and changes no ranking —rankForBriefis unchanged. The diff is a post-ranking, render-time layer over the same surfaced set.score.compositeis never read for mutation; the R3dtemporaloverlay is orthogonal (it orders within the brief; the diff compares across briefs). - Schema boundary:
BRIEF_SCHEMA_VERSIONbumps 1 → 2 (the artifact's frontmatter gainedsurfaced:); the store'sSCHEMA_VERSIONstays 4;types.ts/store.ts/score.ts/item.ts/schedule.ts/run-daily.share untouched. - Hook unaffected: the SessionStart surfacing reads
date+summaryonly (session-start.mjs:60-77). The newsurfaced:frontmatter line is^surfaced:-keyed (the^summary:-anchored, line-scopedextractYamlcannot match it), and thesummary:marker carries no"/\n— so the regex still captures the summary whole. R3e touches neither the hook nor the field set the hook reads; the hook suite must still pass untouched (regression sanity; R3e adds no hook test). - ASCII-only gate literals:
scripts/test-runner.shmust stay ASCII (a multibyte char crashes bash 3.2 underset -u). The Section-16n sentinels grep the ASCII literals (export function diffSurfaced,parseSurfacedFrontmatter,Nytt siden sist,selectPriorBriefFile,surfaced:) — never the🆕emoji (which lives only inbrief.tssource + rendered output, asserted by the TS tests, not by the shell gate). - Domain-general: no hard-coded user/repo path, no vendor/sector token in any edit. The section header +
Norwegian copy (
Nytt siden sist,båret over,ikke vist i dag,nye siden sist,Første brief) are domain-general UI copy (the brief's existing language); pillars/topics remain config. Section 17 de-niche stays green. - Pathguard: R3e adds no new files — every change is an EDIT of an existing file (write-allowed). (No
.mjs-under-hooks/scripts/surface, no newscripts/file.) - Counts (refs/agents/commands 27/19/29) unchanged — recounted live at land, never pinned/guessed.
6. Success criteria (testable)
- SC1 (diffSurfaced — partitions + order + empty prior) —
diffSurfaced(["a","b","c"], ["b","c","d"], "2026- 06-25")→{priorDate:"2026-06-25", added:["a"], carried:["b","c"], dropped:["d"]}(added/carried in current order, dropped in prior order).diffSurfaced(["a","b"], [], null)→{priorDate:null, added:["a","b"], carried:[], dropped:[]}(empty prior ⇒ everything added). Pure: same inputs → same output. The three partitions are mutually disjoint —Setmembership is binary (an id is inpriorIdsor not), so each id lands in exactly one of added/carried and dropped is disjoint from both; within each list, order and any duplicates mirror the input (surfacedIdsyields distinct ids in production, so within-list dups never arise — the cross-partition exclusivity is the real invariant, not within-list dedup). - SC2 (parseSurfacedFrontmatter — read + degrade) — parses
surfaced: 1a2b,3c4d,5e6f(in a full frontmatter block) →["1a2b","3c4d","5e6f"]; a blanksurfaced:→[]; an absentsurfaced:line (a pre-R3e brief) →[]; whitespace around ids is trimmed; thesummary:/store:/date:lines are not mismatched (line-anchored). Never throws on malformed input. - SC3 (selectPriorBriefFile — strict-prior selection) — from
["2026-06-24.md","2026-06-25.md","2026-06-26.md", "README.md","2026-06-30.md"]withtoday="2026-06-26"→"2026-06-25.md"(greatest< today; excludes today2026-06-26.mdand the future2026-06-30.md; ignores the non-datedREADME.md). Empty list, or no file< today, →null. - SC4 (frontmatter
surfaced:line + round-trip) —renderBrief(ranking, diff)emits exactly one^surfaced: <csv>$line, equal tosurfacedIds(ranking).join(","), positioned beforeschemaVersion: 2; an empty-store brief emitssurfaced:(blank);parseSurfacedFrontmatter(renderBrief(r, d))round-trips tosurfacedIds(r).schemaVersion:renders2. - SC5 (Nytt siden sist section — all four branches) — the rendered body contains
## 🆕 Nytt siden sist; withpriorDate:null+ added →Første brief — alt nedenfor er nytt; withpriorDate:null+ no added (empty store) →Første brief.; with a prior +added→ one bullet per added entry (its title present, resolved from the ranking) +N båret over, M ikke vist i dag; with a prior + no added →Ingenting nytt siden <date>- the tally. The section precedes
## 🎯 Topp-treff.
- the tally. The section precedes
- SC6 (summary delta marker) —
briefSummary(ranking, diff)withpriorDate !== nullandadded.length > 0ends with${added.length} nye siden sist.; withpriorDate:null(first brief) oradded.length === 0, the marker is absent (andbriefSummary(ranking)with no diff ===briefSummary(ranking, emptyDiff)— no marker, so the existingfrontmatter summary === briefSummary(r)test stays green). The summary contains no"and no\n. - SC7 (schema boundary) —
BRIEF_SCHEMA_VERSION === 2;SCHEMA_VERSION === 4;types.tsuntouched; abriefrun does not change any record'sscore.composite(onlysurfacedCount/lastSurfacedAtmove — the existing R3b behaviour, sincerankForBrief/markSurfacedare unchanged). - SC8 (determinism, incl. same-day re-run) — two
briefrenders with the same(store, pillars, today, opts, diff)→ byte-identical.md. End-to-end via the CLI: runningbrieftwice on the same day (the second after the first wrote${day}.md) → byte-identical files, becauseselectPriorBriefFileexcludes the same-day file and picks the same prior day. - SC9 (CLI diff wiring — two-day sequence) — write a day-1 brief (records
surfaced:for its cohort), then a day-2 brief over a store with one new trend: the day-2.md## 🆕 Nytt siden sist (<day-1>)section lists the new trend, the day-2--jsoncarriesdiff: { priorDate:<day-1>, added:≥1, carried:…, dropped:… }, and the console line appendsN nye siden sist. A first run (empty dir) →diff.priorDate === null. A custom--outisolates discovery to that dir (the diff reads prior briefs only fromoutDir). - SC10 (gate + wiring + de-niche) —
bash scripts/test-runner.sh→FAIL=0: trends suite green at the bumpedTRENDS_TESTS_FLOOR; new Section 16n green (the six ASCII sentinels + non-vacuity self-test);ASSERT_BASELINE_FLOOR= 123 (117 + 6); Section 17 de-niche green; counts 29/19/27; the hook suite still green untouched (node --test hooks/scripts/__tests__/*.test.mjs).
7. Verification
Deterministic: bash scripts/test-runner.sh → FAIL=0; trends suite ≥ new floor; Section 16n self-test +
greps pass; ASSERT_BASELINE_FLOOR = 123; Section 17 de-niche green; ref/agent/command counts unchanged.
Regression sanity: node --test hooks/scripts/__tests__/*.test.mjs → still green untouched (R3e touches no
hook). The R3c suite (schedule.test.ts/run-daily.test.ts) still green untouched (schedule.ts/run-daily.sh
not edited).
Behavioural (manual):
D=/tmp/r3e-mb-$$; S=/tmp/r3e-$$.json(unique dir, norm). Seed a store with two on-pillar trends and run a day-1 brief:node --import tsx src/cli.ts brief --pillars ai,gov --out "$D" --store "$S"→ inspect the.md: the frontmatter carriessurfaced: <ids>andschemaVersion: 2; the## 🆕 Nytt siden sistsection saysFørste brief — alt nedenfor er nytt.capturea third on-pillar trend into the same store, then run a day-2 brief with a latertoday(seed via a second dated file is not possible — use--out "$D"so day-1's${day}.mdis the prior; on a real next-day run the date advances): inspect the new.md→## 🆕 Nytt siden sist (<prior-date>)lists the new trend with its title, thenN båret over, M ikke vist i dag; the--jsonshowsdiff.added ≥ 1.- Re-run the same-day brief → the written
.mdis byte-identical (diffagainst the same prior file;surfaced:re-written identically) —cmpthe two files. - Confirm the seeded records'
score.compositevalues are unchanged after the briefs (onlysurfacedCount/lastSurfacedAtadvance) — the diff never mutates the store ranking. - Confirm a pre-R3e brief (no
surfaced:line) as the prior →parseSurfacedFrontmatterreturns[]→ every trend reads asadded(graceful degrade, no crash).
8. Open questions for the go-gate
Two architectural decisions are CONFIRMED (operator, AskUserQuestion 2026-06-26): SD1 persist membership
in the brief's frontmatter (no sidecar); SD2 added with titles + dropped as a count (brief.ts
store-free). Residual decisions, all baked to the recommended default — confirm or redirect with "Go":
- D1 — "new" = relative to the immediately-prior brief (artifact diff), not first-ever (
surfacedCount 0)? YES (rec). It is the honest meaning of "siden sist" and catches re-emergence; it is also what unlocksdropped. Re-open only to redefine "new" as first-ever. - D2 — diff against the most recent prior brief only (no
--sincebaseline)? YES (rec). The "since last" a daily feed needs. Re-open only to add an arbitrary historical baseline. - D3 —
## 🆕 Nytt siden sistplaced before## 🎯 Topp-treff(delta leads)? YES (rec). The one thing a feed leads with. Re-open only to place it after the ranked list (appendix) or omit the header on a first brief. - D4 — summary marker
N nye siden sist.(suppressed on first brief / when nothing new)? YES (rec). The delta the SessionStart hook surfaces for free, with no hook edit. Drop only to keep the summary minimal. - D5 —
surfaced:frontmatter always emitted (incl.--no-mark, incl. empty store → blank)? YES (rec). It is the record the next diff reads; gating it on--no-markor non-empty would silently break tomorrow's diff. Re-open only to gate it. - D6 —
dropped/carriedrender as a one-line count (no titles);droppedframed "ikke vist i dag"? YES (rec, = SD2). Keepsbrief.tsstore-free; honest (a count cannot prove "resolved"). Re-open only to pull the reason-labeled follow-up into R3e now. - D7 —
BRIEF_SCHEMA_VERSION1 → 2; storeSCHEMA_VERSIONstays 4? YES (rec). The frontmatter gained a field (the first artifact-schema change since R2b); no store field. Re-open only to add a store field instead. - D8 — README gets the R3e section;
trend-spotter.mdgets one prose line; the SSOT is NOT touched? YES (rec). The diff is not a scoring concern — touchingtrend-scoring-modes.mdwould be scope creep. Re-open only to add an SSOT note. - D9 — commit split? Docs commit first, then one code commit (rec) — the diff (helpers + render + CLI wiring + gate) is one coherent feature. Re-open only for a helpers-then-wiring split.
9. Light-Voyage review — folded
Three Opus reviewers ran COLD on this brief + the plan against live scripts/trends/ code (scope-guardian,
brief-reviewer, plan-critic — the R3c/R3d discipline). Verdicts: scope-guardian MIXED · brief-reviewer
PROCEED_WITH_RISKS · plan-critic REWORK (0.88). They converged on 2 MAJOR (both re-verified against
live code before folding) + 4 MINOR. Every line-cite, the floors, the regex/lex/hook safety, and the §3 scope
fence were confirmed correct by all three and left untouched.
MAJOR-1 — a hard schema literal breaks at the 1→2 bump (the §6/Step-1 "no existing assertion breaks" scoping
missed it). tests/brief.test.ts:574 is assert.equal(BRIEF_SCHEMA_VERSION, 1) — a hard literal, not the
constant-tracking RegExp at :163 (new RegExp("\\nschemaVersion: " + BRIEF_SCHEMA_VERSION + "\\n") auto-tracks).
It lives in the rankForBrief — no schema/score mutation block (:568-577), outside the frontmatter tests
§6/plan-Step-1 enumerated, so the "every frontmatter assertion auto-tracks" claim did not cover it. Resolution:
the GREEN schema bump (BRIEF_SCHEMA_VERSION = 2) must also flip :574 → , 2) in the same step (plan
Step 3). :575 (assert.equal(SCHEMA_VERSION, 4), the store schema) is untouched. Folded into plan Step 3 + R1 +
Step 1's enumeration. (Swept live: :574 is the only hard BRIEF_SCHEMA_VERSION literal in the suite;
cli.test.ts:102 asserts the store persisted.schemaVersion === SCHEMA_VERSION (4) — unrelated, stays green.)
MAJOR-2 — the --json summary would diverge from the file frontmatter on day-2 (the "one source" invariant).
The file's frontmatter summary: is built inside renderBrief (brief.ts:265), which Step 3 routes through
briefSummary(ranking, diff) → on day-2 it carries the N nye siden sist. marker. But the CLI's --json
summary field reads a separate const summary = briefSummary(ranking) (cli.ts:350, comment // SAME source the frontmatter carries) that Step 4 left unthreaded → no marker. cli.test.ts:268 asserts
fileFrontmatter.summary === json.summary ("one source") → would break on day-2. Resolution: Step 4
changes cli.ts:350 to briefSummary(ranking, diff) (the diff is in scope — Step 4 computes it between the
ranking at :339 and the render at :340). Safe on day-1: priorDate === null ⇒ the marker is suppressed ⇒
byte-identical to today's string. Folded into plan Step 4 + the files-touched table.
MINOR (folded):
- M1 —
BriefDiffis an interface (type-only export). Anywhere it is referenced as a type (tests orcli.ts), import it withimport type { BriefDiff }, never a value import — under Node16 ESM + tsx a type-only export is stripped from the emitted JS, so a value-import named binding fails to resolve at module-load (the same Phase-A hazard as a missing named import). The plan's Step-4 code does not annotate: BriefDiff(it infers fromdiffSurfaced's return) and the SC tests pass object literals — so in practice noBriefDiffimport is needed; the rule is the guardrail if one is added. Folded into plan Step 2/4. - M2 — SC9's prior brief is a real renamed brief, not a hand-fixture (rename-real-write). Replace the "pre-write
a
<prior-date>.mdfixture carrying asurfaced:line" mechanism (plan Step 4 note / Phase B / Step 7) with: runbriefonce (writes${today}.mdwith a genuinesurfaced:line),mv ${today}.md → 2026-06-20.md(a fixed past date) in the same--out, then runbriefagain. This (a) closes the write→read loop clock-free (notoday()manipulation), and (b) guarantees the priorsurfaced:ids are real store ids (they came from a real run), socarriedis non-trivial andaddedis exactly the newly-captured trend — a hand-fixture risks an id mismatch that makes everything read as added/dropped (a weaker, possibly-vacuous test). Folded into plan Step 4 note, Phase B, Step 7, SC9. - M3 — SC1 "repeated id" wording.
diffSurfacedusescurrentIds.filter(...)/priorIds.filter(...), which preserve within-list duplicates — so "not double-counted" is wrong as within-list dedup. What theSetmembership actually guarantees is cross-partition disjointness (added/carried/dropped are mutually exclusive). Reworded in §6 SC1 + plan Step 1/verification. (ProductionsurfacedIdsyields distinct ids, so within-list dups never arise.) - M4 — §3 "empty
surfaced:" self-contradiction. The barerenderBrief(ranking)default-diff prose said it yields "an emptysurfaced:reflecting the ranking" — contradictory:surfaced:is alwayssurfacedIds(ranking).join(",")(non-empty for a non-empty ranking; blank only for an empty store), independent of the diff; the default empty diff only drives the_Første brief._section branch. Reworded in §3 (therenderBriefdefault-diff bullet).
Confirmed correct by all three — left untouched: every line-cite (brief.ts:23/204/265/305,
cli.ts:339/340/350/352, session-start.mjs:60-77/534-536); the floors (TRENDS_TESTS_FLOOR 216 @ live :716,
ASSERT_BASELINE_FLOOR 117 @ live :1473 → 123 = 117 + 6 unconditional 16n emitters); Section 16m is the last
trends section (16n sits between its echo "" and Section 18); the 6 ASCII sentinels are non-vacuous; the
/^surfaced: *([^\n]*)/m regex, the ISO-lex compare, the ^surfaced: ≠ ^summary: hook-safety, the --json
shape, and the same-day-determinism strict <.