Skip to Content

← All archived runs

Run: seeded-lead-activity-timelines

run.md

Run: seeded-lead-activity-timelines

  • branch: claude/seeded-lead-activity-timelines-1a85pz
  • pr: #853

02_define/output/spec.md

Spec: Restore activity timelines on seeded demo leads

  • slug: seeded-lead-activity-timelines
  • personas: Admin, CSM, SDM, Customer
  • touches: packages/services/src/db/services/demo-data
  • complexity: standard

Problem

The 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.

Proposed change

Make the demo seeder write a plausible, backdated timeline for every lead it creates, derived from the status chain it already writes:

  • Every seeded lead gets a creation entry, dated on its own timeline and never later than its first status change.
  • Every status-history row the seeder writes gets a matching activity entry at the same timestamp, so the workspace timeline and the status history are the same story told twice rather than two stories that disagree.
  • The four existing narrative entries (requirements approved, proposal accepted, work in progress, qualified out) are dated on the lead's timeline instead of the wall clock, so they sit at the top of the feed only for leads whose history genuinely ends now.
  • The entry set for a lead becomes a pure function of its status chain and its two date anchors, living beside the seeder's other pure helpers (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.

Acceptance criteria

  • Every seeded lead — all of the live pipeline and the whole closed tail — carries at least one activity entry after a fresh reset; no lead's workspace activity section is empty.
  • Each lead's status-history rows each have a matching STATUS_CHANGE activity entry at the same timestamp, so a mid-journey lead's timeline agrees with its status history.
  • Each lead's creation entry is a CREATE entry dated no later than its first status change, so it reads as the oldest event in the feed rather than the newest.
  • The four existing narrative entries are backdated onto the lead's own timeline; no seeded activity entry is dated in the future.
  • The closed tail's entries fall inside each engagement's own delivery span (between its seeded creation date and its close date), not at the wall clock.
  • Timelines are deterministic: the entry set is derived from the status chain and date anchors with no 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.
  • A unit suite covers the derivation under fake timers — entry count, ordering, the creation-entry rule, and one-entry-per-status — so a dropped call fails a test next time.
  • 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.
  • Non-demo paths are untouched: ActivityService, the activity model and its indexes, and every non-demo caller are unchanged.

Out of scope

  • Re-dating the leads' own 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.
  • New storyline content or coverage — demo-data-quality owns that; this run reads the storyline contract as it stands after saas-vendor-storyline (#818, merged 2026-08-17).
  • Activity backfill for real (non-demo) tenants.
  • Any change to the workspace activity UI, the customer-visible filter, or the pagination limits the two consumers pass (workspace.ts reads 12, the customer project feed reads its own page).

Open questions

  • none

03_build/output/notes.md

Build notes: seeded-lead-activity-timelines

  • commits: feat: seeded-lead-activity-timelines — backdated activity timeline on every seeded lead

What changed

  • packages/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.tsrecordHistory 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.tslogActivity 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.

Acceptance criteria status

  • Every seeded lead carries at least one entry — 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-history rows each have a matching STATUS_CHANGE entry at the same timestamp — both are written from the same times array.
  • The creation entry is a 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).
  • The four narrative entries are backdated and nothing is dated in the future — the last status sits two days before endingAt, so afterLastStatus (+1 day) still lands before it.
  • The closed tail's entries fall inside each engagement's delivery span — the chain occupies the 16 days before closedAt and the creation entry sits on startedAt, 60–120 days back.
  • Deterministic — the new module has no Math.random and no Date.now; the only clock read stays the seeder's existing historyEndingAt ?? new Date() relative anchor.
  • A unit suite covers the derivation, with a frozen clock for the live-pipeline case.
  • 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.
  • Non-demo paths untouched — no change to ActivityService, the activity model, its indexes, or any non-demo caller. The diff is two files in demo-data/ plus the new test.

Notes for Verify

  • The DoD smoke is a demo reset. Reset the demo tenant from the admin demo-data page on the preview, then open a lead in each of a few stages — a 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".
  • Watch the "Activity entries" count on the result panel. It should read in the hundreds (roughly one per lead plus one per status step, ~470 for the current storyline), not 12. That number is the regression signal for next time.
  • 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.
  • Pre-existing, deliberately not fixed here: the 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.
  • The seeder is not unit-testable (it needs Mongo, and only the unit tier is configured), so the new suite covers the extracted derivation and the smoke pass covers the writing.