seeded-lead-activity-timelinesrun.md02_define/output/spec.mdThe demo seeder's createLead lost its per-lead logActivity("CREATE", …) call in the merged
storyline work. Only four of the nine pipeline stage cases still log anything, so 49 of the 61
seeded leads open with an empty activity timeline — a presenter clicking into most leads on the
demo tenant shows a dead feed on the workspace, and the customer persona's project audit log is
empty too. The finding was raised at the demo-canned-agents verify gate ("belongs in intake
against that run") and never filed. The demo tenant is the bridge this quarter's
Establish Product-Market Fit with Vendor Partners objective is demonstrated over; a dead feed on
four out of five leads undercuts the story on the surface a prospect clicks first.
The twelve entries that do survive are stamped at wall-clock now, so on a lead whose history
ended 70 days ago — or an engagement that closed 15 months ago — the one entry present contradicts
every other date on the record.
Make the demo seeder write a plausible, backdated timeline for every lead it creates, derived from the status chain it already writes:
deterministic.ts) and covered by a unit suite —
which is the regression guard this defect went without.The count already flows to the admin trigger screen (Activity entries is an existing row on the
seed result panel); once every lead contributes, a future regression shows up there as a collapsed
number rather than silently.
STATUS_CHANGE activity entry at the
same timestamp, so a mid-journey lead's timeline agrees with its status history.CREATE entry dated no later than its first status change,
so it reads as the oldest event in the feed rather than the newest.Math.random and no per-record Date.now() beyond the seeder's existing
relative-date anchor, so two resets of an equivalent tenant produce the same world.counts.activities counts every entry written (one per lead, plus one per status-history
row, plus the narrative entries), and the trigger screen's existing "Activity entries" row
reports that total.ActivityService, the activity model and its indexes, and
every non-demo caller are unchanged.createdAt. Live-pipeline leads keep their wall-clock creation date
(their history chain already reaches back before it); only the activity feed is made coherent.
Changing the lead anchor would move the lead-created and time-to-value analytics windows.demo-data-quality owns that; this run reads the storyline
contract as it stands after saas-vendor-storyline (#818, merged 2026-08-17).workspace.ts reads 12, the customer project feed reads its own page).03_build/output/notes.mdfeat: seeded-lead-activity-timelines — backdated activity timeline on every seeded leadpackages/services/src/db/services/demo-data/activity-timeline.ts (new): the pure
derivation. statusChainTimes(count, endingAt) returns the instants a status chain is
stamped with (two days per step, the last one step before the end), and
timelineFor({ statuses, times, createdAt }) turns a lead's chain into its ordered entries —
a CREATE entry, then one STATUS_CHANGE per step. It reads no clock and holds no database,
which is what makes the regression assertable. It lives beside deterministic.ts for the
same reason that file exists.demo-data/index.ts → recordHistory now takes the timestamps rather than computing its own.
createLead calls statusChainTimes once and hands the result to both recordHistory and
the new logTimeline, so the status history and the activity feed are stamped from a single
source and cannot drift apart. This is the fix for the 49 empty timelines: every lead the
seeder creates now gets its entries, rather than the four stage cases that happened to log
one by hand.demo-data/index.ts → logActivity gained an optional at, written through to the
document's createdAt (the same ...(x ? { createdAt: x } : {}) pattern createLead,
createProposal and createQuote already use). Without it every entry is stamped at the wall
clock, which is why the twelve surviving entries contradicted their leads' dates.demo-data/index.ts → the four hand-written stage entries (BRD_APPROVED,
PROPOSAL_ACCEPTED, INVOICE_RAISED, QUALIFIED_OUT) are now dated a day after the lead
reached its current status, via afterLastStatus and the new lastStatusAt that createLead
returns. They stay the newest event on their lead's feed and never land in the future.demo-data/index.ts → the actiontype id lookup is memoised per seed run. Not cosmetic: the
change takes the seeder from ~12 activity writes to ~470, and findIdByName is a findOne
(create-if-missing) on every call. The map turns ~470 lookups into the ~14 distinct action
names the run actually uses, so the reset — which also runs on a cron — doesn't slow down
because of this PR.demo-data/activity-timeline.test.ts (new): written from the acceptance criteria — the
creation entry leads the feed, one entry per status step, the entries carry the history's own
instants, a revisited status reads as a reopening, a closed engagement's entries stay inside
its delivery span, and the derivation is unchanged by moving the clock.logTimeline is called from createLead
itself, so it covers the live pipeline and the closed tail alike, including the five stage
cases (pending, backlog, delivered, survey, completed) that logged nothing before.STATUS_CHANGE entry at the same timestamp —
both are written from the same times array.CREATE dated no later than the first status change —
timelineFor pulls it one step ahead when the lead's own createdAt is later, which is
the live-pipeline case (created now, history reaching back).endingAt, so afterLastStatus (+1 day) still lands before it.closedAt and the creation entry sits on startedAt, 60–120 days back.Math.random and no Date.now; the only clock read
stays the seeder's existing historyEndingAt ?? new Date() relative anchor.counts.activities counts every entry — the increment sits in logActivity, which every
path goes through; the trigger screen's "Activity entries" row already reads that field.ActivityService, the activity model, its
indexes, or any non-demo caller. The diff is two files in demo-data/ plus the new test.pending one, a mid-journey
in_progress one, and a customer-visible completed one. Before this change most of those
opened empty. Check the dates read as a past, not as "just now".Activity.create honours an explicit createdAt because Mongoose only stamps the field when
it is unset — the same mechanism the seeder already relies on for leads, proposals, quotes and
invoices. If the backdating silently failed, every seeded feed would read "just now" and the
smoke above catches it.in_progress stage files its narrative
entry under the action name INVOICE_RAISED while its message reads "Work in progress —
milestones underway." The two disagree, and the customer feed renders the action name as the
event line with the message beneath. It predates this run and is outside the spec; worth a
tweak lane if it reads badly on the preview.