The lifecycle layer over the trend store: what happens to a trend AFTER first capture.
- re-score on re-capture (last-wins; addTrend duplicate branch, score the one mutable
field; provenance + lifecycle untouched; no false-merge via JSON compare). Reverses
R3a's first-sight D3 — that R3a test reconciled to the new behaviour.
- status new/acted/skipped (effectiveStatus/setStatus + act/skip/reset CLI verbs);
rankForBrief EXCLUDES handled trends (a work queue, not an archive).
- seen-log surfacedCount/lastSurfacedAt (markSurfaced, per-day idempotent); the brief
CLI records surfacing on the store AFTER the pure render, unless --no-mark.
- render: entry id in backticks (copy-paste for act/skip) + · sett Nx prior-day hint.
- schema v3→v4 (additive lossless); the R3a migration block reconciled to the bump,
the new R3b block committed against SCHEMA_VERSION (breaks the reconcile cycle).
score.ts + item.ts untouched (re-score reuses the R3a capture path). RED-first (two
phase: 16 logic-RED + 4 stub-RED). Gate: Section 16k (6 emitters), TRENDS_TESTS_FLOOR
146→171, ASSERT_BASELINE_FLOOR 99→105. trends 171/171, gate 120/0/0, hook suite 139/139.
Plan: docs/research-engine/{brief,plan}-re-r3b.md (light-Voyage hardened @ c40b937).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
Lift the research engine's deterministic core out of agents/trend-spotter.md prose
into pure, tested TypeScript under scripts/trends/, behind a CLI seam the agent calls.
- B1 src/item.ts: TrendItem ingress envelope + normalizeItem/normalizeItems
(required-field validation, topic normalize+dedupe via store's normalizeField,
optional publishedAt ISO-validate). No id (store derives it); no store bridge
(capturedAt injection is R2).
- B2 src/score.ts: per-mode weight consts mirroring the SSOT
(references/trend-scoring-modes.md), composite (weighted sum, [1,10] guard),
band (5-band map + exact SSOT action strings), triage (keep>=threshold, rank desc,
annotate composite+band). Owns ONLY the arithmetic; the five judgment scores stay
model-side.
- CLI normalize/score: JSON payload on STDIN, JSON to stdout (the existing --json
output toggle is untouched); exit 2 on bad invocation, 0 otherwise.
- Wire trend-spotter.md to name 'src/cli.ts score' as the deterministic-step owner
(prose pointer; the agent still supplies the five scores). Domain-general.
- Gate: TRENDS_TESTS_FLOOR 24->62; new unconditional Section 16g (score.ts both-mode
weight-sets + trend-spotter scorer-pointer + non-vacuity self-test);
ASSERT_BASELINE_FLOOR 84->87.
TDD: logic-RED proven (33/34 item+score fail on assertions, not module-not-found),
then GREEN (trends suite 62/62); CLI RED 2/4 -> GREEN 4/4. Full gate 102/0/0.
No store-schema change (SCHEMA_VERSION stays 1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
SessionStart now warns (>=7d, warn-only) when the persistent trend store's
newest capture is stale, firing ONLY when the store already holds captures
(a never-scanned user is never nagged). Neutral wording — "scan for trends"
hits trend-spotter's own trigger; no hardcoded beat (de-niche invariant).
- store.ts: newestCaptureDate() — pure max-capturedAt staleness signal (SSOT)
- cli.ts: status [--json] subcommand (count + newest + daysStale)
- session-start.mjs: trendsNewestCapture() reads trends.json as raw JSON
(no tsx spawn at session start) + the reminder line, beside import-staleness
- tests: +3 store tests (newestCaptureDate) + hook subprocess test (3 cases:
>=7d fires, <7d silent, absent/empty silent + no crash)
- test-runner.sh: trends floor 21->24
Verified: trends 24/24 · all hook tests 131/131 · gate 89/0/0 · real render
confirms "Trend signals are N days old. Scan for trends…".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
[skip-docs] internal plumbing — standalone store, no command/agent/pipeline
surface change until slice 2 wiring (mirrors specifics-bank slice 2). CLAUDE.md
"Telling"/counts untouched; lint stays 84/0/0.
Research-engine §5 (foundation layer) slice 1: the deterministic STORE half of
the persistent trend store — a topic-tagged, provenance-bearing inventory of
trend signals captured over time, so the research engine accumulates HISTORY
instead of starting amnesiac each session. Trend-side twin of the lived-specifics
bank (same store/dedup/query discipline; dedupe key is normalized title+URL, not
free-text content). Generic by architecture: nothing niche-specific lives here —
topics and source are free-form, decided upstream via config/profile.
scripts/trends/ (sibling to specifics-bank, same tsx convention):
- src/types.ts — TrendRecord/TrendStore schema (schemaVersion 1), minimal
generic core: title, url, source, capturedAt, topics[], optional summary
- src/store.ts — pure store: normalizeField, title+url-hash id (= dedupe key),
load/save, addTrend (dedupe + topic union on re-capture; first-sighting
source/capturedAt kept), queryByTopic (overlap-ranked then recency), history
(time-scoped, since/limit)
- src/cli.ts — add / query / list; default store under
${LINKEDIN_STUDIO_DATA:-~/.claude/linkedin-studio}/trends/ so trend history
survives plugin upgrades/reinstalls (M0 data-path seam)
- tests/store.test.ts — 21/21 green; tsc clean
- README + .gitignore for node_modules/build
Capture/scoring agent + MCP-first routing land in slice 2; the CI binding guard
is deferred to wiring, mirroring the specifics-bank timeline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>