Commit graph

7 commits

Author SHA1 Message Date
b185db9a12 feat(linkedin-studio): RE-R3b — trend lifecycle (re-score on re-capture · status · seen-log) [skip-docs]
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
2026-06-26 01:08:43 +02:00
e169c78710 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
2026-06-24 14:05:27 +02:00
fa7551070e feat(linkedin-studio): RE-R2b — dated morning-brief artifact + session-start surfacing [skip-docs]
The visible layer of R2. Pure brief.ts: rankForBrief (pillar-overlap -> recency over
the store; publishedAt ?? capturedAt freshness, 7d window; total-order sort), renderBrief
(dated Markdown + hook-surfaceable summary frontmatter), briefSummary (one summary source),
defaultBriefDir (derived from defaultStorePath). CLI `brief` writes
<data>/trends/morning-brief/YYYY-MM-DD.md; session-start surfaces the latest zero-tsx
(latestMorningBrief). Wired into trend-spotter Step 4.6 (scan->capture->brief->surfaced).
No store-schema/scoring change; no scheduler (R3).

25 new trends tests (21 brief.test + 4 cli brief, RED-first) + 3 hook tests (morning-brief
surfacing). trends 104/104 (floor 104), hook-suite 139/139, gate FAIL=0 (ASSERT floor 94,
Section 16i: cli brief-handler + trend-spotter brief-pointer + session-start surfacing
greps), tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 13:12:54 +02:00
7a158030b6 feat(linkedin-studio): RE-R2a — item→store capture bridge + publishedAt persistence (schema v1→v2, lossless migrate) [skip-docs]
Closes the research-engine capture loop RE-R1 deferred:
- itemToInput(item, capturedAt): pure envelope→TrendInput bridge in item.ts —
  injects capturedAt, carries publishedAt verbatim; no id, no re-validate
- publishedAt persisted: TrendRecord/TrendInput gain it; addTrend conditional-spread,
  first-sight kept on re-capture (no back-fill). SCHEMA_VERSION 1→2 with a lossless
  forward migrate-on-load: Math.max(onDisk, current) + numeric-typeof coercion
  (string/NaN/absent → current; non-array trends coercion preserved verbatim)
- `capture` CLI: stdin raw item|batch → normalize → bridge → addTrend → saveStore once;
  tally {added,duplicates,merged,errors} from AddResult; content-invalid → errors[],
  exit 2 only on bad stdin; --json summary
- wiring: trend-spotter.md Step 4.5 N×`add` → one normalizing `capture` batch; README
  add/capture framing corrected; test-runner Section 16h (capture wiring, unconditional)
  + floors bumped (trends 62→79, ASSERT 87→90)

TDD: 17 new tests (12 genuinely-RED logic-RED + 5 regression guards), tsc clean,
gate 105/0/0. No version bump (additive, v0.5.2 dev).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 11:12:50 +02:00
24775f4493 feat(linkedin-studio): RE-R1 — item-schema (B1) + triage-scorer (B2) as tested code behind CLI seam [skip-docs]
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
2026-06-24 10:09:45 +02:00
0907b2b92d feat(linkedin-studio): trend-store staleness nudge — SessionStart B-S3 [skip-docs]
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
2026-06-23 12:23:53 +02:00
be21788321 feat(linkedin-studio): trends store — research-engine inventory (§5 slice 1)
[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>
2026-06-21 19:08:21 +02:00