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
29 KiB
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 forstore/brief/cliagainst the pre-edit code (inline fixtures, no new import); (B) forscore/item, land non-throwing stubs for the newscore.tsexports 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) → wiretrend-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-fieldTrendScore· 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 · <priority> (<mode>) (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 (· <priority> (<mode>) 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 hasscore; a secondaddTrend(same title+url, different score) → stored score unchanged,added:false, topics unioned; score-free add → score-free record. Migration: aschemaVersion:2store with score-less records →loadStoregivesschemaVersion:3, records intact,"score" in record === false(not invented); round-trip writesschemaVersion:3; a v3 store withscoreis idempotent; a v3 store'sscoresurvives load+resave (read back, re-save, re-read —scorebyte-identical). (Mirror the RE-R2a blockstore.test.ts:403-476, retitled(RE-R3a / score v2→v3), everyschemaVersionassertion literal flipped2→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-1sentinel); a same-title/diff-url both-unscored pair → fixed byurl asc(total order intact);renderBrieffor a scored top entry contains the full line… (<age>d) · <priority> (<mode>) · Pillarer: …, for an unscored one the unchanged line (no token) — assert full lines, not substrings; a scored bullet contains… (<age>d) · <priority> (<mode>) · 🔗 …;briefSummarynames 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· <priority>token; theranking:line equals the pinned string verbatim; tworenderBriefcalls byte-identical.cli.test.ts: acapturebatch (subprocess,--storetemp) with a valid per-itemscore→ a followinglist --store <tmp> --jsonshows the record'sscorewith the computed composite/priority; a batch with one bad score (timing:99) → JSONerrors[]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.tsso 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 matchesObject.keys(KORTFORM_WEIGHTS)so a SSOT reorder fails);scoreEnvelope(mode, dims)returns{mode, dimensions, composite, priority}with composite/priority equal tocomposite(dims,mode)/band(...).priority(assert against the existing functions — not hard-coded — so they share one owner); a bad dim makesscoreEnvelopethrow (it callscomposite). Fails on assertion against the stubs ([] ≠ expected,composite 0 ≠ real).item.test.ts: a validscore→normalizeItemok:truecarrying the validated dims; badmode/ a missing dimension / a dim0or11/ a non-objectscore/ an arraydimensions→ok:falsewith aninvalid scoreerror (does not throw); absentscore→ key omitted.itemToInput(validItemWithScore, "2026-06-24")→scoreequalsscoreEnvelope(mode,dims); without score → noscore;itemToInputwith 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.testpins 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 viacompositeon a bad dim (its contract). Replace the Phase-B stubs. Makescore.testgreen.
Step 3 — (GREEN) types.ts + store.ts (schema v3 + first-sight persist)
types.ts:import type { TrendScore } from "./score.js";; addscore?: TrendScore;toTrendRecord(doc-comment marks it as the realized:21-23field);SCHEMA_VERSION = 3.store.ts: addscore?: TrendScore;toTrendInput(import type { TrendScore } from "./score.js"); inaddTrend's new-record branch add...(input.score !== undefined ? { score: input.score } : {})(after thesummaryspread:136); the duplicate branch is unchanged (topics union only — score is first-sight, D3). Extend theloadStoremigrate comment to "v1→v2→v3 all additive-optional … the migration is the version stamp alone" — no logic change (Math.max(onDisk, SCHEMA_VERSION):87already handles v3;saveStoreJSON.stringify:95preservesscore). Makestore.testgreen (first-sight + v2→v3 + survives-round-trip).
Step 4 — (GREEN) item.ts (validate + bridge)
TrendItemgainsscore?: { mode: ScoreMode; dimensions: DimensionScores };(import type { ScoreMode, DimensionScores } from "./score.js"+import { requiredDimensions } from "./score.js").normalizeItem: after thepublishedAtvalidation (:99-106), ifr.score !== undefined && r.score !== null, validate:scoreis a non-array object;mode∈{kortform, long-form};dimensionsis a non-array object; for each key inrequiredDimensions(mode), the value is a number in [1,10]. On any failureerrors.push("invalid score: <reason>"); on success buildscore = { mode, dimensions }from the validated values (not rawr.score.dimensions). Carried into the returnedTrendItemvia conditional spread (key omitted when absent/invalid). Never throws — structured errors only (thepublishedAtdiscipline).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 bynormalizeItem, soscoreEnvelope→compositecannot throw there; called directly with bad dims it throws by contract (defense-in-depth — SC2 asserts it). Makeitem.testgreen.
Step 5 — (GREEN) brief.ts (composite sort + render band+mode)
rankForBriefcomparator (:94-98): prepend(b.trend.score?.composite ?? -1) - (a.trend.score?.composite ?? -1) ||before the existingb.overlap - a.overlap || …. Sentinel-1(composite ≥ 1.0 always;-1sorts unscored last and subtracts cleanly). Buckets (:100-104) and totals unchanged.renderTopEntry(:132-141): whene.trend.scoreis 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 inbrief.test.briefSummary(:122-130): in the fresh>0 branch, whentop.trend.scoreis present include· ${top.trend.score.priority}in the top mention (band only — no mode to keep the one-line headline clean):… Topp: «<title>» (<pillar> · <priority> · <age>d).; when the top is unscored omit the token:… Topp: «<title>» (<pillar> · <age>d).Keep it one line, no"/\n.renderBrief'sranking:line (:160) → the exact pinned stringcomposite desc, then pillar-overlap desc, then publishedAt desc (capturedAt fallback); freshDays ${ranking.freshDays}(asserted byte-for-byte).BRIEF_SCHEMA_VERSIONstays 1. Makebrief.testgreen.
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 thetests Nline reported by(cd scripts/trends && npm test)after Steps 1–6 — recounted live, NOT additive-guessed. Stays inside theif [ -x …/tsx ]deps guard. Append+ RE-R3a: score +Nto 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 closingfi/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 (puregrep -qF/self-test, notsx), the self-test emitting one pass/fail like 16i: (1) a non-vacuity self-test (a probe carryingscore?.compositeaccepted, 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_FLOORintest-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:scoreEnvelopecomposescomposite()+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:
normalizeItemfully validates the score (mode + the mode's five dims in [1,10], non-array objects) → structured error intoerrors[]; on the capture pathitemToInput→compositeis unreachable for bad dims; SC2 + SC7 assert no-throw +errors[]routing.itemToInputcalled 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 → urlchain 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 —
extractYamlmis-reads thesummaryif 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);
saveStoreJSON.stringify:95strips nothing; SC4 mirrors the proven R2astore.test:403-476and adds a score-survives-round-trip case. - R7 — editing
trend-spotter.mdtrips the de-niche guard. Mitigated: Section 17 runs in the gate; the addedscore/dimensionsare 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 (notsx) → unconditional;TRENDS_TESTS_FLOORstays inside the deps guard. - R9 — import cycle. Mitigated:
score.tsimports nothing internal today (:1-17, verified leaf). R3a adds three new one-way inbound edges to it —score.ts ← types.ts(TrendScoretype),score.ts ← store.ts(TrendScoretype onTrendInput),score.ts ← item.ts(ScoreMode/DimensionScorestypes +requiredDimensions/scoreEnvelopevalues). The DAG stays acyclic:score.ts (leaf) ← {types, store, item, brief, cli}, sincescore.tsimports 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/cliagainst pre-edit code; Phase B stub-first then value-assertion RED forscore/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 (
itemToInputis public; direct bad-dim calls throw). ✅ Step 4 + R2 + brief §5 reword it path-specific (no throw on the capture path becausenormalizeItemgates 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]
requiredDimensionsorder contract ambiguous (SC1 hard-coded arrays vs membership use). ✅ pinned ordered (Step 2 + SC1 deep-equal the SSOT-order array;score.testpins the order;normalizeItemconsumes 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-471stale + v2 assertions → ✅:403-476+ "flip everyschemaVersionliteral 2→3" note (SC4, Step 1). - [MINOR — brief-reviewer] R1 SSOT-pin cite was the doc-comment (
score.ts:9-13) → ✅ nowscore.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]
normalizeItemnon-array object case understated in the brief → ✅ "non-array" added to bothscoreanddimensionsobject checks (Step 4, brief §3, SC2 array-dims case). - [MINOR — plan-critic] header-chain line-ref
:33-49loose → ✅ 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.