linkedin-studio/docs/research-engine/plan-re-r3d.md
Kjell Tore Guttormsen 5aa7187243 docs(linkedin-studio): RE-R3d brief + plan — temporal overlay (first-mover + saturation), light-Voyage hardened
R3 slice (b): the rest of hull #3. Two derived brief-time signals — first-mover
(recent + unsurfaced -> ranked up) and saturation (surfaced >= N prior days ->
ranked down) — computed from already-persisted fields. SB1 derived (no schema
bump, SCHEMA_VERSION stays 4); SB2 R3a composite stays the primary sort key, the
overlay is a within-tier cmp refinement. Zero new source/test files; counts
29/19/27; ASSERT floor 111 -> 117.

Three Opus reviewers (scope-guardian / brief-reviewer / plan-critic) folded:
warming badge gated at >=2 (preserves the R3b contract), disagreement ordering
fixture (true RED), saturationAt clamp, ageDays>=0 guard, fresh->neutral rename,
SSOT one-line note, nightly-thresholds known limitation, cite fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
2026-06-26 11:43:22 +02:00

30 KiB
Raw Permalink Blame History

Plan — RE-R3d: temporal overlay — first-mover + saturation (R3 slice b)

Brief: docs/research-engine/brief-re-r3d.md. Slice: RE-R3d (research-engine rung-2 — R3 slice (b): the live temporal overlay). Closes the rest of hull #3 — first-mover + saturation (relevance R3a; status R3b; angle already a scored dimension). Zero new files — pure EDITs. TDD-order (two-phase RED — light-Voyage discipline, inherited): Step 1 records RED in two phases — (A) temporalSignal is a NEW named export 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 a non-throwing stub first (temporalSignal → {tier:"fresh",…} + the type exports + BriefEntry.temporal populated by the stub in rankForBrief), then record value-assertion RED against it (the constant "fresh" stub fails the first-mover/saturated/ordering/token assertions); (B) the CLI flag tests are value-RED against the existing brief handler (the new flags are silently ignored by parseFlags today → tuned-threshold behaviour unchanged). Then GREEN: real temporalSignal + RankOptions knobs + cmp key → render tokens + summary marker + descriptor → cli.ts flags → wire trend-spotter.md (prose) + SSOT note + README → gate floors + Section 16m → behavioural → land. Counts recounted live at land, never pinned/guessed. Architectural decisions (CONFIRMED, AskUserQuestion 2026-06-26): SB1 derived-at-brief (no schema bump) · SB2 refine recency within the composite tier (composite stays PRIMARY). Go-gate D1D10 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

Make the morning brief read the temporal axis R3b logs but the ranking ignores. Two derived signals, computed at brief time from already-persisted fields (no new store field, no schema bump): first-mover (recent AND never surfaced on a prior day — "you'd be early") ranks a trend up; saturation (surfaced on >= saturationAt prior days — "you keep seeing this and not acting") ranks it down. The R3a relevance composite stays the PRIMARY sort key (SB2); the overlay is a new cmp key after pillar-overlap, before effectiveDate, re-ordering only WITHIN a (composite, overlap) tier. It also promotes the R3b · sett Nx display hint into a graded badge set and surfaces a first-mover marker on the one-line summary. No schema change (SCHEMA_VERSION 4 / BRIEF_SCHEMA_VERSION 1 untouched); no new agent/command/reference/module/file; score.ts/store.ts/ types.ts/item.ts/schedule.ts/run-daily.sh untouched.

Files touched (exhaustive — for scope-guardian)

