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:
parent
c40b937856
commit
b185db9a12
10 changed files with 661 additions and 44 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue