feat(linkedin-studio): RE-R3a — persist relevance score on the store record + rank the morning brief on it [skip-docs]

R3 slice 1 (research-deepening). Stop discarding the relevance judgment the
trend-spotter already computes: persist a 4-field TrendScore {mode, dimensions,
composite, priority} on TrendRecord (schema v2->v3, additive lossless migrate),
computed by the existing score.ts composite()+band() (one owner, no new arithmetic),
threaded item->store; then rankForBrief sorts each bucket composite-first (sentinel
-1 for unscored) and renderBrief surfaces "· <priority> (<mode>)" per body entry
(briefSummary shows the band only). First-sight only; mode-blind ranking with the mode
shown so the operator can disambiguate instruments.

- score.ts: TrendScore + requiredDimensions(mode) (ordered) + scoreEnvelope (composes
  composite+band; throws on bad dim by contract)
- types.ts: SCHEMA_VERSION 2->3; TrendRecord.score?
- store.ts: TrendInput.score?; addTrend persists first-sight (duplicate keeps it);
  migrate comment v1->v2->v3 (logic unchanged, JSON.stringify preserves the field)
- item.ts: TrendItem.score?; normalizeItem validates (non-array score/dimensions + the
  mode's five dims in [1,10]) -> structured error never throw, carries validated dims;
  itemToInput -> scoreEnvelope (no throw on the capture path; direct call throws by contract)
- brief.ts: composite-primary comparator; band+mode render; exact ranking: descriptor
- cli.ts: capture persists score via itemToInput (doc-only); add/score paths unchanged
- agents/trend-spotter.md Step 4.5: capture batch carries the Step-2 dimensions
- gate: TRENDS_TESTS_FLOOR 104->146; new unconditional Section 16j; ASSERT floor 94->99

Tests: trends 146/146 (RED two-phase: logic-RED store/brief/cli; stub-first then
assertion-RED score/item). Gate green (Passed 114 / Failed 0; 113 checks >= 99).
Hook suite 139/139 untouched. Counts 27/19/29 unchanged. No new source file/agent/command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
This commit is contained in:
Kjell Tore Guttormsen 2026-06-24 14:05:27 +02:00
commit e169c78710
14 changed files with 829 additions and 40 deletions

View file

@ -285,21 +285,36 @@ For every trend that cleared the relevance filter (Step 2) — not only the ones
final digest — fold it into the persistent trend store, so the next session reasons over it
instead of re-discovering it. Build ONE raw-item batch (the same trends you just scored) and pipe
it through `capture`: it normalizes each item, dedupes on normalized title+URL, unions topics on
re-capture (so re-capturing an existing trend just enriches the tags), and persists the source's
`publishedAt` for later freshness ranking — one call, not one per trend:
re-capture (so re-capturing an existing trend just enriches the tags), persists the source's
`publishedAt` for later freshness ranking, and — when you carry the score (below) — persists the
relevance assessment so the morning brief ranks on it — one call, not one per trend:
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/trends" && \
echo '[
{"source":"<tavily|websearch|manual|…>","title":"<verbatim headline>","url":"<source url>",
"topics":["<pillar-tag1>","<pillar-tag2>"],"publishedAt":"<YYYY-MM-DD if known>",
"summary":"<one-line what-happened>"}
"summary":"<one-line what-happened>",
"score":{"mode":"kortform","dimensions":{"pillar":N,"audience":N,"timing":N,"angle":N,"authority":N}}}
]' | node --import tsx src/cli.ts capture
```
`source` is the tool you actually fetched with (**Research Routing**); `publishedAt` is the
source's own publish date — omit the key when unknown (the store's `capturedAt` is set
automatically and stays distinct from it). One `capture` call folds the whole batch and reports
automatically and stays distinct from it).
**Carry the Step-2 scores — do not discard them.** You already scored each candidate's five
dimensions 110 in **Relevance Scoring** (Step 2); fold those same numbers into the capture batch
as the item's `"score"`, so the store persists the relevance assessment and the morning brief
ranks on its composite (the store computes the composite + band itself — supply only the judgment).
Use `"mode":"kortform"` by default; use `"mode":"long-form"` with the long-form dimension names
(`pillar`, `depth`, `angle`, `authority`, `currency`) when the caller is producing a chronicle /
newsletter / series edition (e.g. invoked from `/linkedin:newsletter`). The `"dimensions"` keys are
the rubric's, the `"topics"` are the user's pillars — nothing vendor- or sector-specific is baked
in. Omit the `"score"` key when you genuinely did not score an item; an out-of-range or malformed
score is reported in `errors[]` (the valid items still persist) and never crashes the run.
One `capture` call folds the whole batch and reports
`{added, merged, duplicates, errors}`; content-invalid items land in `errors[]`, never failing the
run. Skip this step silently if the store has no deps installed (an adopter without the trends
store) — the digest still compiles, just without persistence.