# Plan — RE-R3a: persist the relevance score + rank the morning brief on it > **Brief:** `docs/research-engine/brief-re-r3a.md`. **Slice:** RE-R3a (research-engine rung-2 — R3 slice 1, > research-deepening: the relevance half of hull 5 + the hull-3 schema remainder). > **TDD-order (two-phase RED — light-Voyage BLOCKER fold):** Step 1 records RED in two phases — > **(A)** true logic-RED for `store`/`brief`/`cli` against the pre-edit code (inline fixtures, no new import); > **(B)** for `score`/`item`, land non-throwing stubs for the new `score.ts` exports first (Node16 ESM throws a > missing named import at module-load, not on assertion), then record the value-assertion RED against the stubs. > Then GREEN: S-score envelope → S-types + S-store (first-sight persist + v2→v3 migrate) → S-item (validate + > bridge) → S-brief (composite-sort + render band+mode) → S-cli (doc-only) → wire `trend-spotter.md` + README → > gate floors + Section 16j → behavioural → land. > **Counts recounted live at land, never pinned/guessed.** > **Go-gate decisions (confirmed "Go" 2026-06-24):** D1 4-field `TrendScore` · D2 composite primary within > bucket · D3 first-sight · D4 one slice (data-then-visible commit order) · D6 mode shown in per-entry render. > **Light-Voyage hardened:** scope-guardian ALIGNED (0) / brief-reviewer PROCEED_WITH_RISKS (6 MINOR) / > plan-critic REVISE (1 BLOCKER, 4 MAJOR, 4 MINOR) — all folded (see §Plan-critic — folded). ## Goal Stop discarding the relevance judgment the `trend-spotter` agent already computes. Persist a 4-field `TrendScore { mode, dimensions, composite, priority }` on the store record (schema v2→v3, additive lossless migrate — the R2a pattern), computed deterministically from the agent's five judgment scores by the already-built `score.ts` (`composite`+`band`, one owner). Then make `rankForBrief` order each bucket on composite first, and `renderBrief` surface the band + mode. No re-score-on-recapture, no saturation/status, no scheduler, no new source file — those stay later R3 slices. ## Files touched (exhaustive — for scope-guardian) | File | Change | SC | |---|---|---| | `scripts/trends/src/score.ts` | **EDIT** — `TrendScore` interface + `requiredDimensions(mode)` (ordered) + `scoreEnvelope(mode, dimensions)` (composes the existing `composite`+`band`, no new arithmetic; throws on bad dim by contract) | SC1 | | `scripts/trends/src/types.ts` | **EDIT** — `import type { TrendScore }`; `TrendRecord.score?: TrendScore`; `SCHEMA_VERSION` 2→3; doc-comment | SC4 | | `scripts/trends/src/store.ts` | **EDIT** — `TrendInput.score?: TrendScore`; `addTrend` persists `score` first-sight on add (conditional spread), duplicate does NOT update; migrate comment v1→v2→v3 (logic unchanged) | SC3, SC4 | | `scripts/trends/src/item.ts` | **EDIT** — `TrendItem.score?: {mode,dimensions}`; `normalizeItem` validates (non-array `score`/`dimensions`; mode; the mode's five dims in [1,10]) → structured error, carries validated dims; `itemToInput` carries `scoreEnvelope(...)` (throws by contract on direct bad dims) | SC2 | | `scripts/trends/src/brief.ts` | **EDIT** — `rankForBrief` comparator: composite primary (`?? -1`), buckets unchanged; `renderBrief`/`renderBulletEntry` surface `· ()` (full pinned shapes); `briefSummary` band only; exact `ranking:` descriptor | SC5, SC6 | | `scripts/trends/src/cli.ts` | **EDIT (doc-only behavior)** — capture persists score automatically via `itemToInput` (no logic change); header doc-comment note | SC7 | | `scripts/trends/tests/score.test.ts` | **EDIT** — `requiredDimensions` (both modes, ordered + order pinned) + `scoreEnvelope` (composite/priority = existing funcs; bad dim throws) | SC1 | | `scripts/trends/tests/item.test.ts` | **EDIT** — score validation (valid carried/validated dims; bad mode/missing/out-of-range/non-array/array-dims → structured error, no throw) + `itemToInput` envelope + `itemToInput` direct bad-dim throws | SC2 | | `scripts/trends/tests/store.test.ts` | **EDIT** — first-sight persist (new persists; duplicate keeps first score, topics union; score-free add) + v2→v3 migration (lossless/idempotent + score-survives-round-trip, mirrors `:403-476` with `2`→`3`) | SC3, SC4 | | `scripts/trends/tests/brief.test.ts` | **EDIT** — composite-primary within bucket; unscored last (`-1`); total order; full render lines (`· ()` scored / unchanged unscored); summary band; unscored single-match-top summary; quote-safe summary; exact `ranking:` string; determinism | SC5, SC6 | | `scripts/trends/tests/cli.test.ts` | **EDIT** — capture batch with score → record carries computed composite/priority (read back via `list --json`); bad score → `errors[]`, valid added, exit 0 | SC7 | | `agents/trend-spotter.md` | **EDIT** — Step 4.5 capture batch carries per-item `"score":{"mode":"kortform","dimensions":{…}}`; prose ("carry the Step-2 scores"); domain-general; contains literal `"dimensions"` (currently absent → grep non-vacuous) | SC8 | | `scripts/trends/README.md` | **EDIT** — item `score` field (judgment in) + persisted `TrendScore` (out) + brief ranks on composite | — | | `scripts/test-runner.sh` | **EDIT** — `TRENDS_TESTS_FLOOR` 104→recount + breakdown comment (`:701`); NEW unconditional **Section 16j** (after 16i `~:1171` / before 18 `:1173`); `ASSERT_BASELINE_FLOOR` 94→**99**; header-enumeration chain (16i clause `:46-49`, Section-18 clause `:49`) | SC8 | | `docs/research-engine/{brief,plan}-re-r3a.md` | **NEW** — slice docs (TRACKED, like `docs/second-brain/*`) | — | | `STATE.md` | **EDIT at land** — Telling-block reconcile (trends floor, ASSERT floor 99, schema v3, gate total). *Land bookkeeping, LOCAL-ONLY.* | — | **Not touched (scope fence):** `references/trend-scoring-modes.md` (SSOT weights/bands unchanged) · the SessionStart hook + its tests (R3a touches no hook; no frontmatter-schema change; no new hook test) · `queryByTopic`/`history`/`newestCaptureDate` (store query unchanged) · the `score` CLI digest path + the `add` manual path (`cli.ts`) · `config/*` · no new `.ts`/`.mjs` file · `agents/*` count (19) · `commands/*` (29) · `references/*` (27) · `.gitignore` (trends lines present). ## Step 1 — (RED, two phases) failing tests across score/item/store/brief/cli RED discipline (R2a/R2b) + the light-Voyage BLOCKER fold: a missing **named** import throws at module-load under Node16 ESM (`package.json:8` `node --import tsx --test`; `tsconfig.json` `module: Node16`), so `score`/`item` (which reference new `score.ts` exports) cannot be assertion-RED before those exports exist. Split the RED proof: **Phase A — true logic-RED against the pre-edit code** (`store`/`brief`/`cli` build fixtures inline; `TrendStore`/ `TrendRecord` are `import type`, erased by tsx; they import no new runtime symbol): - `store.test.ts`: `addTrend(emptyStore, inputWithScore)` → record has `score`; a second `addTrend` (same title+url, **different** score) → stored score **unchanged**, `added:false`, topics unioned; score-free add → score-free record. **Migration:** a `schemaVersion:2` store with score-less records → `loadStore` gives `schemaVersion:3`, records intact, `"score" in record === false` (not invented); round-trip writes `schemaVersion:3`; a v3 store with `score` is idempotent; **a v3 store's `score` survives load+resave** (read back, re-save, re-read — `score` byte-identical). *(Mirror the RE-R2a block `store.test.ts:403-476`, retitled `(RE-R3a / score v2→v3)`, every `schemaVersion` assertion literal flipped `2`→`3`.)* - `brief.test.ts`: two **same-overlap, same-freshness** records, composites 9 vs 6 → the 9 sorts first in its bucket; a **scored** vs **unscored** same-bucket pair → scored first (the `-1` sentinel); a same-title/diff-url **both-unscored** pair → fixed by `url asc` (total order intact); `renderBrief` for a scored top entry contains the **full line** `… (d) · () · Pillarer: …`, for an unscored one the **unchanged** line (no token) — assert full lines, not substrings; a scored bullet contains `… (d) · () · 🔗 …`; `briefSummary` names the band on a scored top, omits it on an unscored top, and is one line with no `"` **even when the top title contains a guillemet/quote**; a store whose only fresh match is a **single-pillar unscored** record → summary with no `· ` token; the `ranking:` line equals the pinned string verbatim; two `renderBrief` calls byte-identical. - `cli.test.ts`: a `capture` batch (subprocess, `--store` temp) with a valid per-item `score` → a following `list --store --json` shows the record's `score` with the computed composite/priority; a batch with one bad score (`timing:99`) → JSON `errors[]` non-empty, the valid items added, **exit 0**. **Phase B — stub-first, then value-assertion RED** (`score`/`item` reference new exports): - Land **non-throwing stubs** in `score.ts` so the imports resolve: `requiredDimensions → []`; `scoreEnvelope → { mode, dimensions, composite: 0, priority: "Skip" }`. (These are the wrong-value stubs Step 2 replaces.) - `score.test.ts`: `requiredDimensions("kortform")`/`("long-form")` deep-equal the two **ordered** five-key lists (and a guard that the order matches `Object.keys(KORTFORM_WEIGHTS)` so a SSOT reorder fails); `scoreEnvelope(mode, dims)` returns `{mode, dimensions, composite, priority}` with composite/priority **equal to `composite(dims,mode)` / `band(...).priority`** (assert against the existing functions — not hard-coded — so they share one owner); a bad dim makes `scoreEnvelope` throw (it calls `composite`). Fails on assertion against the stubs (`[] ≠ expected`, `composite 0 ≠ real`). - `item.test.ts`: a valid `score` → `normalizeItem` `ok:true` carrying the **validated** dims; bad `mode` / a missing dimension / a dim `0` or `11` / a non-object `score` / an **array** `dimensions` → `ok:false` with an `invalid score` error (**does not throw**); absent `score` → key omitted. `itemToInput(validItemWithScore, "2026-06-24")` → `score` equals `scoreEnvelope(mode,dims)`; without score → no `score`; **`itemToInput` with an out-of-range dim throws** (the defense-in-depth contract). Fails on assertion against the stubs. **RED proof (record in commit, two phases):** Phase A — `(cd scripts/trends && npm test)` before any src edit → the `store`/`brief`/`cli` new cases fail on **assertion** (logic-RED), not module-not-found. Phase B — after the non-throwing stubs land, the `score`/`item` cases fail on **value assertion** against the stubs. The plan does **not** claim a single "all five fail before any code" run. ## Step 2 — (GREEN) `score.ts` envelope In `scripts/trends/src/score.ts`: - `export interface TrendScore { mode: ScoreMode; dimensions: DimensionScores; composite: number; priority: Priority }`. - `export function requiredDimensions(mode: ScoreMode): string[] { return Object.keys(WEIGHTS[mode]); }` — **ordered** (insertion order of the SSOT weight literal); `score.test` pins the order. - `export function scoreEnvelope(mode: ScoreMode, dimensions: DimensionScores): TrendScore { const c = composite(dimensions, mode); return { mode, dimensions, composite: c, priority: band(c).priority }; }` — composes the existing pure functions; **no new arithmetic**; throws via `composite` on a bad dim (its contract). Replace the Phase-B stubs. Make `score.test` green. ## Step 3 — (GREEN) `types.ts` + `store.ts` (schema v3 + first-sight persist) - `types.ts`: `import type { TrendScore } from "./score.js";`; add `score?: TrendScore;` to `TrendRecord` (doc-comment marks it as the realized `:21-23` field); `SCHEMA_VERSION = 3`. - `store.ts`: add `score?: TrendScore;` to `TrendInput` (`import type { TrendScore } from "./score.js"`); in `addTrend`'s new-record branch add `...(input.score !== undefined ? { score: input.score } : {})` (after the `summary` spread `:136`); the **duplicate** branch is unchanged (topics union only — score is first-sight, D3). Extend the `loadStore` migrate comment to *"v1→v2→v3 all additive-optional … the migration is the version stamp alone"* — **no logic change** (`Math.max(onDisk, SCHEMA_VERSION)` `:87` already handles v3; `saveStore` `JSON.stringify` `:95` preserves `score`). Make `store.test` green (first-sight + v2→v3 + survives-round-trip). ## Step 4 — (GREEN) `item.ts` (validate + bridge) - `TrendItem` gains `score?: { mode: ScoreMode; dimensions: DimensionScores };` (`import type { ScoreMode, DimensionScores } from "./score.js"` + `import { requiredDimensions } from "./score.js"`). - `normalizeItem`: after the `publishedAt` validation (`:99-106`), if `r.score !== undefined && r.score !== null`, validate: `score` is a **non-array** object; `mode` ∈ `{kortform, long-form}`; `dimensions` is a **non-array** object; **for each key in `requiredDimensions(mode)`**, the value is a number in [1,10]. On any failure `errors.push("invalid score: ")`; on success build `score = { mode, dimensions }` from the **validated** values (not raw `r.score.dimensions`). Carried into the returned `TrendItem` via conditional spread (key omitted when absent/invalid). **Never throws** — structured errors only (the `publishedAt` discipline). - `itemToInput`: add `...(item.score !== undefined ? { score: scoreEnvelope(item.score.mode, item.score.dimensions) } : {})` (`import { scoreEnvelope } from "./score.js"`). On the **capture path** the dims are pre-validated by `normalizeItem`, so `scoreEnvelope`→`composite` cannot throw there; called **directly** with bad dims it throws by contract (defense-in-depth — SC2 asserts it). Make `item.test` green. ## Step 5 — (GREEN) `brief.ts` (composite sort + render band+mode) - `rankForBrief` comparator (`:94-98`): prepend `(b.trend.score?.composite ?? -1) - (a.trend.score?.composite ?? -1) ||` before the existing `b.overlap - a.overlap || …`. **Sentinel `-1`** (composite ≥ 1.0 always; `-1` sorts unscored last and subtracts cleanly). Buckets (`:100-104`) and totals unchanged. - `renderTopEntry` (`:132-141`): when `e.trend.score` is present, insert `· ${e.trend.score.priority} (${e.trend.score.mode})` into the meta line **between `(${e.ageDays}d)` and `· Pillarer`** (unscored: unchanged). `renderBulletEntry` (`:143-145`): when scored, insert `· ${score.priority} (${score.mode})` **before `· 🔗`** (unscored: unchanged). Both shapes asserted as **full lines** in `brief.test`. - `briefSummary` (`:122-130`): in the fresh>0 branch, when `top.trend.score` is present include `· ${top.trend.score.priority}` in the top mention (**band only — no mode** to keep the one-line headline clean): `… Topp: «» (<pillar> · <priority> · <age>d).`; when the top is unscored omit the token: `… Topp: «<title>» (<pillar> · <age>d).` Keep it one line, no `"`/`\n`. - `renderBrief`'s `ranking:` line (`:160`) → the **exact pinned** string `composite desc, then pillar-overlap desc, then publishedAt desc (capturedAt fallback); freshDays ${ranking.freshDays}` (asserted byte-for-byte). `BRIEF_SCHEMA_VERSION` stays 1. Make `brief.test` green. ## Step 6 — (GREEN) `cli.ts` (doc-only) + `cli.test` `capture` (`:243-269`) already folds through `itemToInput` (`:254`), which now carries `score` → capture persists it with **no logic change**. Update only the header doc-comment (`:15-21`): note `capture` persists an optional relevance score computed from the item's judgment scores. Make `cli.test`'s capture-persists-score + bad-score-in-errors cases green. *(No `today()` exact-value assertions in `cli.test` — the wall clock is read at the edge; composite/priority are deterministic and asserted on the read-back record.)* ## Step 7 — wire `trend-spotter.md` (Open Q#1 default = WIRE) + README In `agents/trend-spotter.md` Step 4.5 (`:291-298`), extend each capture-batch item with **`"score": {"mode": "kortform", "dimensions": {"pillar": N, "audience": N, "timing": N, "angle": N, "authority": N}}`** — the same five judgment scores computed in Step 2 (`:134`). Add prose: don't discard the Step-2 scores; fold them into the capture batch so the store persists the relevance assessment and the morning brief ranks on it. Mode defaults `kortform`; `long-form` when invoked from `/linkedin:newsletter` (the long-form dims are `pillar/depth/angle/authority/currency`, `trend-scoring-modes.md:59-65`). The replacement prose **must contain the literal `"dimensions"`** (Section 16j `grep -qF`; verified absent today → non-vacuous). Keep the "skip silently if no deps" escape hatch + domain-general phrasing (Section 17). Update `scripts/trends/README.md`: the item `score` field (judgment in), the persisted `TrendScore` (composite/priority out), and that the brief now ranks on composite. ## Step 8 — gate: floors + new unconditional Section 16j In `scripts/test-runner.sh`: - Set `TRENDS_TESTS_FLOOR` (`:701`, currently 104) to the **`tests N` line** reported by `(cd scripts/trends && npm test)` after Steps 1–6 — recounted live, NOT additive-guessed. Stays **inside** the `if [ -x …/tsx ]` deps guard. **Append** `+ RE-R3a: score +N` to the inline breakdown comment (`:701`) so it can't drift. - Add **Section 16j** ("Trends Score Wiring", RE-R3a), mirroring Section 16i's shape (`:1122-1171`). **Placement (verified):** file order is 17→16g→16h→16i→18 (`:947/:1014/:1078/:1122/:1173`), so **16i is the last section before Section 18** — insert 16j **after 16i's closing `fi`/`echo ""` (~`:1171`), before the Section 18 block (`:1173`)** (anti-erosion Section 18 must stay last so it counts every prior check). Five **unconditional**, deps-absent-safe checks (pure `grep -qF`/self-test, no `tsx`), the self-test emitting **one** pass/fail like 16i: (1) a non-vacuity self-test (a probe carrying `score?.composite` accepted, one without rejected); (2) `grep -qF 'export interface TrendScore' scripts/trends/src/score.ts`; (3) `grep -qF 'score?: TrendScore' scripts/trends/src/types.ts`; (4) `grep -qF '"dimensions"' agents/trend-spotter.md` (the capture batch carries the judgment); (5) `grep -qF 'score?.composite' scripts/trends/src/brief.ts` (the brief ranks on it — payoff wired, not merely doc'd). - Bump `ASSERT_BASELINE_FLOOR` (`:1193`, currently 94) → **exactly 99** (94 + the 5 new unconditional 16j emitters; the self-test emits one pass/fail like 16i, so 99 is deterministic — "live recount" is the safety net, not a guess). Update the **header-enumeration prose chain** by inserting the 16j clause **between** the 16i clause (`:46-49`) and the Section-18 clause (`:49`), preserving sentence flow (it's prose, not an append). - **NOT touched here:** the hook suite (no `HOOK_TESTS_FLOOR` in `test-runner.sh`; R3a adds no hook test). It must still pass untouched (`node --test hooks/scripts/__tests__/`) as a regression sanity at land. ## Step 9 — behavioural verification `(cd scripts/trends && npm install)` if needed, then run brief §7's four behavioural steps (capture A>B with scores, `list --json` to confirm persisted composite/priority, `brief --json` to confirm A precedes B + the band in the summary + `· <priority> (kortform)` in the entry line, a bad-score item lands in `errors[]` with exit 0). Run full `bash scripts/test-runner.sh` → `FAIL=0` (`ASSERT_BASELINE_FLOOR` 99); run `node --test hooks/scripts/__tests__/` → still green (untouched regression). ## Step 10 — land Recount all touched floors live; reconcile STATE.md "Telling" block (trends N/N, ASSERT floor 99, schema v3, gate total). Commit order (house style): **(1)** docs commit `docs/research-engine/{brief,plan}-re-r3a.md` (no suffix, tracked); **(2)** code commit — the six `src/*.ts` + five test files + `agents/trend-spotter.md` + `scripts/trends/README.md` + `scripts/test-runner.sh` with `[skip-docs]`. (Per D4, the code commit MAY be split into a data commit [score/types/store/item/cli + their tests] and a visible commit [brief + its tests + agent wire] if the R2a/R2b two-commit rhythm is preferred.) Push freely (window lifted; gitleaks at commit; `origin` = PUBLIC `open/` — STATE/`*.local.*` never pushed). No version bump (additive; `v0.5.2` dev). ## Verification (testable) | SC | Check | Command | Expected | |---|---|---|---| | — | RED Phase A | `(cd scripts/trends && npm test)` before src edits | store/brief/cli new cases fail on assertion (logic-RED), not module-not-found | | — | RED Phase B | `npm test` after non-throwing stubs | score/item cases fail on value assertion against stubs (`[] ≠ expected`, `composite 0 ≠ real`) | | SC1 | score envelope | `npm test` (score.test) | `requiredDimensions` both modes ordered (order pinned); `scoreEnvelope` composite/priority = existing funcs; bad dim throws | | SC2 | item validate + bridge + contract | `npm test` (item.test) | valid score carried (validated dims); bad mode/missing/out-of-range/non-array/array-dims → structured error (no throw); `itemToInput` envelope; direct bad-dim throws | | SC3 | first-sight persist | `npm test` (store.test) | new persists score; duplicate keeps first score (topics union); score-free add works | | SC4 | migration v2→v3 | `npm test` (store.test) | v2 loads as v3, records intact, no score invented; round-trip writes v3; v3 idempotent; score survives load+resave | | SC5 | brief ranks on composite | `npm test` (brief.test) + manual | composite primary within bucket; unscored last (`-1`); total order; deterministic | | SC6 | render band+mode | `npm test` (brief.test) | full lines `· <priority> (<mode>)` scored / unchanged unscored; summary band (no mode); unscored single-match summary; quote-safe; exact `ranking:` string | | SC7 | CLI persists score | `npm test` (cli.test) + manual | capture batch w/ score → record carries composite/priority; bad score → `errors[]`, valid added, exit 0 | | SC8 | gate + wiring + de-niche | `bash scripts/test-runner.sh` | `FAIL=0`; trends ≥ floor; Section 16j green; `ASSERT_BASELINE_FLOOR`=99; Section 17; counts 27/19/29 | ## Risks - **R1 — composite/band drift from the SSOT** (someone re-implements the math in `scoreEnvelope`). *Mitigated:* `scoreEnvelope` *composes* `composite()`+`band()` (one owner); SC1 asserts equality against those functions; `score.test.ts:12-30` (weights) + the band-string assertions already pin them to the SSOT. - **R2 — a bad score crashes the capture loop.** *Mitigated:* `normalizeItem` fully validates the score (mode + the mode's five dims in [1,10], non-array objects) → structured error into `errors[]`; on the capture path `itemToInput`→`composite` is unreachable for bad dims; SC2 + SC7 assert no-throw + `errors[]` routing. `itemToInput` called **directly** with bad dims throws by contract (SC2) — defense-in-depth, not a leak. - **R3 — comparator NaN from the unscored sentinel** (`-Infinity - -Infinity`). *Mitigated:* sentinel is `-1` (composite ≥ 1.0 = min 1×Σweights); subtracts cleanly; SC5 asserts the both-unscored total order holds. - **R4 — losing the brief's determinism** (composite ties not fully broken). *Mitigated:* the new term is a leading tie-break; the existing `overlap → effectiveDate → title → url` chain still gives a total order (the `(title,url)` pair is the unique dedupe id, `store.ts:66-68`); SC5 asserts byte-identical output + the same-title/diff-url case. - **R5 — `extractYaml` mis-reads the `summary`** if the band token introduces a `"`/newline. *Mitigated:* the band strings (`Immediate`/`High`/…) are bare words; the summary stays one line, no `"` — asserted in SC6 even with a guillemet/quote in the top title; the surfacing hook is untouched. - **R6 — migration not actually lossless** (a v2 record mutated on load, or a new field stripped on resave). *Mitigated:* the migration is the version stamp alone (logic unchanged); `saveStore` `JSON.stringify` `:95` strips nothing; SC4 mirrors the proven R2a `store.test:403-476` and adds a score-survives-round-trip case. - **R7 — editing `trend-spotter.md` trips the de-niche guard.** *Mitigated:* Section 17 runs in the gate; the added `score`/`dimensions` are the rubric's generic dimension names + the user's pillars, vendor/sector-free. - **R8 — new gate checks must survive a deps-absent fresh clone.** *Mitigated:* Section 16j is pure `grep`/self-test on tracked source (no `tsx`) → unconditional; `TRENDS_TESTS_FLOOR` stays inside the deps guard. - **R9 — import cycle.** *Mitigated:* `score.ts` imports nothing internal today (`:1-17`, verified leaf). R3a adds three new **one-way** inbound edges to it — `score.ts ← types.ts` (`TrendScore` type), `score.ts ← store.ts` (`TrendScore` type on `TrendInput`), `score.ts ← item.ts` (`ScoreMode`/`DimensionScores` types + `requiredDimensions`/`scoreEnvelope` values). The DAG stays acyclic: `score.ts (leaf) ← {types, store, item, brief, cli}`, since `score.ts` imports none of them back. - **R10 — mode-mixing makes the ranking apples-to-oranges** (kortform vs long-form composites ranked together). *Accepted for R3a, mitigated visibly:* almost all records are `kortform` (the default); the body entry line **shows the mode** (`<priority> (<mode>)`, D6/SC6) so the operator can see when adjacent entries used different instruments; a mode-segmented brief is a documented R3-later non-goal (brief §4). ## Plan-critic — folded Three Opus reviewers ran, each verifying claims against live code: **scope-guardian ALIGNED** (0 findings; counts 27/19/29 + "no new source file" verified live); **brief-reviewer PROCEED_WITH_RISKS** (all four load-bearing claims verified TRUE; 6 MINOR); **plan-critic REVISE** (1 BLOCKER, 4 MAJOR, 4 MINOR; score 72/C). Resolution, each verified against live code: - **[BLOCKER — plan-critic] Step 1 RED-proof self-contradictory for score/item under ESM** (a missing named import throws at module-load, not on assertion; the stub-first fix inverted the stated ordering). ✅ Step 1 is now **explicitly two-phase**: Phase A true logic-RED for `store`/`brief`/`cli` against pre-edit code; Phase B stub-first then value-assertion RED for `score`/`item`. The header blockquote + brief §5 + §TDD-order state it; the "all five fail before any code" claim is removed. - **[MAJOR — plan-critic] no-throw guarantee overstated** (`itemToInput` is public; direct bad-dim calls throw). ✅ Step 4 + R2 + brief §5 reword it **path-specific** (no throw on the capture path because `normalizeItem` gates it; direct calls throw by contract); SC2 adds a direct-throw assertion + a carries-validated-dims assertion. - **[MAJOR — plan-critic] mode-mixing waved away + "mode shown per entry" contradicted the render spec.** ✅ D6: the render now shows `· <priority> (<mode>)` per body entry (Step 5); brief §4 adds a mode-blind-ranking non-goal with the rationale (mode visible, mostly kortform); SC6 asserts the full line incl. mode; R10 added. - **[MAJOR — plan-critic] `requiredDimensions` order contract ambiguous** (SC1 hard-coded arrays vs membership use). ✅ pinned **ordered** (Step 2 + SC1 deep-equal the SSOT-order array; `score.test` pins the order; `normalizeItem` consumes as a set). - **[MAJOR — plan-critic] `ASSERT_BASELINE_FLOOR` "~99" not pinned.** ✅ pinned **99** (94 + 5 unconditional 16j emitters; the self-test emits one pass/fail like 16i) — Step 8 + SC8 + brief §3. - **[MINOR — brief-reviewer] SC4 ref `:403-471` stale + v2 assertions** → ✅ `:403-476` + "flip every `schemaVersion` literal 2→3" note (SC4, Step 1). - **[MINOR — brief-reviewer] R1 SSOT-pin cite was the doc-comment** (`score.ts:9-13`) → ✅ now `score.test.ts:12-30` (R1, brief §2/§5). - **[MINOR — brief-reviewer] bullet `· <priority>` placement substring-only** → ✅ full pinned line shape (priority+mode before `🔗`), asserted as a full line (Step 5, SC6). - **[MINOR — brief-reviewer] three diverging `ranking:` descriptor strings** → ✅ one verbatim target string, asserted byte-for-byte (Step 5, SC6). - **[MINOR — brief-reviewer] unscored single-match-top summary path untested** → ✅ added as a Phase-A brief.test case + SC6. - **[MINOR — brief-reviewer] `normalizeItem` non-array object case understated in the brief** → ✅ "non-array" added to both `score` and `dimensions` object checks (Step 4, brief §3, SC2 array-dims case). - **[MINOR — plan-critic] header-chain line-ref `:33-49` loose** → ✅ tightened to the 16i clause `:46-49` / Section-18 clause `:49` (Step 8). - **[MINOR — plan-critic] R9 DAG omitted the new `score.ts ←` edges** → ✅ R9 now lists all three one-way edges. - **[MINOR — plan-critic] SC6 quote-safety regression with the new token** → ✅ SC6 asserts the summary stays one-line/no-`"` with a scored top title containing a guillemet/quote. - **[MINOR — plan-critic] SC4 forward-compat / score-survives-round-trip untested** → ✅ added to SC4 + Step 1 store.test. - **[plan-critic headless-readiness] N/A** — R3a executes **in-session, operator-driven** (driftsmodell), not as a headless autonomous run, so per-step revert/halt clauses aren't needed (R1/R2a/R2b had none either). **scope-guardian — ALIGNED:** every SC1–SC8 traces to a step; zero creep; all §4 non-goals held (no re-score-on-recapture, no saturation/status/first-mover field, no scheduler, no seen-log, no brief-diff, no A1–A4, no mode-filter, no `score` in the `add` path, no new source file/agent/command); counts 27/19/29 verified live; exactly 6 `src/*.ts` + 5 `tests/*.test.ts`, all edited, none added.