File Change SC
scripts/trends/src/brief.ts EDIT — add TemporalTier/TemporalSignal exports + pure temporalSignal(ageDays, surfacedCount, {firstMoverDays, saturationAt}); RankOptions gains firstMoverDays?(def 2)/saturationAt?(def 3); BriefEntry gains temporal; rankForBrief threads the opts + populates temporal; cmp gains b.temporal.rank - a.temporal.rank after overlap, before effectiveDate; surfacedTokentemporalToken (badges); briefSummary first-mover marker; ranking: descriptor names the new key SC1SC5, SC7, SC9
scripts/trends/src/cli.ts EDITbrief … [--first-mover-days N] [--saturation-at N] (mirror --fresh-days at :312-317, into rankForBrief opts at :323); usage line (:110) + header synopsis (:14) note the overlay; schedule untouched (nightly uses defaults) SC6
scripts/trends/tests/brief.test.ts EDITtemporalSignal unit incl. clamp + future-date + neutral cases (SC1/SC2/SC9); disagreement ordering fixture (SC3); render badges (SC4) + 4 touched assertions: :407 sett 3xmettet (3x), :408 unchanged (≥2 gate), :325-331+:410-416 descriptor segment, :96-102 re-based to a shared tier; summary marker (SC5); determinism (SC7); no-score-mutation (SC8) SC1SC5, SC7SC9
scripts/trends/tests/cli.test.ts EDIT — subprocess: brief --first-mover-days N --saturation-at N changes tiers vs defaults; absent → defaults 2/3; --first-mover-days -1/x, --saturation-at 0/x → exit 2 SC6
scripts/trends/src/types.ts · store.ts · score.ts · item.ts · schedule.ts · run-daily.sh UNTOUCHED — no data-shape/scoring/store/scheduler change. Listed to assert they are not in scope (SCHEMA_VERSION 4 held in types.ts; the R3c scheduler suite stays green untouched).
agents/trend-spotter.md EDIT (prose-only, minimal) — one line: the brief now applies a live temporal overlay (first-mover↑/saturated↓) at rank time, derived from dates + the seen-log; capture path unchanged. Domain-general.
references/trend-scoring-modes.md EDIT (one-line note, "Consumers") — the brief applies a within-composite-tier temporal overlay (RE-R3d); no weight/band/formula change.
scripts/trends/README.md EDIT — new ## Temporal overlay (RE-R3d) between the R3c scheduler section (:135-154) and ## Tests (:155): first-mover/saturation, derived-not-stored, the flags + defaults, the cmp integration, the badges
scripts/test-runner.sh EDITTRENDS_TESTS_FLOOR (:713, 192)→recount + breakdown comment; NEW unconditional Section 16m between 16l's echo "" (:1374) and Section 18 (:1376); ASSERT_BASELINE_FLOOR (:1403) 111→117; header-enum chain (:57-62) + Section-18 floor-history narration (:1376-1402, ends "= 111") SC10
docs/research-engine/{brief,plan}-re-r3d.md NEW — slice docs (TRACKED, like docs/second-brain/*)
STATE.md EDIT at land — Telling-block reconcile (trends floor, ASSERT floor 117, gate total; schema unchanged v4; correct the stale :709/:1329 line-cites to live :713/:1403). 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 uses default thresholds; no regression surface) · the SessionStart hook + its tests (R3d changes no frontmatter field; the summary's new marker is regex-safe; no hook change/test) · config/* · commands/* (29) · agents/* count (19 — trend-spotter.md is a prose EDIT) · references/* count (27 — trend-scoring-modes.md is an EDIT) · algorithm-signals-reference.md (cited for grounding, not edited) · .gitignore (no new artifact) · BRIEF_SCHEMA_VERSION (1) · 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 type TemporalTier, export interface TemporalSignal, and export function temporalSignal(): TemporalSignal { return { tier: "neutral", firstMover: false, surfacings: 0, rank: 2 }; } (a constant stub ignoring its args); add temporal: TemporalSignal to BriefEntry and populate it in rankForBrief via the stub. The cmp key + temporalToken + the summary marker are NOT added yet (Steps 23).
  • brief.test.ts (value-RED against the stub):
    • unit (SC1/SC2/SC9)temporalSignal(1,0,{firstMoverDays:2,saturationAt:3}) expects {tier:"first-mover",rank:3} (stub returns "neutral" → RED); (5,3,…)saturated; (5,2,…)warming; (5,0,…)neutral; (3,0,…)neutral (past window); (-1,0,…)neutral (future-date >=0 guard); the clamp case (5,5,{…,saturationAt:0})saturated via at=1.
    • ordering (SC3) — DISAGREEMENT fixture (folded — plan-critic M1: surfacedCount correlates with age, so a naive first-mover-vs-saturated fixture is already ordered by the existing effectiveDate-desc key → GREEN in Phase A, vacuous in GREEN). Three same-overlap entries: A neutral (surfaced 0, older date ≈5d, composite 7.0); B warming (surfaced 2, newer date ≈1d, composite 7.0); Z saturated (surfaced 4, composite 8.5). Expect topMatches.map(e=>e.trend.title) === [Z, A, B]. Phase A (stub: constant rank, no temporal key) yields [Z, B, A] (effectiveDate-desc decides A vs B) → RED; GREEN (Step 2 inserts temporal.rank) flips A above the newer B, proving the key.
    • render (SC4)renderBrief(...) expects · 🥇 først ute on the first-mover entry, · 🔁 mettet (3x) on a surfacedCount 3 entry, · sett 2x on surfacedCount 2, no badge on surfacedCount 1, none on a neutral entry (stub render still emits the old surfacedToken → RED). Four existing assertions touched (folded — brief-reviewer MEDIUM-2/3): (1) :407 · sett 3x· 🔁 mettet (3x) (its surfacedCount-3 trend is now saturated); (2) :408 !md.includes("sett 1x") stays unchanged (the ≥2 badge gate keeps surfacedCount-1 markerless); (3) :325-331 + (4) the regex :410-416 (the ranking: descriptor) gain then temporal (first-mover↑/saturated↓), ; and the effectiveDate-isolation test :96-102 is re-based so both entries share a neutral tier (else the new key, not effectiveDate, silently decides it — coverage erosion).
    • summary (SC5)briefSummary(...) with a first-mover top expects · 🥇 først ute in the headline (stub → no marker → RED); assert the summary has no " and no \n.
    • no-score-mutation (SC8) — capture a store's score.composite values, run rankForBrief (pure — no mutation) and assert unchanged; assert BRIEF_SCHEMA_VERSION === 1, SCHEMA_VERSION === 4.

Phase B — subprocess value-RED against the existing CLI handler (no new import; the flags are parsed-but-unused today):

  • cli.test.tsbrief --pillars ai --first-mover-days 1 --saturation-at 2 --store <seeded> --out <tmp>: today parseFlags stores the flags but the brief handler ignores them → the rendered tiers match the defaults → RED. The tier badges live in the written .md body, not in --json (which returns only {path,date,totals,summary,marked}, cli.ts:336), so the assertion readFileSync(path) and checks the badge (a surfacedCount 2 trend renders · 🔁 mettet (2x) at --saturation-at 2, but · sett 2x at the default) — folded — brief-reviewer LOW-5; the first-mover marker is also observable via the --json summary. --first-mover-days x / --saturation-at 0 today are ignored (exit 0) → RED against the expected usage exit 2.

RED proof (record in commit, two phases): Phase A — after the non-throwing temporalSignal stub lands, (cd scripts/trends && npm test) fails the unit/ordering/render/summary cases on value assertions against the constant-"fresh" stub (not module-not-found). Phase B — the cli.test flag cases fail on value/exit assertions against the flag-ignoring handler. The plan does not claim a single "everything fails before any code" run.

Step 2 — (GREEN) brief.ts — the real signal + ranking integration

Replace the Phase-A stub with the real, pure implementation:

  • export type TemporalTier = "first-mover" | "fresh" | "warming" | "saturated";
  • export interface TemporalSignal { tier: TemporalTier; firstMover: boolean; surfacings: number; rank: number; }
  • export function temporalSignal(ageDays, surfacedCount, opts): TemporalSignal:
    const surfacings = surfacedCount ?? 0;
    const at = Math.max(1, opts.saturationAt);            // defensive clamp (plan-critic m3)
    const firstMover = ageDays >= 0 && ageDays <= opts.firstMoverDays && surfacings === 0;  // >=0 guard (m5)
    const tier: TemporalTier = firstMover ? "first-mover"
      : surfacings >= at ? "saturated"
      : surfacings >= 1 ? "warming" : "neutral";
    const rank = tier === "first-mover" ? 3 : tier === "neutral" ? 2 : tier === "warming" ? 1 : 0;
    return { tier, firstMover, surfacings, rank };
    
    Pure (no clock/fs/env). Branches disjoint + total (firstMover requires surfacings === 0; saturated/ warming both require surfacings >= 1).
  • RankOptions (:51-54) gains firstMoverDays?: number + saturationAt?: number. rankForBrief (:78) reads const firstMoverDays = opts.firstMoverDays ?? 2; const saturationAt = opts.saturationAt ?? 3; and populates BriefEntry.temporal = temporalSignal(ageDays, trend.surfacedCount, { firstMoverDays, saturationAt }) at the push site (:93).
  • cmp (:102-107): insert b.temporal.rank - a.temporal.rank || after b.overlap - a.overlap || and before b.effectiveDate.localeCompare(a.effectiveDate). Composite stays the first key (SB2). The chain stays a total order. The bucketing (isFresh/freshDays, :109-113) is unchanged. Make the Phase-A unit + ordering cases green.

Step 3 — (GREEN) brief.ts — render badges + summary marker + descriptor

  • Replace surfacedToken (:154-157) with temporalToken(e: BriefEntry): string:
    const t = e.temporal;
    if (t.tier === "first-mover") return " · 🥇 først ute";
    if (t.tier === "saturated")   return ` · 🔁 mettet (${t.surfacings}x)`;
    if (t.tier === "warming" && t.surfacings >= 2) return ` · sett ${t.surfacings}x`;  // preserves the R3b ≥2 badge contract
    return "";                                                         // neutral, or warming with surfacings 1
    
    Replace the surfacedToken(e) call in renderTopEntry (:162) and renderBulletEntry (:171) with temporalToken(e). (scoreToken untouched.)
  • briefSummary (:131-141): compute const fm = top.temporal.firstMover ? " · 🥇 først ute" : ""; and emit (${pillar}${band}${fm} · ${top.ageDays}d). The marker carries no "/\n (hook-regex invariant, :128-130).
  • renderBrief's ranking: descriptor (:187): insert then temporal (first-mover↑/saturated↓), between pillar-overlap desc, and then publishedAt desc. Make the Phase-A render + summary cases green.

Step 4 — (GREEN) cli.ts — the two brief threshold flags

After the --fresh-days block (:313-317), add (mirroring its idiom):

let firstMoverDays = 2;
if (flags["first-mover-days"] && flags["first-mover-days"] !== "true") {
  const n = Number.parseInt(flags["first-mover-days"], 10);
  if (Number.isNaN(n) || n < 0) usage("--first-mover-days must be a non-negative integer");
  firstMoverDays = n;
}
let saturationAt = 3;
if (flags["saturation-at"] && flags["saturation-at"] !== "true") {
  const n = Number.parseInt(flags["saturation-at"], 10);
  if (Number.isNaN(n) || n < 1) usage("--saturation-at must be a positive integer");
  saturationAt = n;
}

Pass into the rank call (:323): rankForBrief(store, pillars, day, { freshDays, firstMoverDays, saturationAt }). Update the brief usage line (:110) + header synopsis (:14) to list the two flags + a one-line note that the brief applies a derived temporal overlay (first-mover↑/saturated↓) at rank time. No new exit code (0/2). The schedule branch (:343-417) is untouched (nightly run uses defaults). Make the Phase-B cli.test cases green.

Step 5 — wire trend-spotter.md (prose) + SSOT note + README

  • agents/trend-spotter.md — one prose line (no batch-shape change): the morning brief now applies a live temporal overlay at rank time (first-mover ranked up, repeatedly-surfaced/saturated ranked down), derived from the publish/capture dates + the seen-log — no new capture step; the polling/capture path is unchanged. Domain-general (Section 17).
  • references/trend-scoring-modes.md — under "Consumers", add ONE line: the morning brief applies a brief-time temporal overlay (first-mover/saturation, RE-R3d) as a within-composite-tier ranking refinement; this does not change the capture-time weights above. (No weight/band/formula edit.)
  • scripts/trends/README.md — add ## Temporal overlay (RE-R3d) between :154 and ## Tests (:155): the first-mover/saturation definitions (self-surfacing, not market-coverage), the derived-not-stored boundary (no schema bump), the --first-mover-days(2)/--saturation-at(3) flags, the cmp integration (composite stays primary), the badge set (🥇 først ute / 🔁 mettet (Nx) / sett Nx / none).

Step 6 — gate: floors + new unconditional Section 16m

In scripts/test-runner.sh:

  • Set TRENDS_TESTS_FLOOR (:713, currently 192) 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-R3d: brief +N, cli +N (temporal overlay) to the inline breakdown comment.
  • Add Section 16m ("Trends Temporal Overlay", RE-R3d), mirroring Section 16l (unconditional, deps-absent-safe, pure grep -qF/self-test, no tsx, all literals ASCII — the badge emoji are NEVER grepped; the shell must stay ASCII-clean for bash 3.2 set -u). Placement (verified live): between Section 16l's trailing echo "" (:1374) and the Section 18 header (:1376) — anti-erosion stays last. Six unconditional checks, the self-test emitting one pass/fail like 16l: (1) a non-vacuity self-test (a probe carrying temporalSignal accepted, one without rejected); (2) grep -qF 'export function temporalSignal' scripts/trends/src/brief.ts; (3) grep -qF 'b.temporal.rank' scripts/trends/src/brief.ts (the cmp integration); (4) grep -qF '"first-mover"' scripts/trends/src/brief.ts (the tier literal); (5) grep -qF 'first-mover-days' scripts/trends/src/cli.ts (the flag); (6) grep -qF 'saturation-at' scripts/trends/src/cli.ts (the flag).
  • Bump ASSERT_BASELINE_FLOOR (:1403, currently 111) → exactly 117 (111 + the 6 new unconditional 16m emitters; the self-test emits one pass/fail like 16l, so 117 is deterministic — "live recount" is the safety net, not a guess). Insert the 16m clause into the header-enumeration prose chain (:57-62) before "…the assertion-count anti-erosion floor (SC6) in Section 18", preserving sentence flow. Append the RE-R3d (→117) narration to the Section-18 floor-history comment (:1376-1402, which ends "= 111").
  • NOT touched here: the hook suite (no HOOK_TESTS_FLOOR in test-runner.sh; R3d 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: seed a store with a fresh+unsurfaced, a warming (surfacedCount 2), and a saturated (surfacedCount 4) trend at the same composite; brief --pillars … --out /tmp/r3d-mb --store /tmp/r3d.json → the fresh+unsurfaced sorts first with · 🥇 først ute, the saturated last with · 🔁 mettet (4x), the ranking: descriptor names the temporal key; re-run with --first-mover-days 0 --saturation-at 2 → first-mover badge gone, surfacedCount 2 escalates to mettet (2x); --first-mover-days x → exit 2; confirm the seeded score.composite values are unchanged after the brief (only surfacedCount/lastSurfacedAt advance). Run full bash scripts/test-runner.shFAIL=0 (ASSERT_BASELINE_FLOOR 117, trends ≥ new floor, Section 16m 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 117, gate total; schema unchanged v4; correct the stale :709/:1329 cites to live :713/:1403). Commit order (house style): (1) docs commit docs/research-engine/{brief,plan}-re-r3d.md (no suffix, tracked); (2) code commit — brief.ts + cli.ts + brief.test.ts + cli.test.ts + agents/trend-spotter.md + references/trend-scoring-modes.md + scripts/trends/README.md + scripts/test-runner.sh with [skip-docs] (D10: single code commit — the overlay 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).

Verification (testable)

SC Check Command Expected
RED Phase A (cd scripts/trends && npm test) after the temporalSignal stub unit/ordering/render/summary cases fail on value assertions vs the constant-"fresh" stub (not module-not-found)
RED Phase B npm test (cli.test) before the flag impl --first-mover-days/--saturation-at cases fail on value/exit vs the flag-ignoring handler
SC1 first-mover detection npm test (brief.test) (≤firstMoverDays, surfaced 0) → first-mover; (>window,0)/(≤window,≥1) → not
SC2 saturation grading npm test (brief.test) surfacings ≥ saturationAt → saturated; 1..at-1 → warming; 0 → fresh (inclusive >=)
SC3 ranking — within-tier re-order, composite dominates (DISAGREE fixture) npm test (brief.test) [Z(8.5,sat), A(7.0,neutral,older), B(7.0,warming,newer)]; temporal key flips A above newer B; Phase A [Z,B,A]→RED; total order
SC4 render badges + ≥2 boundary npm test (brief.test) 🥇 først ute / 🔁 mettet (Nx) / sett Nx (warming≥2) / none (warming 1, neutral); :407 updated, :408 stays green
SC5 summary first-mover marker npm test (brief.test) first-mover top → · 🥇 først ute in headline; else absent; no "/\n
SC6 CLI flags npm test (cli.test) flags change tiers vs defaults; defaults 2/3 when absent; bad values → exit 2
SC7 determinism npm test (brief.test) same (store,pillars,today,freshDays,firstMoverDays,saturationAt) → byte-identical .md
SC8 no schema / no score mutation npm test (brief.test) SCHEMA_VERSION 4; BRIEF_SCHEMA_VERSION 1; score.composite unchanged after a brief
SC9 purity npm test (brief.test) temporalSignal stable over a grid; first-mover ⊆ recent∧unsurfaced; saturated ⇔ surfacings≥saturationAt
SC10 gate + wiring + de-niche bash scripts/test-runner.sh FAIL=0; trends ≥ floor; Section 16m green; ASSERT_BASELINE_FLOOR=117; Section 17; counts 29/19/27; hook suite green

Risks

  • R1 — changing cmp re-orders existing brief output → silently breaks downstream expectations. Mitigated: composite stays the PRIMARY key (SB2) — the overlay only re-orders WITHIN a (composite, overlap) tier; the bucketing is unchanged; SC3 pins the exact order; SC7 pins byte-determinism; the hook reads only date+summary (unaffected).
  • R2 — the surfacedTokentemporalToken promotion breaks pinned R3b/descriptor assertions. Mitigated/expected (folded — all three reviewers): the warming badge is gated at surfacings >= 2, so brief.test.ts:408 (!md.includes("sett 1x")) stays green and the R3b ≥2 contract is preserved exactly; Step 1 enumerates the four touched assertions (:407 sett 3xmettet (3x); :325-331+:410-416 descriptor) and re-bases the coverage-eroded :96-102. surfacedCount 2 still renders · sett 2x.
  • R3 — emoji in the gate crashes bash 3.2 set -u. Mitigated: Section 16m greps ONLY ASCII literals (temporalSignal, b.temporal.rank, "first-mover", first-mover-days, saturation-at); the emoji live only in brief.ts source + rendered output, asserted by the TS tests, never by the shell gate.
  • R4 — the summary marker breaks the SessionStart extractYaml regex. Mitigated: 🥇 først ute carries no " and no \n; SC5 asserts the invariant; the hook suite is a land-time regression check.
  • R5 — saturation framing overclaims (reads as market-coverage). Accepted/honest: R3d's saturation is self-surfacing (our seen-log), a proxy for a closing/ignored window — NOT external coverage (that is slice e, AI polling). The README + badge wording say "seen N×", not "covered online"; the brief §4 non-goal states the boundary. No salesmanship.
  • R6 — first-mover default (2) too tight / saturationAt default (3) arbitrary. Mitigated: both are CLI-tunable (--first-mover-days/--saturation-at), documented as deliberate defaults (like freshDays 7), grounded in the SSOT timing band (<24-72h) + the existing sett Nx >=2 hint; D1/D2 are operator go-gate knobs.
  • R7 — float/-1 composite sentinel interaction with the new integer key. Mitigated: the temporal key is a separate || term (a small integer diff); it never touches the score?.composite ?? -1 term; the comparator stays a sum-free short-circuit chain (no NaN risk).
  • R8 — editing trend-spotter.md / the SSOT trips the de-niche guard (Section 17). Mitigated: the added prose + the SSOT note carry only generic overlay wording; pillars/topics stay config; Section 17 runs in the gate.
  • R9 — gate checks must survive a deps-absent fresh clone. Mitigated: Section 16m is pure grep/self-test on tracked source (brief.ts + cli.ts; no tsx) → unconditional; TRENDS_TESTS_FLOOR stays inside the deps guard.
  • R10 — STATE's pinned floor line-cites (:709/:1329) are stale (live :713/:1403). Mitigated: caught at brief time (the line numbers drifted when R3c added Section 16l + the floor-history narration); the plan cites live values; Step 8 corrects STATE.
  • R11 — a temporal ordering test that passes WITHOUT the feature (false RED / vacuous GREEN). Mitigated (folded — plan-critic M1): surfacedCount correlates with age, so a naive first-mover-vs-saturated fixture is already ordered by effectiveDate-desc. The SC3 fixture is built to force the temporal key and effectiveDate to disagree (older-neutral A vs newer-warming B at equal composite), so it is RED in Phase A and the new key is provably what decides in GREEN; the coverage-eroded :96-102 is re-based to a shared tier.

Plan-critic — folded

Three Opus reviewers ran on the brief + this plan, each verifying against live code; they converged on the same two defects. Verdicts: scope-guardian MIXED (0 hard creep; both confirmed decisions honored; every SC traces to a step; the floor/line cites verified live); brief-reviewer PROCEED_WITH_RISKS (all seven RED-premise/ correctness claims HOLD; the gap was GREEN-completeness — 1 of 4 breaking assertions listed); plan-critic APPROVE_WITH_NOTES 78/B (floor arithmetic, line-cites, grep sentinels, cmp total-order, two-phase-RED structure all verified correct). Per-finding resolution (full headline list in brief-re-r3d.md §9):

  • [MAJOR — all three] warming badge fired at >=1, but live surfacedToken fires at >=2 (brief.ts:156) + brief.test.ts:408 pins !sett 1x. Step 3 gates the warming badge at surfacings >= 2 (R3b ≥2 contract preserved exactly; :408 unchanged); the warming tier still demotes in rank. SC4 gains the surfacedCount-1 no-badge boundary. The "preserves the hint" wording is corrected.
  • [MAJOR — plan-critic M1 / brief-reviewer MEDIUM-3] the ordering test was not RED + vacuous (surfacedCount correlates with age → effectiveDate-desc already orders first-mover-vs-saturated). Step 1 SC3 fixture forces temporal↔date disagreement (older-neutral A vs newer-warming B, equal composite; expect [Z,A,B], Phase A [Z,B,A]→RED); :96-102 re-based to a shared tier (R11).
  • [MEDIUM — brief-reviewer MEDIUM-2] the ranking: descriptor change breaks :325-331 + :410-416. Step 1
    • the Files table enumerate all four touched assertions, not one.
  • [MINOR — plan-critic m3] temporalSignal undefensive vs saturationAt < 1. Step 2 clamps const at = Math.max(1, opts.saturationAt) inside the pure function (the CLI guard alone is insufficient — the function is a public, gate-grepped export). SC2 clamp case added.
  • [MINOR — plan-critic m4] the "fresh" tier was a misnomer (collides with freshDays; a 30-day unsurfaced trend is not "fresh"). renamed neutral throughout (Steps 13, SCs); the gate sentinel greps "first-mover" (unaffected). The in-bucket effect (unsurfaced ranks above seen-and-skipped within olderMatched) documented in brief §3 as intended.
  • [MINOR — plan-critic m5] future publishedAt (ageDays < 0) became a first-mover "act now" headline. Step 2 adds the ageDays >= 0 guard (future → neutral). SC1 gains the (-1,0) case.
  • [MINOR — plan-critic m6 / brief-reviewer] nightly run locked to default thresholds (the primary saturation consumer). reframed in brief §4 as a known limitation, not a "later nicety."
  • [LOW — brief-reviewer LOW-5] SC6 tier assertions can't read tiers from --json (it omits the body badges). Step 1 Phase B reads the written .md via readFileSync(path); the first-mover marker is also in the --json summary.
  • [LOW — all three] long-form angle cite :34:32 (:34 = currency; substance holds). brief §0/§2 corrected.

Verified correct (no change needed): the floor arithmetic (16m = 1 self-test + 5 greps = 6 → ASSERT_BASELINE_ FLOOR 111→117), all live cites (:713/:1403/:1374/:1376/:57-62), the six ASCII grep sentinels match the literals Steps 24 write, the cmp insertion preserves a total order with no NaN risk, and the two-phase RED structurally avoids a module-load ERROR. This slice is authored + landed in-session (driftsmodell), not as a headless autonomous run, so per-step revert/halt clauses are not needed (R1/R2/R3a/R3b had none either).