linkedin-studio/docs/research-engine/plan-re-r3e.md
Kjell Tore Guttormsen ddedb3d1de docs(linkedin-studio): RE-R3e — Nytt-siden-sist header carries the prior date (plan/brief fidelity)
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
2026-06-26 14:39:35 +02:00

35 KiB
Raw Permalink Blame History

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 EXISTING brief.ts; Node16 ESM throws a missing named import at module-load (every brief.test.ts test would error, not assert), so land non-throwing stubs first (constant returns; renderBrief/briefSummary gain an optional diff param 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 existing brief handler (no surfaced: write, no prior read, no diff in --json today). Then GREEN: real diffSurfaced/parseSurfacedFrontmatter/selectPriorBriefFilesurfaced: frontmatter + BRIEF_SCHEMA_VERSION 1→2 → ## 🆕 Nytt siden sist section → summary marker → cli.ts prior-discovery + --json diff → wire trend-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 frontmatter surfaced: (no sidecar) · SD2 added w/ titles + dropped as a count (brief.ts store-free). Go-gate D1D9 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 EDITBRIEF_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 SC1SC8
scripts/trends/src/cli.ts EDITbrief handler: readdirSync(outDir) (existsSync-guarded) → selectPriorBriefFilereadFileSync + parseSurfacedFrontmatterdiffSurfaced(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 EDITdiffSurfaced 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) SC1SC8
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 EDITTRENDS_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.md is 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 — add export interface BriefDiff, and
    • export 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 optional diff?: BriefDiff param to renderBrief and briefSummary, wired but inert in the stub: renderBrief does NOT yet emit the surfaced: line or the section; briefSummary does 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_VERSION is still 1 in Phase A (so the SC7 === 2 assertion 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); blank surfaced: []; absent line → []; trims whitespace; does not match summary:/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" (stub null → RED); empty list / none < todaynull.
    • frontmatter surfaced: + round-trip (SC4)renderBrief(r, diff) includes \nsurfaced: + the surfacedIds(r).join(",") value, before \nschemaVersion: 2\n; parseSurfacedFrontmatter(renderBrief(r,d)) === surfacedIds(r); empty store → surfaced: blank. Stub render emits neither the line nor schemaVersion: 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 with 1 nye siden sist. (stub omits → RED); briefSummary(r, {priorDate:null,…}) and briefSummary(r, {…,added:[]}) have no marker; briefSummary(r) === briefSummary(r, emptyDiff) (the invariant that keeps the existing :166-171 test 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 fixed d, stable bytes.
  • 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 imported BRIEF_SCHEMA_VERSION constant 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, so briefSummary(r)===briefSummary(r,emptyDiff)), the determinism pair :182-184 (both sides default to the empty diff → still equal), and the ranking: descriptor tests (descriptor unchanged by R3e → unaffected). The one break: :574 assert.equal(BRIEF_SCHEMA_VERSION, 1) — a hard literal in the rankForBrief — no schema/score mutation block (:568-577), outside the frontmatter set, which the GREEN bump must flip to , 2) in Step 3 (:575 assert.equal(SCHEMA_VERSION, 4) stays — store schema untouched). Swept: :574 is the only hard BRIEF_SCHEMA_VERSION literal 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, run brief --pillars ai,gov --out <tmp> --store <s> --json → today the written .md has no surfaced: line and the --json has no diff key → RED against the day-1 assertions (surfaced: present, --json.diff.priorDate === null). (2) capture one new on-pillar trend, then rename the day-1 .md to a fixed past date (mv it to 2026-06-20.md in the same <tmp> — the rename-real-write mechanism, M2) and run a second brief → against the existing handler it reads no prior, renders no Nytt siden sist (<date>) section, --json has no diff → RED. Diff content lives in the written .md body, so the assertion readFileSync(path) checks ## 🆕 Nytt siden sist; the diff counts 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):
    const 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)),
    };
    
    Order-stable (filters preserve input order); empty priorIdsadded===currentIds, dropped===[].
  • parseSurfacedFrontmatter(md):
    const m = md.match(/^surfaced: *([^\n]*)/m);
    if (!m) return [];
    return m[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);
    
    Absent/blank/malformed → [] (mirrors the hook's extractYaml line-anchoring; never throws).
  • selectPriorBriefFile(filenames, today):
    const todayFile = `${today}.md`;
    return (
      filenames
        .filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f) && f < todayFile)
        .sort()
        .pop() ?? null
    );
    
    ISO dates sort lexicographically, so f < todayFile = date < today (strict — excludes today + future); greatest remaining = the most recent prior. Mirrors session-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:574 assert.equal(BRIEF_SCHEMA_VERSION, 1)assert.equal(BRIEF_SCHEMA_VERSION, 2). (The RegExp at :163 and the new SC7 already track the constant; :575 assert.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 the schemaVersion: 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._`, "");
      }
      
      (renderBulletEntry is the existing bullet renderer — reused, no new format.) An added id always resolves (added ⊆ surfacedIds ⊆ ranking entries); the if (e) guard keeps it total.
    • Pass the diff through: briefSummary(ranking, diff) at the frontmatter summary: line (brief.ts:265).
  • briefSummary(ranking, diff?: BriefDiff) (brief.ts:204): after building the headline, append the marker:
    const delta = diff && diff.priorDate !== null && diff.added.length > 0
      ? ` ${diff.added.length} nye siden sist.` : "";
    return `${...existing headline...}${delta}`;
    
    Suppressed on the first brief / when nothing new; carries no "/\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 readdirSync to the node:fs import (cli.ts:51).
  • In the brief handler, 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, selectPriorBriefFile from ./brief.js (cli.ts:71). (If any code annotates : BriefDiff, import it via import type — it is an interface, stripped from emitted JS, so a value-import fails at module-load (M1); the code above infers the type from diffSurfaced's return, so no BriefDiff import is actually needed.)
  • Thread the diff into the shared summary (cli.ts:350, MAJOR-2): change const summary = briefSummary(ranking)const summary = briefSummary(ranking, diff). The frontmatter summary: (built inside renderBriefbriefSummary(ranking, diff)) and the --json summary (read from this var) must stay one source (cli.test.ts:268); without this, day-2's file carries the N nye siden sist. marker but --json.summary would not. Safe on day-1: priorDate === null ⇒ marker suppressed ⇒ byte-identical to the pre-R3e string.
  • --json (cli.ts:352): add diff: { 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>.md fixture (which risks an id mismatch — its surfaced: ids would not be real store ids, so every trend reads as added/dropped, a weak/vacuous test), the cli.test runs brief for real (writing ${today}.md with a genuine surfaced: line = surfacedIds(ranking)), renames it to a fixed past date (mv ${out}/${today}.md ${out}/2026-06-20.md), then runs brief again in the same --out. The second run discovers 2026-06-20.md as the strict-prior, parses its real ids, and diffs against today's cohort — proving discovery + parse + diff clock-free, with carried/added that are exactly right (id-matched). Capture one new on-pillar trend between the runs → that trend is the sole added. 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 (frontmatter surfaced:) 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: the surfaced: frontmatter record (one self-describing artifact, BRIEF_SCHEMA_VERSION 1→2, store SCHEMA_VERSION stays 4), selectPriorBriefFile strict-< today discovery (same-day re-run determinism), the diffSurfaced partitions (added/carried/dropped), the ## 🆕 Nytt siden sist section (added with titles, carried/dropped as a count, "ikke vist i dag" framing) + the N 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 the tests N line reported by (cd scripts/trends && npm test) after Steps 15 — 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, no tsx, all literals ASCII — the 🆕 emoji is NEVER grepped; the shell stays ASCII-clean for bash 3.2 set -u). Placement (verify live): between Section 16m's trailing echo "" 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 carrying diffSurfaced accepted, 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_FLOOR in test-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.shFAIL=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 frontmatter schemaVersion: test uses the imported BRIEF_SCHEMA_VERSION constant (auto-tracks 1→2); the summary:-equality test is preserved by the SC6 invariant (briefSummary(r)===briefSummary(r,empty)); startsWith "---\n" and the date:/ranking: tests are unaffected. The one hard break (MAJOR-1): :574 assert.equal(BRIEF_SCHEMA_VERSION, 1) — a literal outside the frontmatter set — which Step 3 flips to , 2) with the bump (:575 SCHEMA_VERSION === 4 stays). 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. (Test mkTrend ids are title|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: selectPriorBriefFile filters f < ${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: parseSurfacedFrontmatter returns [] on absent/blank/malformed (never throws); the cli try/catch degrades any fs error to the empty-prior (first-brief) path. SC2 + behavioural step 5 cover it.
  • R6 — the summary marker breaks the SessionStart extractYaml regex. 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 reads date+summary only; surfaced: is ^surfaced:-keyed (the ^summary: regex cannot match it).
  • R7 — the 🆕 emoji in the gate crashes bash 3.2 set -u. Mitigated: Section 16n greps ONLY ASCII literals (export function diffSurfaced, parseSurfacedFrontmatter, Nytt siden sist, selectPriorBriefFile, surfaced: ); the emoji lives only in brief.ts source + rendered output, asserted by the TS tests.
  • R8 — BRIEF_SCHEMA_VERSION bump misread as a store-schema/plugin-version change. Mitigated: it is the artifact frontmatter version (brief.ts:23, distinct from the store's SCHEMA_VERSION — the comment says so); store SCHEMA_VERSION stays 4; no plugin version bump (additive). README + §4 state the boundary.
  • R9 — the diff couples brief.ts to fs (directory read) → breaks the purity claim. Mitigated (SD2): the three new helpers are pure (string/array in, value out); the readdirSync/readFileSync live in cli.ts (the edge), injected exactly like today/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.added count — both absent in the flag-ignoring handler (Phase B RED). No fixture is ordered-by-accident.
  • R12 — --no-mark desync: the artifact records surfaced: but the store seen-log is not written. Accepted/ intended: surfaced: is a property of the rendered brief (what it showed), --no-mark governs only the store mutation. The next diff reads the artifact, so it is correct regardless of --no-mark. SC4 (frontmatter) and the existing --no-mark test (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-1tests/brief.test.ts:574 assert.equal(BRIEF_SCHEMA_VERSION, 1) is a hard literal (outside the frontmatter set §Step-1 enumerated; the :163 RegExp 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-2cli.ts:350 const summary = briefSummary(ranking) was left unthreaded → day-2 --json.summary would lose the marker the file's frontmatter carries, breaking the cli.test.ts:268 "one source" invariant. Folded: Step 4 changes it to briefSummary(ranking, diff) (safe day-1; priorDate===null suppresses the marker).
  • M1 (MINOR)BriefDiff is type-only → import type if 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 (the surfaced: 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.