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
This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 01:08:43 +02:00
commit b185db9a12
10 changed files with 661 additions and 44 deletions

View file

@ -7,6 +7,7 @@ import {
renderBrief,
briefSummary,
defaultBriefDir,
surfacedIds,
BRIEF_SCHEMA_VERSION,
} from "../src/brief.js";
import type { TrendRecord, TrendStore } from "../src/types.js";
@ -30,6 +31,9 @@ function mkTrend(
source?: string;
summary?: string;
score?: TestScore;
status?: "new" | "acted" | "skipped";
surfacedCount?: number;
lastSurfacedAt?: string;
},
): TrendRecord {
return {
@ -42,6 +46,9 @@ function mkTrend(
topics: p.topics,
...(p.summary !== undefined ? { summary: p.summary } : {}),
...(p.score !== undefined ? { score: p.score } : {}),
...(p.status !== undefined ? { status: p.status } : {}),
...(p.surfacedCount !== undefined ? { surfacedCount: p.surfacedCount } : {}),
...(p.lastSurfacedAt !== undefined ? { lastSurfacedAt: p.lastSurfacedAt } : {}),
};
}
@ -348,3 +355,80 @@ describe("defaultBriefDir", () => {
}
});
});
describe("RE-R3b — exclude acted/skipped (A3)", () => {
const pillars = ["AI", "gov"];
const store = mkStore([
mkTrend({ title: "New top", url: "https://e/n", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23" }),
mkTrend({ title: "Acted top", url: "https://e/a", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", status: "acted" }),
mkTrend({ title: "Skipped single", url: "https://e/s", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", status: "skipped" }),
]);
const r = rankForBrief(store, pillars, TODAY);
test("RED: acted/skipped are dropped from every bucket", () => {
assert.deepEqual(r.topMatches.map((e) => e.trend.title), ["New top"]);
assert.deepEqual(r.singleMatches.map((e) => e.trend.title), []);
assert.deepEqual(r.olderMatched.map((e) => e.trend.title), []);
});
test("RED: totals.trends counts the full inventory (incl. handled); matched is post-filter", () => {
assert.equal(r.totals.trends, 3, "full store count");
assert.equal(r.totals.matched, 1, "only the new record is matched");
});
test("RED: a store whose only matches are handled → no fresh + the empty summary", () => {
const s = mkStore([
mkTrend({ title: "A", url: "https://e/aa", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", status: "acted" }),
mkTrend({ title: "B", url: "https://e/bb", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22", status: "skipped" }),
]);
const rr = rankForBrief(s, pillars, TODAY);
assert.equal(rr.totals.fresh, 0);
assert.match(briefSummary(rr), /Ingen ferske tema-signaler/);
});
});
describe("RE-R3b — render id + surfaced marker + descriptor (D4/D5)", () => {
const pillars = ["AI", "gov"];
test("RED: a top entry carries the id in backticks (copy-paste-ready for act/skip)", () => {
const s = mkStore([mkTrend({ title: "Top", url: "https://e/t", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23" })]);
const md = renderBrief(rankForBrief(s, pillars, TODAY));
assert.ok(md.includes("· `Top|https://e/t`"), "top entry meta line must end with the id in backticks");
});
test("RED: a single-match bullet carries the id in backticks", () => {
const s = mkStore([mkTrend({ title: "Single", url: "https://e/sg", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22" })]);
const md = renderBrief(rankForBrief(s, pillars, TODAY));
assert.ok(md.includes("· `Single|https://e/sg`"), "bullet must end with the id in backticks");
});
test("RED: · sett Nx appears only when surfacedCount >= 2 (prior-day count)", () => {
const s = mkStore([
mkTrend({ title: "Seen", url: "https://e/seen", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", surfacedCount: 3 }),
mkTrend({ title: "Once", url: "https://e/once", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23", surfacedCount: 1 }),
]);
const md = renderBrief(rankForBrief(s, pillars, TODAY));
assert.ok(md.includes("· sett 3x"), "surfacedCount 3 → · sett 3x");
assert.ok(!md.includes("sett 1x"), "surfacedCount 1 → no marker");
});
test("RED: the ranking: descriptor ends with '; excludes acted/skipped'", () => {
const md = renderBrief(rankForBrief(mkStore([]), pillars, TODAY));
assert.match(
md,
/\nranking: composite desc, then pillar-overlap desc, then publishedAt desc \(capturedAt fallback\); freshDays 7; excludes acted\/skipped\n/,
);
});
});
describe("RE-R3b — surfacedIds (D7, Phase B)", () => {
test("RED: surfacedIds = topMatches singleMatches olderMatched.slice(0,5)", () => {
const olders = Array.from({ length: 7 }, (_, i) =>
mkTrend({ title: "O" + i, url: "https://e/o" + i, topics: ["ai", "gov"], publishedAt: "2026-05-01", capturedAt: "2026-05-01" }),
);
const s = mkStore([
mkTrend({ title: "Top", url: "https://e/t", topics: ["ai", "gov"], publishedAt: "2026-06-23", capturedAt: "2026-06-23" }),
mkTrend({ title: "Sg", url: "https://e/sg", topics: ["ai"], publishedAt: "2026-06-22", capturedAt: "2026-06-22" }),
...olders,
]);
const r = rankForBrief(s, ["AI", "gov"], TODAY);
const expected = [...r.topMatches, ...r.singleMatches, ...r.olderMatched.slice(0, 5)].map((e) => e.trend.id);
assert.deepEqual(surfacedIds(r), expected);
assert.equal(surfacedIds(r).length, 1 + 1 + 5, "older capped at 5");
});
});

View file

@ -319,3 +319,123 @@ describe("trends CLI — brief subcommand (RE-R2b / Step 3)", () => {
}
});
});
describe("trends CLI — lifecycle: act/skip/reset + brief surfacing (RE-R3b)", () => {
// One temp dir per fixture holds both the store file and the brief out dir, so the
// real per-user data dir (defaultBriefDir) is never touched.
const fixture = () => {
const dir = mkdtempSync(join(tmpdir(), "trends-r3b-"));
return { dir, store: join(dir, "trends.json"), out: join(dir, "briefs") };
};
const listJson = (store: string): Array<Record<string, any>> =>
JSON.parse(run(["list", "--store", store, "--json"], "").stdout);
const seedScored = (store: string, title: string, url: string, timing = 9): void => {
const batch = JSON.stringify([
{
source: "tavily",
title,
url,
topics: ["ai", "gov"],
publishedAt: "2026-06-23",
score: { mode: "kortform", dimensions: { pillar: 9, audience: 8, timing, angle: 7, authority: 6 } },
},
]);
run(["capture", "--store", store], batch);
};
test("RED: act --id → acted; skip → skipped; reset → new (read back via list --json)", () => {
const { dir, store } = fixture();
try {
seedScored(store, "Lifecycle", "https://e/lc");
const id = listJson(store)[0].id;
assert.equal(run(["act", "--id", id, "--store", store], "").status, 0);
assert.equal(listJson(store)[0].status, "acted");
run(["skip", "--id", id, "--store", store], "");
assert.equal(listJson(store)[0].status, "skipped");
run(["reset", "--id", id, "--store", store], "");
assert.equal(listJson(store)[0].status, "new");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("RED: an unknown id → exit 2 + store unchanged; a missing --id → exit 2", () => {
const { dir, store } = fixture();
try {
seedScored(store, "X", "https://e/x");
const before = readFileSync(store, "utf8");
assert.equal(run(["act", "--id", "nope", "--store", store], "").status, 2);
assert.equal(readFileSync(store, "utf8"), before, "store untouched on a not-found id");
assert.equal(run(["skip", "--store", store], "").status, 2, "missing --id → exit 2");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("RED: brief records surfacing (surfacedCount + lastSurfacedAt + marked); a second same-day run is idempotent", () => {
const { dir, store, out } = fixture();
try {
seedScored(store, "Surf", "https://e/surf");
const o1 = JSON.parse(run(["brief", "--pillars", "ai,gov", "--out", out, "--store", store, "--json"], "").stdout);
assert.equal(o1.marked, 1, "first brief marks 1");
const rec = listJson(store)[0];
assert.equal(rec.surfacedCount, 1);
assert.match(rec.lastSurfacedAt, /^\d{4}-\d{2}-\d{2}$/);
const o2 = JSON.parse(run(["brief", "--pillars", "ai,gov", "--out", out, "--store", store, "--json"], "").stdout);
assert.equal(o2.marked, 0, "second same-day brief is idempotent");
assert.equal(listJson(store)[0].surfacedCount, 1, "count unchanged on the second same-day run");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("RED: brief --no-mark writes no surfacedCount", () => {
const { dir, store, out } = fixture();
try {
seedScored(store, "Dry", "https://e/dry");
const o = JSON.parse(run(["brief", "--pillars", "ai,gov", "--no-mark", "--out", out, "--store", store, "--json"], "").stdout);
assert.equal(o.marked, 0);
assert.equal("surfacedCount" in listJson(store)[0], false, "--no-mark must not write surfacedCount");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("RED: the brief .md omits an acted record", () => {
const { dir, store, out } = fixture();
try {
seedScored(store, "Handled", "https://e/handled");
const id = listJson(store)[0].id;
run(["act", "--id", id, "--store", store], "");
const o = JSON.parse(run(["brief", "--pillars", "ai,gov", "--out", out, "--store", store, "--json"], "").stdout);
const md = readFileSync(o.path, "utf8");
assert.ok(!md.includes("Handled"), "an acted record must not appear in the brief");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("RED: a re-capture with a changed score reports merged:1 + refreshes the composite", () => {
const { dir, store } = fixture();
try {
seedScored(store, "Re", "https://e/re", 9);
const c1 = listJson(store)[0].score.composite;
const batch = JSON.stringify([
{
source: "tavily",
title: "Re",
url: "https://e/re",
topics: ["ai", "gov"],
publishedAt: "2026-06-23",
score: { mode: "kortform", dimensions: { pillar: 9, audience: 8, timing: 2, angle: 7, authority: 6 } },
},
]);
const o = JSON.parse(run(["capture", "--store", store, "--json"], batch).stdout);
assert.equal(o.merged, 1, "a changed score on a duplicate → merged");
const c2 = listJson(store)[0].score.composite;
assert.ok(c2 < c1, "a lower timing → lower composite (re-score last-wins)");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});

View file

@ -14,6 +14,9 @@ import {
queryByTopic,
history,
newestCaptureDate,
effectiveStatus,
setStatus,
markSurfaced,
} from "../src/store.js";
import { SCHEMA_VERSION } from "../src/types.js";
import type { TrendStore } from "../src/types.js";
@ -307,7 +310,7 @@ describe("trends store", () => {
assert.equal("score" in res.store.trends[0], false);
});
test("RED: re-capture keeps the first sighting's score (no overwrite), unions topics", () => {
test("re-capture REFRESHES the score (last-wins, RE-R3b reverses R3a's D3), unions topics", () => {
let store = emptyStore();
store = addTrend(store, {
title: "Same scored trend",
@ -333,7 +336,7 @@ describe("trends store", () => {
});
assert.equal(res2.added, false);
assert.equal(res2.merged, true);
assert.deepEqual(res2.store.trends[0].score, kortScore, "first-sight score kept (D3)");
assert.deepEqual(res2.store.trends[0].score, lowerScore, "score refreshed last-wins (RE-R3b reverses R3a's D3)");
assert.deepEqual([...res2.store.trends[0].topics].sort(), ["a", "b"]);
});
});
@ -567,8 +570,9 @@ describe("trends store", () => {
}
};
// ── genuinely RED: pre-bump SCHEMA_VERSION=2 → loadStore(v2).schemaVersion===2 ≠ 3 ──
test("RED: a v2 store (no score) loads stamped as v3, records intact, no score invented", () => {
// RE-R3a v2→v3 block, reconciled to the v4 bump (RE-R3b): the stamped-version assertions track
// SCHEMA_VERSION (a v2 store now migrates to the current version), the v2 INPUT fixture stays literal.
test("a v2 store (no score) loads stamped as the current version, records intact, no score invented", () => {
const v2 = JSON.stringify({
schemaVersion: 2,
trends: [
@ -585,7 +589,7 @@ describe("trends store", () => {
});
withFixture(v2, (path) => {
const s = loadStore(path);
assert.equal(s.schemaVersion, 3, "v2 store must migrate to v3");
assert.equal(s.schemaVersion, SCHEMA_VERSION, "v2 store must migrate to the current version");
assert.equal(s.trends.length, 1);
assert.equal(s.trends[0].title, "Old dated trend");
assert.equal(s.trends[0].capturedAt, "2026-05-01");
@ -595,15 +599,15 @@ describe("trends store", () => {
});
});
test("RED: round-trip loadStore→saveStore writes schemaVersion:3 to disk", () => {
test("round-trip loadStore→saveStore writes the current schemaVersion to disk", () => {
withFixture(JSON.stringify({ schemaVersion: 2, trends: [] }), (path) => {
saveStore(path, loadStore(path));
const onDisk = JSON.parse(readFileSync(path, "utf8"));
assert.equal(onDisk.schemaVersion, 3);
assert.equal(onDisk.schemaVersion, SCHEMA_VERSION);
});
});
test("RED: a v3 store with score on records loads idempotent", () => {
test("a v3 store with score migrates to the current version, score preserved", () => {
const v3 = JSON.stringify({
schemaVersion: 3,
trends: [
@ -625,7 +629,7 @@ describe("trends store", () => {
});
withFixture(v3, (path) => {
const s = loadStore(path);
assert.equal(s.schemaVersion, 3);
assert.equal(s.schemaVersion, SCHEMA_VERSION);
assert.deepEqual(s.trends[0].score, {
mode: "kortform",
dimensions: { pillar: 9, audience: 8, timing: 9, angle: 7, authority: 6 },
@ -664,4 +668,187 @@ describe("trends store", () => {
});
});
});
// ── RE-R3b: re-score on re-capture (last-wins; A2) ──
describe("addTrend — re-score on re-capture (RE-R3b)", () => {
const mkScore = (composite: number, priority: string, timing = 5) => ({
mode: "kortform",
dimensions: { pillar: 5, audience: 5, timing, angle: 5, authority: 5 },
composite,
priority,
});
const seed = (score?: unknown) => ({
title: "Re-scored trend",
url: "https://example.com/rs",
source: "tavily",
capturedAt: "2026-06-01",
topics: ["ai"],
...(score !== undefined ? { score } : {}),
});
test("RED: a duplicate with a DIFFERENT score replaces the stored score (last-wins), merged:true", () => {
let store = emptyStore();
store = addTrend(store, seed(mkScore(8.1, "Immediate", 9))).store;
const res = addTrend(store, {
...seed(mkScore(4.0, "Medium", 3)),
capturedAt: "2026-06-08",
topics: ["ai", "rag"],
});
assert.equal(res.added, false);
assert.equal(res.merged, true, "a changed score (or new topics) → merged:true");
assert.deepEqual(res.store.trends[0].score, mkScore(4.0, "Medium", 3), "score must be the fresh one");
assert.equal(res.store.trends[0].source, "tavily", "provenance source unchanged");
assert.equal(res.store.trends[0].capturedAt, "2026-06-01", "provenance capturedAt unchanged (first-sight)");
assert.deepEqual([...res.store.trends[0].topics].sort(), ["ai", "rag"], "topics still unioned");
});
test("RED: a re-capture with a BYTE-IDENTICAL score and no new topics → merged:false (no false-merge)", () => {
let store = emptyStore();
store = addTrend(store, seed(mkScore(8.1, "Immediate", 9))).store;
const res = addTrend(store, { ...seed(mkScore(8.1, "Immediate", 9)), capturedAt: "2026-06-09" });
assert.equal(res.added, false);
assert.equal(res.merged, false, "identical score + same topics → not a merge");
assert.deepEqual(res.store.trends[0].score, mkScore(8.1, "Immediate", 9));
});
test("RED: a duplicate with NO score leaves the stored score unchanged", () => {
let store = emptyStore();
store = addTrend(store, seed(mkScore(8.1, "Immediate", 9))).store;
const res = addTrend(store, { ...seed(), capturedAt: "2026-06-10" });
assert.equal(res.added, false);
assert.deepEqual(res.store.trends[0].score, mkScore(8.1, "Immediate", 9), "no input score → keep the stored one");
});
test("RED: re-scoring an ACTED trend updates the score but never resets status/surfacedCount", () => {
let store = emptyStore();
store = addTrend(store, seed(mkScore(8.1, "Immediate", 9))).store;
// Simulate a handled, surfaced record (status/surfacedCount are set by act/markSurfaced, not addTrend).
(store.trends[0] as Record<string, unknown>).status = "acted";
(store.trends[0] as Record<string, unknown>).surfacedCount = 3;
const res = addTrend(store, { ...seed(mkScore(4.0, "Medium", 3)), capturedAt: "2026-06-11" });
assert.deepEqual(res.store.trends[0].score, mkScore(4.0, "Medium", 3), "score refreshed");
assert.equal((res.store.trends[0] as Record<string, unknown>).status, "acted", "status must NOT reset on re-score");
assert.equal((res.store.trends[0] as Record<string, unknown>).surfacedCount, 3, "surfacedCount must NOT change on re-score");
});
});
// ── RE-R3b: schema migration v3→v4 (additive-optional lifecycle fields) ──
describe("schema migration (RE-R3b / lifecycle v3→v4)", () => {
const withFixture = (contents: string, fn: (path: string) => void) => {
const dir = tmp();
const path = join(dir, "trends.json");
try {
writeFileSync(path, contents, "utf8");
fn(path);
} finally {
rmSync(dir, { recursive: true, force: true });
}
};
const scored = {
mode: "kortform",
dimensions: { pillar: 9, audience: 8, timing: 9, angle: 7, authority: 6 },
composite: 8.1,
priority: "Immediate",
};
// ── genuinely RED while SCHEMA_VERSION=3: loadStore(v3).schemaVersion===3 ≠ 4 (hard-4 device) ──
test("RED: a v3 store (no lifecycle fields) loads stamped as v4, records intact, no field invented", () => {
const v3 = JSON.stringify({
schemaVersion: 3,
trends: [
{ id: "x", title: "Scored", url: "https://example.com/s", source: "tavily", capturedAt: "2026-06-01", topics: ["ai"], score: scored },
],
});
withFixture(v3, (path) => {
const s = loadStore(path);
assert.equal(s.schemaVersion, SCHEMA_VERSION, "v3 store must migrate to the current version");
assert.equal(s.trends.length, 1);
assert.deepEqual(s.trends[0].score, scored, "score intact");
assert.equal("status" in s.trends[0], false, "migration must not invent a status");
assert.equal("surfacedCount" in s.trends[0], false, "migration must not invent a surfacedCount");
assert.equal("lastSurfacedAt" in s.trends[0], false, "migration must not invent a lastSurfacedAt");
});
});
test("RED: round-trip loadStore→saveStore writes schemaVersion:4 to disk", () => {
withFixture(JSON.stringify({ schemaVersion: 3, trends: [] }), (path) => {
saveStore(path, loadStore(path));
const onDisk = JSON.parse(readFileSync(path, "utf8"));
assert.equal(onDisk.schemaVersion, SCHEMA_VERSION);
});
});
test("a v4 store with lifecycle fields loads idempotent", () => {
const v4 = JSON.stringify({
schemaVersion: SCHEMA_VERSION,
trends: [
{ id: "y", title: "Handled", url: "https://example.com/h", source: "tavily", capturedAt: "2026-06-01", topics: ["ai"], status: "acted", surfacedCount: 3, lastSurfacedAt: "2026-06-25" },
],
});
withFixture(v4, (path) => {
const s = loadStore(path);
assert.equal(s.schemaVersion, SCHEMA_VERSION);
assert.equal(s.trends[0].status, "acted");
assert.equal(s.trends[0].surfacedCount, 3);
assert.equal(s.trends[0].lastSurfacedAt, "2026-06-25");
});
});
test("a v4 store's lifecycle fields survive load → save → load (field preservation)", () => {
const v4 = JSON.stringify({
schemaVersion: 4,
trends: [
{ id: "z", title: "Persist", url: "https://example.com/p", source: "tavily", capturedAt: "2026-06-01", topics: ["ai"], status: "skipped", surfacedCount: 2, lastSurfacedAt: "2026-06-24" },
],
});
withFixture(v4, (path) => {
const first = loadStore(path);
saveStore(path, first);
const second = loadStore(path);
assert.equal(second.trends[0].status, "skipped");
assert.equal(second.trends[0].surfacedCount, 2);
assert.equal(second.trends[0].lastSurfacedAt, "2026-06-24");
});
});
});
// ── RE-R3b: lifecycle functions (Phase B — RED against the stubs) ──
describe("lifecycle functions: effectiveStatus / setStatus / markSurfaced (RE-R3b)", () => {
const seedStore = () =>
addTrend(
addTrend(emptyStore(), { title: "A", url: "https://e/a", source: "tavily", capturedAt: "2026-06-01", topics: ["ai"] }).store,
{ title: "B", url: "https://e/b", source: "tavily", capturedAt: "2026-06-02", topics: ["gov"] },
).store;
test("RED: effectiveStatus is the stored status, or 'new' when absent", () => {
assert.equal(effectiveStatus({ status: "acted" } as TrendRecord), "acted");
assert.equal(effectiveStatus({} as TrendRecord), "new");
});
test("RED: setStatus sets a present record's status (found:true); an absent id → found:false", () => {
const store = seedStore();
const idA = store.trends[0].id;
const res = setStatus(store, idA, "acted");
assert.equal(res.found, true);
assert.equal(store.trends[0].status, "acted");
assert.equal(setStatus(store, "missing-id", "skipped").found, false, "absent id → found:false (no throw)");
});
test("RED: markSurfaced increments + sets lastSurfacedAt; per-day idempotent; later day re-increments", () => {
const store = seedStore();
const idA = store.trends[0].id;
const r1 = markSurfaced(store, [idA], "2026-06-25");
assert.equal(r1.marked, 1);
assert.equal(store.trends[0].surfacedCount, 1);
assert.equal(store.trends[0].lastSurfacedAt, "2026-06-25");
assert.equal("surfacedCount" in store.trends[1], false, "an id not in the set is untouched");
const r2 = markSurfaced(store, [idA], "2026-06-25");
assert.equal(r2.marked, 0, "same-day re-mark is idempotent");
assert.equal(store.trends[0].surfacedCount, 1);
const r3 = markSurfaced(store, [idA], "2026-06-26");
assert.equal(r3.marked, 1, "a later day increments again");
assert.equal(store.trends[0].surfacedCount, 2);
assert.equal(store.trends[0].lastSurfacedAt, "2026-06-26");
});
});
});