# 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`/`selectPriorBriefFile` → `surfaced:` 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 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.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 `< today` → `null`.
  - **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)`:
  ```ts
  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 `priorIds` ⇒ `added===currentIds`, `dropped===[]`.
- `parseSurfacedFrontmatter(md)`:
  ```ts
  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)`:
  ```ts
  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). The
    `schemaVersion:` line now renders `2` via the bumped constant.
  - After the intro line (`brief.ts:273-276`) and **before** `## 🎯 Topp-treff` (`:278`), emit the section:
    ```ts
    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:
  ```ts
  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`):
  ```ts
  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 `renderBrief` →
  `briefSummary(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 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, 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.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
  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-1** — `tests/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-2** — `cli.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.
