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
35 KiB
Plan — RE-R3e: brief history + day-over-day diff (R3 slice d)
Brief:
docs/research-engine/brief-re-r3e.md. Slice: RE-R3e (research-engine rung-2 — R3 slice (d): brief history + day-over-day diff). Closes hull #7 ("ingen brief-historikk") — each brief records the trends it showed (surfaced:frontmatter) and renders "Nytt siden sist" against the most recent prior brief. Zero new files — pure EDITs (the two tracked slice docs aside). TDD-order (two-phase RED — light-Voyage discipline, inherited): Step 1 records RED in two phases — (A)diffSurfaced/parseSurfacedFrontmatter/selectPriorBriefFile(+BriefDiff) are NEW named exports of the EXISTINGbrief.ts; Node16 ESM throws a missing named import at module-load (everybrief.test.tstest would error, not assert), so land non-throwing stubs first (constant returns;renderBrief/briefSummarygain an optionaldiffparam ignored by the stub render), then record value-assertion RED against them (the constant stubs fail the diff/parse/select/section/marker assertions); (B) the CLI two-day diff test is value-RED against the existingbriefhandler (nosurfaced:write, no prior read, nodiffin--jsontoday). Then GREEN: realdiffSurfaced/parseSurfacedFrontmatter/selectPriorBriefFile→surfaced:frontmatter +BRIEF_SCHEMA_VERSION1→2 →## 🆕 Nytt siden sistsection → summary marker →cli.tsprior-discovery +--json diff→ wiretrend-spotter.md(prose) + README → gate floors + Section 16n → behavioural → land. Counts recounted live at land, never pinned/guessed. Architectural decisions (CONFIRMED, AskUserQuestion 2026-06-26): SD1 frontmattersurfaced:(no sidecar) · SD2addedw/ titles +droppedas a count (brief.tsstore-free). Go-gate D1–D9 baked to recommended defaults (brief §8). Light-Voyage: scope-guardian / brief-reviewer / plan-critic to run on these drafts; findings folded in §Plan-critic — folded before the code commit.
Goal
Turn the dated morning brief from a standalone daily snapshot into a history rung with a day-over-day diff.
Each brief persists the set of trend ids it showed into its YAML frontmatter (surfaced: <id-csv>), bumping
BRIEF_SCHEMA_VERSION 1→2 (the store's SCHEMA_VERSION stays 4 — no store field). The next brief discovers
the most recent prior dated file (strictly < today), parses its surfaced: line, and renders a
## 🆕 Nytt siden sist section — added (in today, not prior — the headline, with titles resolved from the
ranking), carried/dropped as a one-line count — plus a N nye siden sist. marker on the one-line summary the
SessionStart hook already surfaces. The diff is a pure, render-time layer: rankForBrief and the R3a
composite / R3d temporal overlay are unchanged; brief.ts stays store-free and fs-free (the directory +
file reads live at the cli.ts edge, injected like today/pillars). No new agent/command/reference/module/
file; types.ts/store.ts/score.ts/item.ts/schedule.ts/run-daily.sh + the hook untouched.
Files touched (exhaustive — for scope-guardian)
| File | Change | SC |
|---|---|---|
scripts/trends/src/brief.ts |
EDIT — BRIEF_SCHEMA_VERSION 1→2; add BriefDiff interface + pure diffSurfaced(currentIds, priorIds, priorDate) + parseSurfacedFrontmatter(md) + selectPriorBriefFile(filenames, today); renderBrief gains optional diff? → emits surfaced: frontmatter line (before schemaVersion:) + the ## 🆕 Nytt siden sist section (before Topp-treff); briefSummary gains optional diff? → N nye siden sist. marker; renderBrief passes diff through to briefSummary |
SC1–SC8 |
scripts/trends/src/cli.ts |
EDIT — brief handler: readdirSync(outDir) (existsSync-guarded) → selectPriorBriefFile → readFileSync + parseSurfacedFrontmatter → diffSurfaced(surfacedIds(ranking), priorIds, priorDate) → renderBrief(ranking, diff); thread diff into the shared summary at :350 (briefSummary(ranking, diff) — one-source, MAJOR-2); --json gains diff:{priorDate,added,carried,dropped} (counts); console line appends , N nye siden sist; add readdirSync to the node:fs import |
SC9 |
scripts/trends/tests/brief.test.ts |
EDIT — diffSurfaced unit (SC1), parseSurfacedFrontmatter unit (SC2), selectPriorBriefFile unit (SC3), surfaced: frontmatter + round-trip (SC4), ## 🆕 Nytt siden sist four branches (SC5), summary marker + the briefSummary(r)===briefSummary(r,empty) invariant (SC6), BRIEF_SCHEMA_VERSION===2 (SC7), determinism-with-diff (SC8). Stubs imported in Phase A. No existing assertion breaks (verified §Step 1) |
SC1–SC8 |
scripts/trends/tests/cli.test.ts |
EDIT — subprocess two-day sequence: day-1 brief writes surfaced:; day-2 over a +1-trend store → Nytt siden sist (<day1>) lists the new trend, --json diff.added≥1, console N nye siden sist; first run → diff.priorDate===null; custom --out isolates discovery |
SC9 |
scripts/trends/src/types.ts · store.ts · score.ts · item.ts · schedule.ts · run-daily.sh |
UNTOUCHED — no data-shape/scoring/store/scheduler change (SCHEMA_VERSION 4 held in types.ts; the R3c scheduler suite + R3d ranking stay green untouched). Listed to assert they are not in scope. |
— |
hooks/scripts/session-start.mjs + its tests |
UNTOUCHED — the surfaced: frontmatter line is ^surfaced:-keyed (the ^summary:-anchored extractYaml cannot match it); the summary marker is "/\n-free. No hook edit/test; the hook suite is a land-time regression check. |
— |
agents/trend-spotter.md |
EDIT (prose-only, minimal) — one line: the brief now records its shown set + renders a day-over-day diff ("Nytt siden sist"); capture path unchanged. Domain-general. | — |
scripts/trends/README.md |
EDIT — new ## Brief history + diff (RE-R3e) between the R3d temporal-overlay section and ## Tests: surfaced: record, selectPriorBriefFile strict-prior + same-day determinism, diffSurfaced partitions, section + summary marker, BRIEF_SCHEMA_VERSION 1→2 (artifact-only) |
— |
scripts/test-runner.sh |
EDIT — TRENDS_TESTS_FLOOR (live :716, 216)→recount + breakdown comment; NEW unconditional Section 16n between 16m's echo "" and Section 18; ASSERT_BASELINE_FLOOR (live :1473) 117→123; header-enum chain (:53-64) + Section-18 floor-history narration (ends "= 117") |
SC10 |
docs/research-engine/{brief,plan}-re-r3e.md |
NEW — slice docs (TRACKED, like docs/second-brain/*) |
— |
STATE.md |
EDIT at land — Telling-block reconcile (trends floor, ASSERT floor 123, gate total; BRIEF_SCHEMA_VERSION 1→2; store schema unchanged v4; correct stale line-cites to live :716/:1473). Land bookkeeping, LOCAL-ONLY. |
— |
Not touched (scope fence): types.ts/store.ts/score.ts/item.ts (no data-shape/scoring change;
SCHEMA_VERSION 4) · schedule.ts/run-daily.sh + their tests (R3c untouched — the nightly run gets the diff
internally; no scheduler edit) · the SessionStart hook + its tests (R3e adds no field the hook reads; surfaced:
- marker are regex-safe) ·
references/trend-scoring-modes.md(the diff is not a scoring concern) ·config/*·commands/*(29) ·agents/*count (19 —trend-spotter.mdis a prose EDIT) ·references/*count (27) ·.gitignore(no new artifact — the brief files already live under the gitignored data dir).SCHEMA_VERSION(4).
Step 1 — (RED, two phases) failing tests across brief/cli
Phase A — stub-first, then value-assertion RED (brief.test.ts imports the new brief.ts exports):
- Land non-throwing stubs so the static imports resolve (Node16 ESM links named imports before any test runs):
in
brief.ts— addexport interface BriefDiff, andexport function diffSurfaced(): BriefDiff { return { priorDate: null, added: [], carried: [], dropped: [] }; }export function parseSurfacedFrontmatter(): string[] { return []; }export function selectPriorBriefFile(): string | null { return null; }(each a constant stub ignoring its args). Add an optionaldiff?: BriefDiffparam torenderBriefandbriefSummary, wired but inert in the stub:renderBriefdoes NOT yet emit thesurfaced:line or the section;briefSummarydoes NOT yet emit the marker. (Keeps the static signatures stable for the RED tests while the behaviour is still absent → value-RED, not type-RED.)BRIEF_SCHEMA_VERSIONis still 1 in Phase A (so the SC7=== 2assertion is RED).
brief.test.ts(value-RED against the stubs):- diffSurfaced (SC1) —
diffSurfaced(["a","b","c"],["b","c","d"],"2026-06-25")expects{priorDate:"2026-06-25",added:["a"],carried:["b","c"],dropped:["d"]}(stub returns all-empty/null → RED);diffSurfaced(["a","b"],[],null)expects{priorDate:null,added:["a","b"],carried:[],dropped:[]}; a repeated id not double-counted. - parseSurfacedFrontmatter (SC2) — a full frontmatter string with
surfaced: 1a2b,3c4d,5e6f→["1a2b", "3c4d","5e6f"](stub[]→ RED); blanksurfaced:→[]; absent line →[]; trims whitespace; does not matchsummary:/store:. - selectPriorBriefFile (SC3) —
(["2026-06-24.md","2026-06-25.md","2026-06-26.md","README.md","2026-06-30.md"], "2026-06-26")→"2026-06-25.md"(stubnull→ RED); empty list / none< today→null. - frontmatter
surfaced:+ round-trip (SC4) —renderBrief(r, diff)includes\nsurfaced:+ thesurfacedIds(r).join(",")value, before\nschemaVersion: 2\n;parseSurfacedFrontmatter(renderBrief(r,d)) === surfacedIds(r); empty store →surfaced:blank. Stub render emits neither the line norschemaVersion: 2→ RED. - section (SC5) —
renderBrief(r, diff)contains## 🆕 Nytt siden sist; the four branches (first-brief+ added →Første brief — alt nedenfor er nytt; empty first →Første brief.; prior+added → the added title +båret over+ikke vist i dag; prior+no-added →Ingenting nytt siden <date>). Stub render omits the section → RED. Assert the section precedes## 🎯 Topp-treff(index check). - summary marker (SC6) —
briefSummary(r, {priorDate:"2026-06-25",added:["x"],carried:[],dropped:[]})ends with1 nye siden sist.(stub omits → RED);briefSummary(r, {priorDate:null,…})andbriefSummary(r, {…,added:[]})have no marker;briefSummary(r) === briefSummary(r, emptyDiff)(the invariant that keeps the existing:166-171test green); no"/\n. - schema (SC7) —
BRIEF_SCHEMA_VERSION === 2(stub still 1 → RED);SCHEMA_VERSION === 4. - determinism (SC8) —
renderBrief(r, d) === renderBrief(r, d); with a fixedd, stable bytes.
- diffSurfaced (SC1) —
- Existing assertions — one hard literal flips with the bump; the rest auto-track (verified live, MAJOR-1):
the frontmatter tests are
:158-160(startsWith "---\n"— unaffected),:161-164(schemaVersion:built from the importedBRIEF_SCHEMA_VERSIONconstant via RegExp:163— auto-tracks 1→2;date:/store:unaffected),:166-173(summary: === briefSummary(r)— preserved by the SC6 invariant: with no prior the marker is suppressed, sobriefSummary(r)===briefSummary(r,emptyDiff)), the determinism pair:182-184(both sides default to the empty diff → still equal), and theranking:descriptor tests (descriptor unchanged by R3e → unaffected). The one break::574assert.equal(BRIEF_SCHEMA_VERSION, 1)— a hard literal in therankForBrief — no schema/score mutationblock (:568-577), outside the frontmatter set, which the GREEN bump must flip to, 2)in Step 3 (:575assert.equal(SCHEMA_VERSION, 4)stays — store schema untouched). Swept::574is the only hardBRIEF_SCHEMA_VERSIONliteral in the suite. The new section is additive, asserted only by new tests; no test pins the intro→Topp-treff adjacency (verified — all body assertions are substring/match).
Phase B — subprocess value-RED against the existing CLI handler (no new import; the handler ignores prior briefs today):
cli.test.ts— a two-day sequence sharing one--out <tmp>dir: (1) seed a store, runbrief --pillars ai,gov --out <tmp> --store <s> --json→ today the written.mdhas nosurfaced:line and the--jsonhas nodiffkey → RED against the day-1 assertions (surfaced:present,--json.diff.priorDate === null). (2)captureone new on-pillar trend, then rename the day-1.mdto a fixed past date (mvit to2026-06-20.mdin the same<tmp>— the rename-real-write mechanism, M2) and run a secondbrief→ against the existing handler it reads no prior, renders noNytt siden sist (<date>)section,--jsonhas nodiff→ RED. Diff content lives in the written.mdbody, so the assertionreadFileSync(path)checks## 🆕 Nytt siden sist; thediffcounts are read from--json.
RED proof (record in commit, two phases): Phase A — after the non-throwing stubs land, (cd scripts/trends && npm test) fails the diff/parse/select/frontmatter/section/marker/schema cases on value assertions against the
constant stubs (not module-not-found). Phase B — the cli.test two-day cases fail on the missing surfaced:/
diff/section. The plan does not claim a single "everything fails before any code" run.
Step 2 — (GREEN) brief.ts — the three pure helpers
Replace the Phase-A stubs with the real, pure implementations (all no clock/fs/env):
export interface BriefDiff { priorDate: string | null; added: string[]; carried: string[]; dropped: string[]; }diffSurfaced(currentIds, priorIds, priorDate):
Order-stable (filters preserve input order); emptyconst prior = new Set(priorIds); const cur = new Set(currentIds); return { priorDate, added: currentIds.filter((id) => !prior.has(id)), carried: currentIds.filter((id) => prior.has(id)), dropped: priorIds.filter((id) => !cur.has(id)), };priorIds⇒added===currentIds,dropped===[].parseSurfacedFrontmatter(md):
Absent/blank/malformed →const m = md.match(/^surfaced: *([^\n]*)/m); if (!m) return []; return m[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);[](mirrors the hook'sextractYamlline-anchoring; never throws).selectPriorBriefFile(filenames, today):
ISO dates sort lexicographically, soconst todayFile = `${today}.md`; return ( filenames .filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f) && f < todayFile) .sort() .pop() ?? null );f < todayFile= date< today(strict — excludes today + future); greatest remaining = the most recent prior. Mirrorssession-start.mjs:63-66, minus today. Make the Phase-A diff/parse/select cases green.
Step 3 — (GREEN) brief.ts — frontmatter surfaced: + schema bump + the section + the marker
BRIEF_SCHEMA_VERSION = 2(brief.ts:23) — and flip the one hard test literal in the same step (MAJOR-1):tests/brief.test.ts:574assert.equal(BRIEF_SCHEMA_VERSION, 1)→assert.equal(BRIEF_SCHEMA_VERSION, 2). (The RegExp at:163and the new SC7 already track the constant;:575assert.equal(SCHEMA_VERSION, 4)is untouched.)renderBrief(ranking, diff: BriefDiff = { priorDate: null, added: [], carried: [], dropped: [] })— the default empty diff keeps single-arg call sites valid. Two additive emissions:- In the frontmatter block (
brief.ts:263-269), insert before theschemaVersion:line:lines.push(\surfaced: ${surfacedIds(ranking).join(",")}`);(empty store ⇒surfaced:blank). TheschemaVersion:line now renders2` via the bumped constant. - After the intro line (
brief.ts:273-276) and before## 🎯 Topp-treff(:278), emit the section:
(lines.push(diff.priorDate !== null ? `## 🆕 Nytt siden sist (${diff.priorDate})` : "## 🆕 Nytt siden sist"); if (diff.priorDate === null) { lines.push(diff.added.length > 0 ? "_Første brief — alt nedenfor er nytt._" : "_Første brief._", ""); } else if (diff.added.length === 0) { lines.push(`_Ingenting nytt siden ${diff.priorDate}._`, `_${diff.carried.length} båret over, ${diff.dropped.length} ikke vist i dag._`, ""); } else { const byId = new Map( [...ranking.topMatches, ...ranking.singleMatches, ...ranking.olderMatched].map((e) => [e.trend.id, e]), ); for (const id of diff.added) { const e = byId.get(id); if (e) lines.push(renderBulletEntry(e)); } lines.push(`_${diff.carried.length} båret over, ${diff.dropped.length} ikke vist i dag._`, ""); }renderBulletEntryis the existing bullet renderer — reused, no new format.) An added id always resolves (added ⊆ surfacedIds ⊆ ranking entries); theif (e)guard keeps it total. - Pass the diff through:
briefSummary(ranking, diff)at the frontmattersummary:line (brief.ts:265).
- In the frontmatter block (
briefSummary(ranking, diff?: BriefDiff)(brief.ts:204): after building the headline, append the marker:
Suppressed on the first brief / when nothing new; carries noconst delta = diff && diff.priorDate !== null && diff.added.length > 0 ? ` ${diff.added.length} nye siden sist.` : ""; return `${...existing headline...}${delta}`;"/\n(the existing summary already guarantees this — the marker adds only digits + ASCII words + a period). The no-diff call (briefSummary(ranking)) yields exactly the pre-R3e string (the SC6 invariant). Make the Phase-A frontmatter/section/marker/schema cases green.
Step 4 — (GREEN) cli.ts — prior-brief discovery + the diff in --json
- Add
readdirSyncto thenode:fsimport (cli.ts:51). - In the
briefhandler, between the ranking (cli.ts:339) and the render (:340):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; } // unreadable prior ⇒ first-brief path const diff = diffSurfaced(todayIds, priorIds, priorDate); const md = renderBrief(ranking, diff); - Import
diffSurfaced,parseSurfacedFrontmatter,selectPriorBriefFilefrom./brief.js(cli.ts:71). (If any code annotates: BriefDiff, import it viaimport type— it is an interface, stripped from emitted JS, so a value-import fails at module-load (M1); the code above infers the type fromdiffSurfaced's return, so noBriefDiffimport is actually needed.) - Thread the diff into the shared summary (
cli.ts:350, MAJOR-2): changeconst summary = briefSummary(ranking)→const summary = briefSummary(ranking, diff). The frontmattersummary:(built insiderenderBrief→briefSummary(ranking, diff)) and the--jsonsummary(read from this var) must stay one source (cli.test.ts:268); without this, day-2's file carries theN nye siden sist.marker but--json.summarywould not. Safe on day-1:priorDate === null⇒ marker suppressed ⇒ byte-identical to the pre-R3e string. --json(cli.ts:352): adddiff: { priorDate: diff.priorDate, added: diff.added.length, carried: diff.carried.length, dropped: diff.dropped.length }.- The non-JSON console line (
cli.ts:355): append${diff.added.length > 0 && diff.priorDate !== null ? \, ${diff.added.length} nye siden sist` : ""}`. - No new flag, no new exit code. Note (Phase-B test mechanism — rename-real-write, M2):
today()is wall-clock, so a same-process two-day sequence cannot advance the date. Rather than hand-author a<prior>.mdfixture (which risks an id mismatch — itssurfaced:ids would not be real store ids, so every trend reads as added/dropped, a weak/vacuous test), the cli.test runsbrieffor real (writing${today}.mdwith a genuinesurfaced:line =surfacedIds(ranking)), renames it to a fixed past date (mv ${out}/${today}.md ${out}/2026-06-20.md), then runsbriefagain in the same--out. The second run discovers2026-06-20.mdas the strict-prior, parses its real ids, and diffs against today's cohort — proving discovery + parse + diff clock-free, withcarried/addedthat are exactly right (id-matched). Capture one new on-pillar trend between the runs → that trend is the soleadded. Make the Phase-B cli cases green.
Step 5 — wire trend-spotter.md (prose) + README
agents/trend-spotter.md— one prose line (no batch-shape change): the morning brief now records the 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 polling/capture path is unchanged. Domain-general (Section 17).scripts/trends/README.md— add## Brief history + diff (RE-R3e)between the R3d temporal-overlay section and## Tests: thesurfaced:frontmatter record (one self-describing artifact,BRIEF_SCHEMA_VERSION 1→2, storeSCHEMA_VERSIONstays 4),selectPriorBriefFilestrict-< todaydiscovery (same-day re-run determinism), thediffSurfacedpartitions (added/carried/dropped), the## 🆕 Nytt siden sistsection (added with titles, carried/dropped as a count, "ikke vist i dag" framing) + theN nye siden sist.summary marker the SessionStart hook surfaces for free.
Step 6 — gate: floors + new unconditional Section 16n
In scripts/test-runner.sh:
- Set
TRENDS_TESTS_FLOOR(live:716, currently 216) to thetests Nline reported by(cd scripts/trends && npm test)after Steps 1–5 — recounted live, NOT additive-guessed. Stays inside the deps guard. Append+ RE-R3e: brief +N, cli +N (brief history + diff)to the inline breakdown comment. - Add Section 16n ("Trends Brief History / Diff", RE-R3e), mirroring Section 16m (unconditional,
deps-absent-safe, pure
grep -qF/self-test, notsx, all literals ASCII — the🆕emoji is NEVER grepped; the shell stays ASCII-clean for bash 3.2set -u). Placement (verify live): between Section 16m's trailingecho ""and the Section 18 header — anti-erosion stays last. Six unconditional checks, the self-test emitting one pass/fail like 16m: (1) a non-vacuity self-test (a probe carryingdiffSurfacedaccepted, one without rejected); (2)grep -qF 'export function diffSurfaced' scripts/trends/src/brief.ts; (3)grep -qF 'parseSurfacedFrontmatter' scripts/trends/src/brief.ts; (4)grep -qF 'Nytt siden sist' scripts/trends/src/brief.ts(the section header literal — ASCII portion only); (5)grep -qF 'selectPriorBriefFile' scripts/trends/src/cli.ts(the diff wiring in the CLI); (6)grep -qF 'surfaced: ' scripts/trends/src/brief.ts(the frontmatter emit). - Bump
ASSERT_BASELINE_FLOOR(live:1473, currently 117) → exactly 123 (117 + the 6 new unconditional 16n emitters; the self-test emits one pass/fail like 16m, so 123 is deterministic — "live recount" is the safety net, not a guess). Insert the 16n clause into the header-enumeration prose chain (:53-64) before "…the assertion-count anti-erosion floor (SC6) in Section 18," preserving sentence flow. Append the RE-R3e (→123) narration to the Section-18 floor-history comment (which ends "= 117"). - NOT touched here: the hook suite (no
HOOK_TESTS_FLOORintest-runner.sh; R3e adds no hook test). It must still pass untouched (node --test hooks/scripts/__tests__/*.test.mjs) as a regression sanity at land.
Step 7 — behavioural verification
(cd scripts/trends && npm install) if needed, then run brief §7's five behavioural steps with a unique tmp dir
(no rm): D=/tmp/r3e-mb-$$; S=/tmp/r3e-$$.json. Seed an on-pillar store; brief --pillars … --out "$D" --store "$S" → frontmatter carries surfaced: <ids> + schemaVersion: 2, the section says Første brief — alt nedenfor er nytt; rename that real brief to a fixed past date (mv "$D/$(ls "$D")" "$D/2026-06-20.md"),
capture a new on-pillar trend, and re-run brief → ## 🆕 Nytt siden sist (2026-06-20) lists the added trend +
N båret over, M ikke vist i dag, --json diff.added ≥ 1 (the rename-real-write path, M2 — the prior's ids
are real, so carried/added are id-exact); same-day re-run → cmp the two ${day}.md byte-identical; confirm
score.composite unchanged after the briefs (only surfacedCount/lastSurfacedAt advance); strip the surfaced:
line from the renamed prior (a pre-R3e brief) and re-run → every trend reads as added (graceful degrade). Run
full bash scripts/test-runner.sh → FAIL=0 (ASSERT_BASELINE_FLOOR 123, trends ≥ new floor, Section 16n green,
Section 17 de-niche green, counts 29/19/27); run node --test hooks/scripts/__tests__/*.test.mjs → still green
(untouched regression); confirm schedule.test/run-daily.test still green (R3c untouched).
Step 8 — land
Recount all touched floors live; reconcile STATE.md "Telling" block (trends N/N, ASSERT floor 123, gate total;
BRIEF_SCHEMA_VERSION 1→2; store schema unchanged v4; correct the stale :713/:1403 cites to live
:716/:1473). Commit order (house style): (1) docs commit docs/research-engine/{brief,plan}-re-r3e.md
(no suffix, tracked); (2) code commit — brief.ts + cli.ts + brief.test.ts + cli.test.ts +
agents/trend-spotter.md + scripts/trends/README.md + scripts/test-runner.sh with [skip-docs] (D9: single
code commit — the diff is one coherent feature). Push freely (window lifted; gitleaks at commit; origin =
PUBLIC open/ — STATE/*.local.* never pushed). No version bump (additive; v0.5.2 dev) — note
BRIEF_SCHEMA_VERSION 1→2 is the artifact schema, not the plugin version.
Verification (testable)
| SC | Check | Command | Expected |
|---|---|---|---|
| — | RED Phase A | (cd scripts/trends && npm test) after the stubs |
diff/parse/select/frontmatter/section/marker/schema cases fail on value assertions vs the constant stubs (not module-not-found) |
| — | RED Phase B | npm test (cli.test) before the wiring |
the two-day cases fail on the missing surfaced:/diff/section |
| SC1 | diffSurfaced partitions | npm test (brief.test) |
added/carried/dropped order-stable; empty prior ⇒ all added; repeated id once |
| SC2 | parseSurfacedFrontmatter | npm test (brief.test) |
csv → ids; blank/absent/malformed → []; line-anchored (no summary: mismatch) |
| SC3 | selectPriorBriefFile | npm test (brief.test) |
greatest < today; excludes today + future; ignores non-dated; none → null |
| SC4 | surfaced: + round-trip |
npm test (brief.test) |
one surfaced: <csv> line = surfacedIds(r).join(","), before schemaVersion: 2; round-trips; empty store → blank |
| SC5 | Nytt siden sist (4 branches) | npm test (brief.test) |
## 🆕 Nytt siden sist; first-brief/empty/added/no-added branches; section precedes Topp-treff |
| SC6 | summary delta marker | npm test (brief.test) |
N nye siden sist. when prior+added; absent on first/no-added; briefSummary(r)===briefSummary(r,empty); no "/\n |
| SC7 | schema boundary | npm test (brief.test) |
BRIEF_SCHEMA_VERSION 2; SCHEMA_VERSION 4; no score.composite mutation after a brief |
| SC8 | determinism + same-day | npm test (brief.test) |
same (store,pillars,today,opts,diff) → byte-identical; same-day re-run picks the same prior (strict <) |
| SC9 | CLI diff wiring | npm test (cli.test) |
two-day (rename-real-write: real day-1 brief renamed to 2026-06-20.md): Nytt siden sist (<day1>) lists the new trend; --json diff counts; first run priorDate null; --out isolates |
| SC10 | gate + wiring + de-niche | bash scripts/test-runner.sh |
FAIL=0; trends ≥ floor; Section 16n green; ASSERT_BASELINE_FLOOR=123; Section 17; counts 29/19/27; hook suite green |
Risks
- R1 — adding
surfaced:/ bumping the schema breaks pinned tests. Mitigated (verified live): the frontmatterschemaVersion:test uses the importedBRIEF_SCHEMA_VERSIONconstant (auto-tracks 1→2); thesummary:-equality test is preserved by the SC6 invariant (briefSummary(r)===briefSummary(r,empty));startsWith "---\n"and thedate:/ranking:tests are unaffected. The one hard break (MAJOR-1)::574assert.equal(BRIEF_SCHEMA_VERSION, 1)— a literal outside the frontmatter set — which Step 3 flips to, 2)with the bump (:575SCHEMA_VERSION === 4stays). Step 1 enumerates every surviving assertion + this one break. - R2 — inserting a section before Topp-treff breaks an ordering assertion. Mitigated (verified): no existing
test pins the intro→Topp-treff adjacency (all body assertions are substring/
match); the new section is asserted only by new tests. SC5 pins the section-before-Topp index in the new suite. - R3 — the
surfaced:CSV could collide with a comma in an id. Mitigated: real ids are 12-hex (store.ts:69-72) — comma-free; the join/split is unambiguous in production. The brief.test fixtures use comma-free ids for the round-trip. (TestmkTrendids aretitle|url; the round-trip unit uses clean ids.) - R4 — same-day re-run picks its own just-written file → self-diff (empty) → non-deterministic vs the first
run. Mitigated:
selectPriorBriefFilefiltersf < ${today}.md(strict), so the same-day file is excluded and the re-run picks the same true-prior → byte-identical (SC8). This is the exact R3c SC7 guarantee, preserved. - R5 — a malformed / hand-edited / pre-R3e prior brief crashes the diff. Mitigated:
parseSurfacedFrontmatterreturns[]on absent/blank/malformed (never throws); the clitry/catchdegrades any fs error to the empty-prior (first-brief) path. SC2 + behavioural step 5 cover it. - R6 — the summary marker breaks the SessionStart
extractYamlregex. Mitigated:N nye siden sist.is digits + ASCII words + a period — no", no\n; SC6 asserts the invariant; the hook suite is a land-time regression check. The hook readsdate+summaryonly;surfaced:is^surfaced:-keyed (the^summary:regex cannot match it). - R7 — the
🆕emoji in the gate crashes bash 3.2set -u. Mitigated: Section 16n greps ONLY ASCII literals (export function diffSurfaced,parseSurfacedFrontmatter,Nytt siden sist,selectPriorBriefFile,surfaced:); the emoji lives only inbrief.tssource + rendered output, asserted by the TS tests. - R8 —
BRIEF_SCHEMA_VERSIONbump misread as a store-schema/plugin-version change. Mitigated: it is the artifact frontmatter version (brief.ts:23, distinct from the store'sSCHEMA_VERSION— the comment says so); storeSCHEMA_VERSIONstays 4; no plugin version bump (additive). README + §4 state the boundary. - R9 — the diff couples
brief.tsto fs (directory read) → breaks the purity claim. Mitigated (SD2): the three new helpers are pure (string/array in, value out); thereaddirSync/readFileSynclive incli.ts(the edge), injected exactly liketoday/pillars.brief.ts's "No fs" header claim holds. - R10 — STATE's pinned floor line-cites (
:713/:1403) are stale (live:716/:1473). Mitigated: caught at brief time (the lines drifted when R3d added Section 16m + the floor-history narration); the plan cites live values; Step 8 corrects STATE. - R11 — a diff test that passes WITHOUT the feature (vacuous GREEN). Mitigated: SC1/SC2/SC3 are unit tests
of pure functions whose stubs return constants (true value-RED in Phase A); SC9's day-2 asserts a specific
added trend appears in
Nytt siden sist (<day1>)AND the--json diff.addedcount — both absent in the flag-ignoring handler (Phase B RED). No fixture is ordered-by-accident. - R12 —
--no-markdesync: the artifact recordssurfaced:but the store seen-log is not written. Accepted/ intended:surfaced:is a property of the rendered brief (what it showed),--no-markgoverns only the store mutation. The next diff reads the artifact, so it is correct regardless of--no-mark. SC4 (frontmatter) and the existing--no-marktest (cli.test:392-398) both hold.
Plan-critic — folded
Three Opus reviewers (scope-guardian, brief-reviewer, plan-critic) ran COLD on the brief + this plan against live
scripts/trends/. Verdicts: scope-guardian MIXED · brief-reviewer PROCEED_WITH_RISKS · plan-critic
REWORK (0.88) — converged on 2 MAJOR + 4 MINOR (all re-verified against live code before folding; full
rationale in brief-re-r3e.md §9).
- MAJOR-1 —
tests/brief.test.ts:574assert.equal(BRIEF_SCHEMA_VERSION, 1)is a hard literal (outside the frontmatter set §Step-1 enumerated; the:163RegExp auto-tracks). Folded: Step 3 flips it to, 2)with the constant bump; Step 1's enumeration + R1 now name it;:575(SCHEMA_VERSION === 4) stays. - MAJOR-2 —
cli.ts:350const summary = briefSummary(ranking)was left unthreaded → day-2--json.summarywould lose the marker the file's frontmatter carries, breaking thecli.test.ts:268"one source" invariant. Folded: Step 4 changes it tobriefSummary(ranking, diff)(safe day-1;priorDate===nullsuppresses the marker). - M1 (MINOR) —
BriefDiffis type-only →import typeif referenced (Step 2/4); the Step-4 code infers it, so no import is actually needed. - M2 (MINOR) — SC9 uses rename-real-write (run
brief,mv ${day}.md → 2026-06-20.md, re-run), not a hand-fixture → clock-free + id-exact prior. Folded: Step 4 note, Phase B, Step 7, SC9 row. - M3 (MINOR) — SC1 "repeated id not double-counted" reworded → cross-partition disjointness (filters preserve within-list dups; production ids are distinct). Brief §6 SC1 + Step 1.
- M4 (MINOR) — brief §3 "empty
surfaced:" contradiction reworded (thesurfaced:line is diff-independent =surfacedIds(ranking); the default empty diff only drives the_Første brief._section). Brief §3.
Confirmed correct (untouched): all line-cites, the floors (216 @ :716; 117 → 123 @ :1473), Section 16m as
the last trends section, the 6 ASCII sentinels' non-vacuity, the regex/ISO-lex/hook-safety/--json-shape/same-day
strict-< — verified by all three.