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
30 KiB
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;anglealready 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)temporalSignalis a NEW named export of the EXISTINGbrief.ts; Node16 ESM throws a missing named import at module-load (everybrief.test.tstest would error, not assert), so land a non-throwing stub first (temporalSignal → {tier:"fresh",…}+ the type exports +BriefEntry.temporalpopulated by the stub inrankForBrief), 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 existingbriefhandler (the new flags are silently ignored byparseFlagstoday → tuned-threshold behaviour unchanged). Then GREEN: realtemporalSignal+RankOptionsknobs +cmpkey → render tokens + summary marker + descriptor →cli.tsflags → wiretrend-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 D1–D10 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; surfacedToken→temporalToken (badges); briefSummary first-mover marker; ranking: descriptor names the new key |
SC1–SC5, SC7, SC9 |
scripts/trends/src/cli.ts |
EDIT — brief … [--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 |
EDIT — temporalSignal unit incl. clamp + future-date + neutral cases (SC1/SC2/SC9); disagreement ordering fixture (SC3); render badges (SC4) + 4 touched assertions: :407 sett 3x→mettet (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) |
SC1–SC5, SC7–SC9 |
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 |
EDIT — TRENDS_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— addexport type TemporalTier,export interface TemporalSignal, andexport function temporalSignal(): TemporalSignal { return { tier: "neutral", firstMover: false, surfacings: 0, rank: 2 }; }(a constant stub ignoring its args); addtemporal: TemporalSignaltoBriefEntryand populate it inrankForBriefvia the stub. Thecmpkey +temporalToken+ the summary marker are NOT added yet (Steps 2–3). 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>=0guard); the clamp case(5,5,{…,saturationAt:0})→saturatedviaat=1. - ordering (SC3) — DISAGREEMENT fixture (folded — plan-critic M1:
surfacedCountcorrelates with age, so a naive first-mover-vs-saturated fixture is already ordered by the existingeffectiveDate-desc key → GREEN in Phase A, vacuous in GREEN). Three same-overlap entries: Aneutral(surfaced 0, older date ≈5d, composite 7.0); Bwarming(surfaced 2, newer date ≈1d, composite 7.0); Zsaturated(surfaced 4, composite 8.5). ExpecttopMatches.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 insertstemporal.rank) flips A above the newer B, proving the key. - render (SC4) —
renderBrief(...)expects· 🥇 først uteon the first-mover entry,· 🔁 mettet (3x)on asurfacedCount 3entry,· sett 2xonsurfacedCount 2, no badge onsurfacedCount 1, none on aneutralentry (stub render still emits the oldsurfacedToken→ 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(theranking:descriptor) gainthen temporal (first-mover↑/saturated↓),; and theeffectiveDate-isolation test:96-102is re-based so both entries share aneutraltier (else the new key, noteffectiveDate, silently decides it — coverage erosion). - summary (SC5) —
briefSummary(...)with a first-mover top expects· 🥇 først utein the headline (stub → no marker → RED); assert the summary has no"and no\n. - no-score-mutation (SC8) — capture a store's
score.compositevalues, runrankForBrief(pure — no mutation) and assert unchanged; assertBRIEF_SCHEMA_VERSION === 1,SCHEMA_VERSION === 4.
- unit (SC1/SC2/SC9) —
Phase B — subprocess value-RED against the existing CLI handler (no new import; the flags are parsed-but-unused today):
cli.test.ts—brief --pillars ai --first-mover-days 1 --saturation-at 2 --store <seeded> --out <tmp>: todayparseFlagsstores the flags but thebriefhandler ignores them → the rendered tiers match the defaults → RED. The tier badges live in the written.mdbody, not in--json(which returns only{path,date,totals,summary,marked},cli.ts:336), so the assertionreadFileSync(path)and checks the badge (asurfacedCount 2trend renders· 🔁 mettet (2x)at--saturation-at 2, but· sett 2xat the default) — folded — brief-reviewer LOW-5; the first-mover marker is also observable via the--jsonsummary.--first-mover-days x/--saturation-at 0today are ignored (exit 0) → RED against the expectedusageexit 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:
Pure (no clock/fs/env). Branches disjoint + total (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 };firstMoverrequiressurfacings === 0;saturated/warmingboth requiresurfacings >= 1).RankOptions(:51-54) gainsfirstMoverDays?: number+saturationAt?: number.rankForBrief(:78) readsconst firstMoverDays = opts.firstMoverDays ?? 2; const saturationAt = opts.saturationAt ?? 3;and populatesBriefEntry.temporal = temporalSignal(ageDays, trend.surfacedCount, { firstMoverDays, saturationAt })at the push site (:93).cmp(:102-107): insertb.temporal.rank - a.temporal.rank ||afterb.overlap - a.overlap ||and beforeb.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) withtemporalToken(e: BriefEntry): string:
Replace theconst 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 1surfacedToken(e)call inrenderTopEntry(:162) andrenderBulletEntry(:171) withtemporalToken(e). (scoreTokenuntouched.) briefSummary(:131-141): computeconst 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'sranking:descriptor (:187): insertthen temporal (first-mover↑/saturated↓),betweenpillar-overlap desc,andthen 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:154and## 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, thecmpintegration (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 thetests Nline reported by(cd scripts/trends && npm test)after Steps 1–5 — 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, notsx, all literals ASCII — the badge emoji are NEVER grepped; the shell must stay ASCII-clean for bash 3.2set -u). Placement (verified live): between Section 16l's trailingecho ""(: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 carryingtemporalSignalaccepted, 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_FLOORintest-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.sh → FAIL=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
cmpre-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
surfacedToken→temporalTokenpromotion breaks pinned R3b/descriptor assertions. Mitigated/expected (folded — all three reviewers): the warming badge is gated atsurfacings >= 2, sobrief.test.ts:408(!md.includes("sett 1x")) stays green and the R3b ≥2 contract is preserved exactly; Step 1 enumerates the four touched assertions (:407sett 3x→mettet (3x);:325-331+:410-416descriptor) and re-bases the coverage-eroded:96-102.surfacedCount 2still 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 inbrief.tssource + rendered output, asserted by the TS tests, never by the shell gate. - R4 — the summary marker breaks the SessionStart extractYaml regex. Mitigated:
🥇 først utecarries 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 (likefreshDays7), grounded in the SSOT timing band (<24-72h) + the existingsett Nx>=2hint; D1/D2 are operator go-gate knobs. - R7 — float/
-1composite sentinel interaction with the new integer key. Mitigated: the temporal key is a separate||term (a small integer diff); it never touches thescore?.composite ?? -1term; 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; notsx) → unconditional;TRENDS_TESTS_FLOORstays 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):
surfacedCountcorrelates with age, so a naive first-mover-vs-saturated fixture is already ordered byeffectiveDate-desc. The SC3 fixture is built to force the temporal key andeffectiveDateto disagree (older-neutralA vs newer-warmingB at equal composite), so it is RED in Phase A and the new key is provably what decides in GREEN; the coverage-eroded:96-102is 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 livesurfacedTokenfires at>=2(brief.ts:156) +brief.test.ts:408pins!sett 1x. ✅ Step 3 gates the warming badge atsurfacings >= 2(R3b ≥2 contract preserved exactly;:408unchanged); the warming tier still demotes inrank. 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 (
surfacedCountcorrelates with age →effectiveDate-desc already orders first-mover-vs-saturated). ✅ Step 1 SC3 fixture forces temporal↔date disagreement (older-neutralA vs newer-warmingB, equal composite; expect[Z,A,B], Phase A[Z,B,A]→RED);:96-102re-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]
temporalSignalundefensive vssaturationAt < 1. ✅ Step 2 clampsconst 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"). ✅ renamedneutralthroughout (Steps 1–3, SCs); the gate sentinel greps"first-mover"(unaffected). The in-bucket effect (unsurfaced ranks above seen-and-skipped withinolderMatched) documented in brief §3 as intended. - [MINOR — plan-critic m5] future
publishedAt(ageDays < 0) became a first-mover "act now" headline. ✅ Step 2 adds theageDays >= 0guard (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.mdviareadFileSync(path); the first-mover marker is also in the--jsonsummary. - [LOW — all three] long-form
anglecite: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 2–4 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).