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

401 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 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` | **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` | SC1SC8 |
| `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) | 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` | **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 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.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.