R3 slice 1 (research-deepening): persist a 4-field TrendScore {mode, dimensions,
composite, priority} on the store record (schema v2->v3, additive lossless), computed
by the already-built score.ts (composite+band, one owner), threaded item->store, and
rank rankForBrief on composite first + surface band+mode in renderBrief.
Go-gate confirmed (operator "Go"): D1 4-field envelope · D2 composite primary within
bucket · D3 first-sight only · D4 one slice · D6 mode shown per body entry.
Light-Voyage: scope-guardian ALIGNED (0) / brief-reviewer PROCEED_WITH_RISKS (6 MINOR)
/ plan-critic REVISE (1 BLOCKER, 4 MAJOR, 4 MINOR) — all folded. Headline fold: the RED
proof is now explicitly two-phase (logic-RED for store/brief/cli; stub-first then
assertion-RED for score/item, since a missing named import throws at module-load under
Node16 ESM, not on assertion).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
31 KiB
Brief — RE-R3a: persist the relevance score + rank the morning brief on it
Slice: RE-R3a (research-engine rung-2, R3 slice 1 — research-deepening). R3 ("deepen the research engine") is an arc of 5 open hulls (substrate §1: autonomous trigger · freshness-as-seen-log · relevance/saturation/status scoring · brief history+diff · A1–A4 fan-out). R3a takes the first: the relevance half of hull 5 (and the remainder of hull 3 — "the store schema lacks fields a brief ranks on"). It persists the composite relevance score the
trend-spotteragent ALREADY computes, onto the store record, and makesrankForBrieforder on it. Predecessor: RE-R1 (score.ts, B2 triage-scorer — built, tested, deterministic) + RE-R2a (capturebridge +publishedAt, schema v1→v2) + RE-R2b (brief.tsdated artifact + surfacing). R2b explicitly deferred this in its §4: "the B2 triage scorer stays out of the brief path — its output isn't persisted on records yet — that's R3." R3a is exactly that R3 step. Substrate:docs/research-engine-concepts.local.md§1 hull (3) (schema fields a brief ranks on: relevance/...) + (5) (relevance scoring) + §B2 ("scoring/filtering as a gate before expensive work — the output is the rank key") + §A2 ("curate/score before synthesis — the writer sees ranked material"). TDD-order: RED tests land before code — but as two phases (light-Voyage BLOCKER fold): the store/brief/cli tests are true logic-RED against the pre-edit code (they build fixtures inline, import no new symbol); the score/item tests reference not-yet-existingscore.tsexports, so under Node16 ESM a missing named import throws at module-load (not on assertion) — they are RED against non-throwing stubs landed as the first GREEN-prep sub-step. See plan Step 1.
1. Operator decision context (2026-06-24)
The research engine is Tier-1 (operator, 2026-06-23): "hele min arbeidsflyt hviler på at jeg får en jevn
strøm av gode forslag til tema å skrive om." RE-R2 made the stream visible (a dated morning brief surfaced
at session-start). R2b ranks that brief on pillar-overlap + recency only — a coarse proxy for "good topic
to write about." The actual relevance judgment (audience pull, timing, angle potential, authority, depth) lives
in the five 1–10 dimension scores the trend-spotter agent produces in Step 2 and pipes to the score CLI —
and is then thrown away before the trend reaches the store (Step 4.5 builds a separate, score-free capture
batch). R3a stops discarding it: persist the composite + band on the record, and rank the brief on composite
first. The slice the operator chose ("scoring inn i briefen", 2026-06-24) — the highest-leverage next step
on the core need (better-ordered suggestions), built on already-shipped-but-dormant code (score.ts is tested
and unused on records). The bigger R3 arcs (autonomous trigger / seen-log / saturation+status / A1–A4 fan-out)
stay later slices.
Go-gate resolutions — CONFIRMED (operator "Go", 2026-06-24; baked into the plan): D1 persist the
4-field TrendScore { mode, dimensions, composite, priority } (composite+priority to rank/display, mode to
disambiguate the instrument, dimensions for audit + lossless re-weight). D2 composite is the primary
within-bucket sort (buckets still assigned by overlap+freshness; composite orders inside a bucket). D3
score is first-sight (set on add, never updated on re-capture — matches the store's provenance discipline;
re-score-on-recapture pairs with the R3b seen-log/status slice). D4 ship persist+rank as one slice (the
operator named the visible payoff; splitting would land an invisible schema-only cut like R2a).
2. The gap — grounded in code
- The score the agent computes never reaches the store.
trend-spotter.mdscores each candidate's five dimensions and pipes them to thescoreCLI (agents/trend-spotter.md:134-140), which returns{composite, band}per candidate (score.ts:110-122triage). But Step 4.5's capture batch (agents/trend-spotter.md:291-298) is built separately and carries no score —source/title/url/topics/ publishedAt/summaryonly.TrendItem(item.ts:22-39) andTrendRecord(types.ts:26-48) have no score field. The relevance judgment is recomputed for the digest and discarded for persistence. score.tsis built, tested, deterministic — and unconsumed on records. It exportscomposite()(score.ts:77-88) andband()(score.ts:91-97) as pure functions, pinned to the SSOT (references/trend-scoring-modes.md, byscore.test.ts:12-30weights + the band-string assertions). Nothing persists their output.TrendRecord's own doc-comment anticipates the field: "can gain fields (relevance score, first-mover timing, status) in a later slice" (types.ts:21-23).- The brief ranks on a proxy.
rankForBriefsorts each bucketoverlap desc → effectiveDate desc → title asc → url asc(brief.ts:94-104). Overlap (a hard pillar count) is part of what the composite already weights (Pillar Fit 30 %,trend-scoring-modes.md:43), but the composite also captures audience/ timing/angle/authority — signal the brief currently can't see.brief.ts's own header already names this as the next slice: "A persisted relevance/saturation score … (R3)" (brief.ts:11-14).
3. Scope — what is IN (RE-R3a)
S-score — scripts/trends/src/score.ts (EDIT)
export interface TrendScore { mode: ScoreMode; dimensions: DimensionScores; composite: number; priority: Priority }— the persist-ready envelope. Lives inscore.ts(the score domain owns it);types.tsimports it (one-way:score.tsimports nothing — verified leaf,:1-17— so no cycle).export function requiredDimensions(mode: ScoreMode): string[]—Object.keys(WEIGHTS[mode])(score.ts:37-40). Contract: ordered — the keys come back in the SSOT weight-literal order (kortform["pillar","audience","timing","angle","authority"], long-form["pillar","depth","angle","authority", "currency"],score.ts:20-35); SC1 deep-equals that ordered array, andscore.testpins the order so a silent SSOT reorder fails loudly.normalizeItemconsumes it as a set (membership), which is order-safe either way.WEIGHTSstays private; the keys are exposed via this function.export function scoreEnvelope(mode: ScoreMode, dimensions: DimensionScores): TrendScore— composes the existing pure functions:const c = composite(dimensions, mode); return { mode, dimensions, composite: c, priority: band(c).priority }. No new arithmetic —composite()+band()stay the single owners (SSOT discipline). It throws (viacomposite,score.ts:83) on an out-of-range dimension — that is its contract, exercised directly by SC1/SC2; on the capture path it is unreachable becausenormalizeItempre-validates (below).
S-types — scripts/trends/src/types.ts (EDIT)
import type { TrendScore } from "./score.js";TrendRecordgainsscore?: TrendScore;(optional — pre-R3a records simply lack it; theaddmanual path and unscored adopters omit it). Doc-comment updated to markscoreas the now-realized field the:21-23note anticipated.SCHEMA_VERSION = 2 → 3(types.ts:62). The bump is the only schema signal; the record shape change is additive-optional, so the migration is the version-stamp alone (below).
S-store — scripts/trends/src/store.ts (EDIT)
TrendInput(store.ts:26-35) gainsscore?: TrendScore;(imported fromscore.js).addTrend(store.ts:120-140): on a new record, persistscorefirst-sight via the existing conditional-spread idiom (...(input.score !== undefined ? { score: input.score } : {}), mirroringpublishedAt/summary:134,136). On a duplicate, score is NOT updated (D3 — first-sight, likesource/capturedAt/firstpublishedAt); topics still union (:124-126, unchanged).AddResultis unchanged (no new flag).loadStoremigrate comment (:79-84): extend to "v1→v2→v3 are all purely additive-optional (an old record is already a valid record that simply lacks the optional field), so the migration is the version stamp alone — records pass through untouched." No code change to the migration logic (Math.max(onDisk, SCHEMA_VERSION):87already does v2→v3 correctly;saveStoreJSON.stringify:95preserves thescorefield — no field stripping); onlySCHEMA_VERSION(intypes.ts) and the comment move.
S-item — scripts/trends/src/item.ts (EDIT)
TrendItem(item.ts:22-39) gainsscore?: { mode: ScoreMode; dimensions: DimensionScores };— the ingress envelope carries the agent's judgment (five scores + mode), not a precomputed composite (the store computes it, so the composite has one owner).import type { ScoreMode, DimensionScores } from "./score.js"+import { requiredDimensions } from "./score.js".normalizeItem(:86-119): ifr.scorepresent, validate structurally (returns a structured error intoerrors[], never throws — the existing discipline, like thepublishedAtISO check:99-106):scoreis a non-array object;mode ∈ {kortform, long-form};dimensionsis a non-array object; each key inrequiredDimensions(mode)is present and a number in [1,10]. On any failure →errors.push("invalid score: …"). On success carry the validatedscore = { mode, dimensions }forward (the validated dimensions object, not rawr.score.dimensions). Absent/null/invalid → key omitted. This guarantees the capture path (cli.ts:246-254:normalizeItems→itemToInput) never reachescompositewith bad dims.itemToInput(:129-139): ifitem.scorepresent → addscore: scoreEnvelope(item.score.mode, item.score.dimensions)to the returnedTrendInput(conditional spread, key omitted when absent). The item→store bridge is the natural place to turn judgment into the persisted envelope.itemToInputis a public function: called directly (e.g. in a test) with unvalidated dims it throws by contract (defense-in-depth); the no-throw guarantee is a property of the capture path, not ofitemToInputin isolation (§5).
S-brief — scripts/trends/src/brief.ts (EDIT)
rankForBrief(:72-114): composite becomes the primary within-bucket sort key (D2). The comparator (:94-98) gains a leading term:(b.trend.score?.composite ?? -1) - (a.trend.score?.composite ?? -1) || <existing overlap desc → effectiveDate desc → title asc → url asc>. Sentinel-1, not-Infinity— composite is a weighted sum of [1,10] dims so it is always ≥ 1.0 (min = 1×Σweights = 1.0, verified);-1sorts every unscored record below every scored one and subtracts cleanly (-Infinity - -Infinity = NaNwould corrupt the comparator). Buckets are UNCHANGED — assignment staysoverlap≥2 & fresh/overlap==1 & fresh/!fresh(:100-104); composite only re-orders within a bucket. Total order preserved: the(title,url)pair is unique per store (it is the dedupe id,store.ts:66-68), so the finalurl asctie-break makes the order insertion-independent even for equal composites.renderBrief(:152-191): surface the band and mode where a record is scored (so a reader can tell a kortform "High" from a long-form "High" — the two are different instruments). Pinned line shapes:- Top-entry meta line (
renderTopEntry,:135), scored:- Kilde: <source> · Publisert: <date> (<age>d) · <priority> (<mode>) · Pillarer: <matched>(the· <priority> (<mode>)token sits between(<age>d)and· Pillarer); unscored: unchanged (no token). - Bullet line (
renderBulletEntry,:144), scored:- **<title>** — «<matched>» · <date> (<age>d) · <priority> (<mode>) · 🔗 <url>(token before· 🔗); unscored: unchanged. - The
ranking:frontmatter descriptor (:160) → the exact stringcomposite desc, then pillar-overlap desc, then publishedAt desc (capturedAt fallback); freshDays <N>(pinned verbatim;brief.testasserts it byte-for-byte).
- Top-entry meta line (
briefSummary(:122-130): the top mention names the band only (mode stays a body-entry detail to keep the one-line headline clean) — fresh>0 with a scored top →… Topp: «<title>» (<pillar> · <priority> · <age>d).; fresh>0 with an unscored top →… Topp: «<title>» (<pillar> · <age>d).(no token). Still one line, no", no\n— theextractYamlcontract (brief.ts:118-120) holds; the band strings (Immediate/High/…) are bare words.BRIEF_SCHEMA_VERSIONstays 1 (no frontmatter field added/removed; the surfacing hook still readsdate+summary; only theranking:descriptor string and body content change). Bumping is an Open Q (§8), not required for correctness.
S-cli — scripts/trends/src/cli.ts (EDIT, doc-only behavior)
- The
capturebranch (:243-269) folds viaitemToInput(:254) — so onceitem.tsthreadsscore, capture automatically persists it with no logic change. Update only the header doc-comment (:15-21) to note capture now persists an optional relevance score. ThescoreCLI (:218-241, the digest path) and theaddmanual path (:123-147, score-free) are unchanged. (Capture's{added, merged, duplicates, errors}tally is left unchanged — ascoredcount is an Open-Q nice-to-have, §8.)
Wiring (D-default — WIRE, mirrors R2a/R2b Open Q#1)
agents/trend-spotter.md(EDIT): Step 4.5's capture batch (:291-298) gains a per-item"score": {"mode": "kortform", "dimensions": {"pillar": N, "audience": N, "timing": N, "angle": N, "authority": N}}— the same five judgment scores the agent computed in Step 2 (:134), carried into capture so the store persists them and the brief ranks on them. Prose explains the carry ("don't discard the Step-2 scores — fold them into the capture batch"). Mode defaultskortform;long-formwhen invoked from/linkedin:newsletter(long-form dimspillar/depth/angle/authority/currency). Domain-general (dimensions are the rubric's, pillars are the user's config; no vendor/sector tokens). Keep the "skip silently if no deps" escape hatch. Verified non-vacuous:agents/trend-spotter.mddoes NOT currently contain the literal"dimensions", so the Section 16j grep passes only after the wire is added.scripts/trends/README.md(EDIT): document the itemscorefield (judgment in), the persistedTrendScore(composite/priority out), and that the brief now ranks on composite.scripts/test-runner.sh(EDIT): bumpTRENDS_TESTS_FLOOR(:701, currently 104) to thetests Nline reported after Steps 1–6, append+ RE-R3a: score +Nto the inline breakdown comment (:701). Add Section 16j ("Trends Score Wiring", RE-R3a) after Section 16i's closingecho ""(~:1171), before the Section 18 block (:1173) (16i is the last 16x before anti-erosion; file order 17→16g→16h→16i→18,:947/:1014/:1078/:1122/:1173). Mirror 16i's shape: unconditional, deps-absent-safegrep -qF+ a non-vacuity self-test emitting one pass/fail (so the count is exact) — (1) self-test; (2)export interface TrendScoreinscore.ts; (3)score?: TrendScoreintypes.ts; (4)"dimensions"inagents/trend-spotter.md; (5)score?.compositeinbrief.ts. 5 unconditional emitters → bumpASSERT_BASELINE_FLOOR94 → exactly 99 (:1193; "live recount" is the safety net, but the expected value is the pinned 94 + 5 = 99). 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.
4. Non-goals — what is OUT (deferred)
- Re-score on re-capture (refresh the score when a trend is re-seen) — R3b. R3a is first-sight only
(D3). Re-score pairs naturally with the seen-log/status slice (the Timing dimension decays, so a refresh is a
real improvement — but it expands
addTrend's mutation surface and wants the status/lifecycle model alongside). - Mode-segmented / mode-normalized ranking — OUT. R3a ranks all records by composite regardless of mode;
a kortform composite and a long-form composite are different instruments (different dimensions,
trend-scoring-modes.md:50,68), so the ranking is mode-blind by design for R3a. This is acceptable because (a) almost all records arekortform(the default), and (b) the body entry line shows the mode (<priority> (<mode>)) so the operator can see when two adjacent entries were scored on different instruments. A mode-segmented brief (separate sections per mode) or a--modefilter is a later refinement. - Saturation / status (acted/skipped) / first-mover-as-a-field (the rest of hull 5) — R3b+. R3a does the relevance half of hull 5 only.
- Autonomous nightly trigger (cron/launchd, hull 1) — R3 later. No scheduler enters the repo.
- Freshness as a persisted seen-log / dedup-vs-seen (B4) — R3 later.
- Brief history surfacing / diff ("yesterday vs today", hull 7) — R3 later.
- Research-deepening A1–A4 (plan → isolated parallel workers → gap loop → curate), adapter sub-agents, MCP fetch fan-out — R3 later (the big slice).
- A new
scorefield in theaddmanual CLI path — OUT.addstays the raw, score-free manual path; only the normalizingcapturepath carries scores. BRIEF_SCHEMA_VERSIONbump — OUT by default (no frontmatter field changes); Open Q#5.- New source file / new agent / new command — none. R3a is all edits to the six existing
src/*.ts+ one agent + README + gate. Counts stay 27/19/29.
5. Boundaries / invariants (must hold)
- TDD iron law (two-phase RED): the failing tests land BEFORE the implementation.
store/brief/clitests are true logic-RED against the pre-edit code (inline fixtures, no new import).score/itemtests reference newscore.tsexports → under Node16 ESM a missing named import throws at module-load, so they are RED against non-throwing stubs landed first (the stubs return wrong-but-present values; the value assertions then fail). The plan records the RED proof in two phases (Step 1); it does NOT claim a single "all five fail on assertion before any code" run. - One composite owner:
composite()+band()(score.ts) stay the sole arithmetic;scoreEnvelopecomposes them, never re-derives. The agent supplies judgment, the code computes the composite (SSOT discipline,score.test.ts:12-30pins the weights/bands). - Purity:
scoreEnvelope/requiredDimensions/rankForBrief/renderBrieftouch no fs, no clock, no env, no AI. All fs stays at the CLI edge. - No throw on the capture path (not "everywhere"):
normalizeItemfully validates the score beforeitemToInput, so the capture loop (cli.ts:246-258) never reachescompositewith bad dims and never crashes (a bad score →errors[]).itemToInput/scoreEnvelope/compositecalled directly with bad dims throw by contract — that is the defense-in-depth boundary, asserted (SC2), not a leak. - Determinism: same
(store, pillars, today, freshDays)→ byte-identical brief (the composite sort is a total order via the unique(title,url)final tie-break;-1sentinel for unscored is deterministic). - Lossless additive migration (both directions): a v2 store loads as v3 with records untouched (no
scoreinvented); round-trip writesschemaVersion: 3; a v3 store is idempotent; a v3 store's new optionalscorefield survives a load+resave (no field stripping,JSON.stringifystore.ts:95). Mirrors the R2a v1→v2 proof (store.test.ts:403-476) + a new field-preservation case. - Hook unaffected: the SessionStart surfacing reads
date+summaryonly and never shells out to tsx (analytics fresh-clone-crash invariant) — R3a touches neither the hook nor the frontmatter schema, so the zero-tsx surfacing is unchanged. (No hook test added; the existing hook suite must still pass untouched as a regression sanity.) - Domain-general: Section 17 de-niche stays green; the
trend-spotter.mdedit carries the rubric's dimension names + the user's pillars, no vendor/sector tokens. - No SSOT change:
references/trend-scoring-modes.md(weights/bands/actions) untouched;score.tsmirrors it exactly as today. - No store-query change:
queryByTopic/history/newestCaptureDateuntouched; the brief recomputes overlap as before (queryByTopicNOT refactored). - Pathguard: all edits are to existing files (no new
.mjsunderhooks/scripts/; no new.ts— R3a adds no source file)..gitignorealready coversscripts/trends/{node_modules,build}. - Counts (refs/agents/commands 27/19/29) unchanged. Recounted live at land, never pinned/guessed.
6. Success criteria (testable)
- SC1 (score envelope) —
requiredDimensions("kortform")deep-equals (ordered)["pillar","audience", "timing","angle","authority"];requiredDimensions("long-form")deep-equals["pillar","depth","angle", "authority","currency"](theWEIGHTSliteral order,score.ts:20-35), andscore.testpins the order so a SSOT reorder fails.scoreEnvelope("kortform", {pillar:8,audience:7,timing:9,angle:6,authority:5})returns{ mode:"kortform", dimensions:<the five>, composite: composite(dims,"kortform"), priority: band(composite). priority }— composite/priority equal the existing functions' output byte-for-byte (one owner); a bad dimension makesscoreEnvelopethrow (viacomposite). - SC2 (item validation + bridge + the throw contract) —
normalizeItemon an item with a validscorecarries the validated dims; with a badmode, a missing dimension, a dimension out of [1,10], a non-objectscore, or an arraydimensions→{ ok:false, errors:["invalid score: …"] }(structured, never throws); absentscore→ key omitted.itemToInput(validItemWithScore, capturedAt)returns aTrendInputwhosescoreisscoreEnvelope(mode, dimensions)(composite/priority computed); without a score → noscorekey;itemToInputcalled directly with an out-of-range dim throws (the defense-in-depth contract). - SC3 (first-sight persist) —
addTrend(store, inputWithScore)on a new title+url persistsscoreon the record; re-addTrendof the same title+url with a different score does NOT change the stored score (first-sight, D3) while topics still union; an input without a score adds a score-free record. - SC4 (migration v2→v3, both directions) — a
schemaVersion:2store with records lackingscoreloads as v3, records intact, noscoreinvented; round-triploadStore→saveStorewritesschemaVersion:3; a v3 store withscoreon records loads idempotent; a v3 store'sscorefield survives load+resave (field preservation of a new optional field — not covered by the mirrored v1→v2 block). Mirrorsstore.test.ts:403-476, retitled(RE-R3a / score v2→v3)with everyschemaVersionassertion literal flipped2→3. - SC5 (brief ranks on composite) — within a bucket,
rankForBrieforders composite desc first (a composite-9 record ahead of a composite-6 record at the same overlap); an unscored record sorts after every scored record in its bucket (the-1sentinel) and then by the existing keys; buckets are unchanged (still overlap+freshness); the order is a total order (same-title/diff-url, both unscored → fixed byurl asc); same input → byte-identical brief (determinism). - SC6 (render surfaces band + mode) —
renderBriefemits the full pinned line shapes (§3): a scored top-entry shows· <priority> (<mode>)between(<age>d)and· Pillarer; a scored bullet shows· <priority> (<mode>)before· 🔗; an unscored entry renders the unchanged line (no token) — both asserted as full lines, not substrings.briefSummarynames the band (no mode) on a scored top, omits the token on an unscored top, and stays one line with no"/\neven when the top title contains a guillemet/ quote (the only new code path touching the summary). Theranking:frontmatter descriptor equals the pinned string verbatim. A store whose only fresh match is a single-pillar unscored record →briefSummaryrenders with no· <priority>token, one line. - SC7 (CLI persists score end-to-end) —
echo '[{…,"score":{"mode":"kortform","dimensions":{…valid…}}}]' | … capture --store <tmp>then… list --store <tmp> --jsonshows the record carrying ascorewith the computed composite/priority; a batch with one bad score → that item inerrors[], the valid ones added, exit 0 (the run isn't failed). - SC8 (gate + wiring + de-niche) —
bash scripts/test-runner.sh→FAIL=0: trends suite green at the bumpedTRENDS_TESTS_FLOOR; new Section 16j green (TrendScoreinscore.ts,score?: TrendScoreintypes.ts,"dimensions"intrend-spotter.md,score?.compositeinbrief.ts, non-vacuity self-test);ASSERT_BASELINE_FLOOR= 99 (94 + 5); Section 17 de-niche green; counts 27/19/29.
7. Verification
Deterministic: bash scripts/test-runner.sh → FAIL=0; trends suite ≥ new floor; new Section 16j
self-test + greps pass; ASSERT_BASELINE_FLOOR = 99; Section 17 de-niche green; ref/agent/command counts
unchanged. Regression sanity: node --test hooks/scripts/__tests__/ → still green untouched (R3a touches no
hook; adds no hook test).
Behavioural (manual):
echo '[{"source":"tavily","title":"A","url":"https://e/a","topics":["ai","gov"],"publishedAt":"<~2d ago>", "score":{"mode":"kortform","dimensions":{"pillar":9,"audience":8,"timing":9,"angle":7,"authority":6}}}, {"source":"tavily","title":"B","url":"https://e/b","topics":["ai","gov"],"publishedAt":"<~2d ago>", "score":{"mode":"kortform","dimensions":{"pillar":6,"audience":5,"timing":6,"angle":5,"authority":5}}}]' | node --import tsx src/cli.ts capture --store /tmp/r3a.json— both overlap-2 & fresh, A scored higher.node --import tsx src/cli.ts list --store /tmp/r3a.json --json→ confirm both records carryscorewith computed composite/priority.node --import tsx src/cli.ts brief --pillars ai,gov --store /tmp/r3a.json --out /tmp/r3a-brief --json→ confirm A precedes B intopMatches(higher composite, same overlap+freshness), the entry line shows· <priority> (kortform), and thesummarynames A with its band.- Append a bad-score item (
"timing":99) to the batch and re-capture→ confirm it lands inerrors[], the valid items still added, exit 0.
8. Open questions for the go-gate — RESOLVED
D1–D4 confirmed by the operator ("Go", 2026-06-24): D1 4-field TrendScore; D2 composite primary within
bucket; D3 first-sight; D4 one slice (data-then-visible commit order within it). Two residual cosmetics,
both baked to the recommended default:
- D5 —
BRIEF_SCHEMA_VERSION1→2? No (no frontmatter field added/removed; the hook reads onlydate+summary). Re-open only if the artifact should self-announce the ranking change. - D6 — mode in the per-entry render? YES (folded from plan-critic #3): the body entry shows
<priority> (<mode>); the summary shows the band only. This makes the mode-blind ranking honest (the reader can see the instrument).
9. Light-Voyage review — folded
Three Opus reviewers ran on the drafts, each verifying claims against live code. scope-guardian: ALIGNED
(every SC1–SC8 traces to a step; zero creep; all §4 non-goals held; counts 27/19/29 verified live; "no new
source file" verified — exactly 6 src/*.ts + 5 tests/*.test.ts, all edited, none added; 0 findings).
brief-reviewer: PROCEED_WITH_RISKS (all four load-bearing claims — score.ts-is-a-leaf/no-cycle, composite ≥
1.0, version-stamp-only migration, single-owner arithmetic — verified TRUE; 6 MINOR). plan-critic: REVISE
(1 BLOCKER, 4 MAJOR, 4 MINOR). All findings folded; see plan-re-r3a.md §Plan-critic — folded for per-step
resolution. Headlines:
- [BLOCKER, folded] the "all five test files fail on assertion after Step 1" RED claim is false for
score/itemunder Node16 ESM (a missing named import throws at module-load, not on assertion). → RED is now explicitly two-phase: logic-RED forstore/brief/cliagainst pre-edit code; stub-first then assertion-RED forscore/item(§5; plan Step 1; the header blockquote). - [MAJOR, folded] the no-throw guarantee was overstated ("unreachable" — but
itemToInputis public and throws on direct bad-dim calls). → reworded path-specific (no throw on the capture path; direct calls throw by contract); SC2 asserts both (§5, §6). - [MAJOR, folded] mode-mixing was waved away and "mode shown per entry" contradicted the render spec (which
only showed priority). → the render now shows
<priority> (<mode>)per body entry (D6); §4 states mode-blind ranking is accepted for R3a with the mode visible; SC6 asserts the full line incl. mode. - [MAJOR, folded]
requiredDimensionsorder contract was ambiguous (SC1 hard-coded arrays vs membership use). → pinned ordered (SC1 deep-equals the SSOT-order array;score.testpins order;normalizeItemuses membership) (§3 S-score, SC1). - [MAJOR, folded]
ASSERT_BASELINE_FLOOR"~99" was not pinned. → pinned 99 (94 + 5 unconditional 16j emitters; self-test emits one pass/fail like 16i) (§3 wiring, SC8). - [MINOR, folded] SC4 ref
:403-471stale + pointed at v2 assertions →:403-476+ "flip everyschemaVersionliteral 2→3" note (SC4). [MINOR, folded] R1 SSOT-pin cite was the doc-comment → nowscore.test.ts:12-30(§2, §5; plan R1). [MINOR, folded] bullet· <priority>placement was substring-only → full pinned line shape, priority+mode before🔗, asserted as a full line (§3, SC6). [MINOR, folded] three divergingranking:descriptor strings → one verbatim target, asserted byte-for-byte (§3, SC6). [MINOR, folded] unscored single-match-top summary path untested → added as an SC6 case. [MINOR, folded]normalizeItemnon-array object case understated → "non-array" added to both object checks + SC2. [MINOR, folded] header-chain line-ref tightened to the 16i clause:46-49/ Section-18:49. [MINOR, folded] R9 DAG now lists the three new one-wayscore.ts ←edges. [MINOR, folded] SC4 forward-compat / score-survives-round-trip added. [MINOR, folded] SC6 quote-safety regression (scored top title with a guillemet) added.