Skip to Content

← All archived runs

Run: demo-seed-storyline

run.md

Run: demo-seed-storyline

  • branch: claude/demo-seed-storyline-pipeline-41es39
  • pr: #795

03_define/output/spec.md

Spec: Demo seed storyline — a client-credible world, every headline surface populated

  • slug: demo-seed-storyline
  • personas: Admin, CSM, SDM, Expert, Vendor, Customer
  • touches: packages/services/src/db/services/demo-data, apps/docs/app/technical/demo-environment
  • complexity: complex
  • demo: none

Problem

demo-tenant-foundation stood up the flagged demo tenant and ran the first seed, so the platform now renders — but not as a story a prospect would believe. Every field is drawn at random from a pool (pick/randInt, demo-data/index.ts:343–361): a lead's title, description, requirements, catalogue path and budget have nothing to do with each other or with the account they sit on, and two runs never produce the same world. Worse, the world has no past. Every record is written at Date.now() (only dueDate is offset), and the tenant has exactly one customer account, so the vendor analytics — which window on createdAt/paid dates and compare against the prior window — have one bucket, one account and nothing to trend. And the generator imports no CSAT model at all (the docs page's claim that demo data includes CSAT is drift), so every CSAT-driven read returns empty: the vendor CSAT, health, risk, retention and customers pages, plus sdm-coverage, sdm-strength, sdm-market-signals, expert-workbench and the matching quality scores.

A walkthrough that dies on an empty analytics page is the failure mode this whole batch exists to remove. This advances Refine the bridge / Q2-2026 Objective 1 — Establish Product-Market Fit with Vendor Partners: the demo is the product, and the product has to look like a business.

Proposed change

A storyline pass over the demo-data generator, in five functional pieces.

1. An authored, deterministic world

Replace random selection with a fixed authored dataset — the tables in Storyline content below are the source of truth. Every engagement's account, title, description, requirements, catalogue path, value and dates are authored, not drawn. Math.random() leaves the generator entirely; the only non-determinism left is Mongo's ObjectIds and the wall-clock the relative dates hang off. This is what makes demo-reset-ops able to reproduce the approved storyline rather than a fresh roll of the dice.

2. Ten customer accounts and an expert bench

The generator reads its users from the tenant and today finds one of each, so "top customers", per-account health and retention cohorts are single-row. It gains one new responsibility: upsert-by-name creation of the nine non-persona customer company records and the five non-persona expert records (the same idempotent pattern the tiered catalogue already uses), so a re-run reports 0 created rather than duplicating the world.

The six Clerk-provisioned persona users are not created or renamed by the seeder — a rename would be overwritten by tenantService.upsertFromClerk on the next sign-in. Their storyline identities are set in Clerk as a runbook step, and the seeder matches them by email and weaves them into the story: demo+customer@apoyar.eu is Anneke Vos at Meridian Foods Group (the anchor account), demo+expert@apoyar.eu is Ivo Marchetti, demo+csm@apoyar.eu is Sofia Almeida, demo+sdm@apoyar.eu is Ben Hartley, demo+vendor@apoyar.eu is Calderon Technology Partners, demo+admin@apoyar.eu is Alex Rendell.

3. A backdated history

Keep the 25-lead, nine-stage current pipeline (STAGE_PLAN is unchanged in shape) and add a closed-engagement tail of 36 engagements spread across the last 18 months, denser in the recent months so the default last-30-days window is populated and has a prior window to compare against. Each tail engagement carries its lead, status history, quote, milestones, paid invoice, expert rating and CSAT, all dated to its close month — history rows and money dates included, not just the lead. Backdating is relative (months-ago offsets), so the world stays current on every reseed.

4. CSAT records

Import the CSAT model and write a record for every engagement that reached survey_sent or beyond — the 2 survey and 2 completed leads in the current pipeline, and all 36 tail engagements bar the four deliberate exceptions that carry paid revenue and no response (so revenueAwaitingCSAT is a real number, not zero). Scores are authored per engagement on the model's 1–10 scale, with the sub-scores (communicationEffectiveness, timelineAdherence, valueForMoney, technicalExpertise, …) varying around the overall rather than duplicating it. The unique index is {tenantId, lead, customer} with isDeleted: false, so one record per engagement.

5. Audit every headline surface, then re-seed production

Walk the 11 vendor deep-dives (/vendor/{revenue,csat,renewals,retention,delivery,metrics,funnel, risk,pipeline,customers,health}), the vendor dashboard, the six persona dashboards (/{admin,csm,sdm,expert,vendor,customer}/dashboard) and the project workspace on the seeded tenant, at both the default window and last-12-months. Record every surface in a fill-or-flag table in the PR. A surface that renders empty for want of data is filled here; one that renders empty for want of product code is flagged, and gets an intake stub in pipeline/intake/demo-tenant/ rather than being built in this run. Then re-run the seed against the demo tenant so the signed-off storyline is live.

Storyline content

Fictional throughout — no real prospect or client names, ever. Paul reviews and signs off these tables; the sign-off is recorded on the PR before the Spec-approved tick.

The world

Calderon Technology Partners is a Benelux/UK technology services vendor. It sells delivery capacity it does not itself hold, and uses Sustentus as the bridge: customer demand in, matched experts out, delivery tracked to invoice. Sofia Almeida (CSM) owns the customer relationships, Ben Hartley (SDM) owns delivery, Alex Rendell administers the tenant.

Accounts

Account Sector Country Primary contact Role
Meridian Foods Group Food manufacturing NL Anneke Vos Operations Director
Aldercroft Insurance Insurance UK Ruth Delaney Head of Digital
Volta Mobility EV charging DE Jonas Brandt CTO
Harbourline Logistics Freight & logistics BE Pieter Claes IT Manager
Kestrel Energy Renewables NL Lieke de Wit Programme Manager
Orbis Pharma Services Pharma CRO BE Camille Renard Head of Systems
Bramble & Co Retail Specialty retail UK Marcus Ainsley Digital Director
Fennfield Health Private healthcare IE Siobhan Kerr CIO
Saltmarsh Utilities Water utility UK Dev Raghunathan Head of IT
Tanner Rowe Legal Legal services UK Elaine Tanner Managing Partner

Meridian Foods Group is the anchor: it is the signable customer persona, the largest account by revenue, and appears in the current pipeline, the tail and the in-flight delivery work, so the customer dashboard and the vendor dashboard show two views of one relationship.

Expert bench

Expert Specialism
Ivo Marchetti ERP & integration lead (persona)
Priya Raman Data platform & analytics
Tomas Lindqvist Security & compliance (SOC 2, ISO)
Grace Obi Accessibility & front-end
Daniel Fournier Commerce & payments
Marta Nowak Identity & access management

Current pipeline — 25 engagements across the nine stages

STAGE_PLAN counts are unchanged (3/3/3/3/4/3/2/2/2). Each slot is now a named engagement.

Stage Account Engagement Value
pending Meridian Foods Group Warehouse management system replacement 120k
pending Volta Mobility Charge-point telemetry data platform 95k
pending Tanner Rowe Legal Matter-management system migration 48k
backlog Harbourline Logistics Freight visibility portal 78k
backlog Bramble & Co Retail Store stock-accuracy programme 64k
backlog Kestrel Energy Asset performance reporting 71k
bidding Aldercroft Insurance Claims intake automation 105k
bidding Orbis Pharma Services GxP document workflow rebuild 88k
bidding Saltmarsh Utilities Field-engineer mobile app 67k
quoting Fennfield Health Patient portal accessibility remediation 52k
quoting Meridian Foods Group Supplier onboarding integration 84k
quoting Volta Mobility Payments & roaming settlement integration 97k
in_progress Meridian Foods Group ERP migration to cloud — phase 2 142k
in_progress Aldercroft Insurance Policy data warehouse build 118k
in_progress Harbourline Logistics Customs API integration 73k
in_progress Kestrel Energy SCADA data pipeline modernisation 91k
delivered Bramble & Co Retail Headless commerce replatform 109k
delivered Orbis Pharma Services Identity & access management rollout 86k
delivered Saltmarsh Utilities Billing reconciliation automation 63k
survey Fennfield Health Clinical scheduling integration 57k
survey Volta Mobility Driver app accessibility audit 34k
completed Meridian Foods Group ERP migration to cloud — phase 1 128k
completed Tanner Rowe Legal Document e-signature rollout 42k
qualified_out Bramble & Co Retail In-store AR pilot
qualified_out Saltmarsh Utilities Drone survey trial

The two qualified_out engagements carry their reason in the lead: no budget in the current year (Bramble) and outside the service catalogue (Saltmarsh).

Closed-engagement tail — 36 engagements, last 18 months

Closed is a months-ago offset from the seed run, so the history stays current on every reseed. CSAT is the authored overall score on the model's 1–10 scale; means paid with no response yet (this is what makes revenueAwaitingCSAT non-zero).

# Account Engagement Closed Value CSAT
1 Meridian Foods Group Supplier EDI onboarding wave 3 M-1 96k 9
2 Aldercroft Insurance Claims dashboard rollout M-1 84k 8
3 Harbourline Logistics Port terminal API connector M-1 72k 9
4 Saltmarsh Utilities Meter-read exception handling M-1 58k 6
5 Volta Mobility Roaming settlement reconciliation M-2 88k 9
6 Kestrel Energy Turbine telemetry ingestion M-2 76k 8
7 Bramble & Co Retail Checkout performance hardening M-2 62k
8 Fennfield Health Appointment reminder service M-2 39k 9
9 Orbis Pharma Services eTMF integration M-3 81k 9
10 Meridian Foods Group Cold-chain sensor feed M-3 54k 8
11 Saltmarsh Utilities Outage notification service M-3 47k 7
12 Aldercroft Insurance Broker portal SSO M-4 69k 8
13 Tanner Rowe Legal Client intake automation M-4 44k 7
14 Harbourline Logistics Customs declaration validator M-4 58k 9
15 Volta Mobility Tariff engine rebuild M-5 93k 9
16 Bramble & Co Retail Loyalty data migration M-5 51k 8
17 Kestrel Energy Grid-export reporting M-6 67k 8
18 Fennfield Health Referral routing rules M-6 42k 9
19 Meridian Foods Group Production planning integration M-7 88k 9
20 Orbis Pharma Services Lab result ingestion M-7 63k 8
21 Saltmarsh Utilities Asset register cleanse M-8 49k 6
22 Aldercroft Insurance Fraud-signal enrichment M-8 77k 8
23 Harbourline Logistics Fleet telematics dashboard M-9 61k 9
24 Tanner Rowe Legal Matter billing exports M-9 38k 7
25 Volta Mobility Charge-point provisioning API M-10 84k 9
26 Bramble & Co Retail Product data syndication M-10 56k
27 Kestrel Energy Maintenance scheduling M-11 58k 8
28 Meridian Foods Group Warehouse label printing M-11 41k 9
29 Fennfield Health Consent capture rebuild M-12 47k 8
30 Orbis Pharma Services Audit trail hardening M-12 72k 9
31 Aldercroft Insurance Legacy claims decommissioning M-13 66k 7
32 Harbourline Logistics Depot scheduling pilot M-14 35k 8
33 Meridian Foods Group Recipe management migration M-15 59k 8
34 Saltmarsh Utilities Field app phase 1 M-16 44k
35 Volta Mobility Site survey portal M-17 48k 9
36 Tanner Rowe Legal Knowledge base rebuild M-18 31k

The story the numbers tell

The tail totals ≈2.2M closed over 18 months, and it is authored to make three narrative beats visible on the analytics pages rather than leaving them flat:

  • Growth. The last 30 days close 310k against 265k in the prior 30 — the "vs prior window" deltas on revenue, funnel and delivery read positive.
  • A satisfaction problem worth talking about. Saltmarsh Utilities scores 6, 7 and 6 — averaging 6.3/10 (3.2 normalised), under the 4.0 LOW_CSAT_CUTOFF — so it surfaces as a genuine "Failing CSAT" row on the risk and health pages instead of a table of green.
  • A churn risk. Tanner Rowe Legal declines across the tail (31k → 38k → 44k) and has closed nothing in four months, so it lands on renewals and revenue-at-risk as an account to save.

Acceptance criteria

  • The generator contains no Math.random(): two seed runs against an equivalent empty tenant produce identical accounts, engagement titles, descriptions, catalogue paths, values and CSAT scores, differing only in ObjectIds and the wall-clock the relative dates hang off.
  • Every engagement in the current-pipeline table and the closed-engagement tail exists on the demo tenant with the account, title, value and CSAT score this spec gives it.
  • The seeder upserts the nine non-persona customer company records and the five non-persona expert records by name — a second run against the same tenant creates zero of them.
  • The six Clerk persona users are matched by email and carry their storyline engagements; the seeder does not create or rename them, and their identities survive a re-sign-in.
  • CSAT records exist for every engagement past survey_sent except the four authored exceptions, one per lead, with sub-scores that vary around the overall.
  • vendorService's CSAT-driven reads return non-zero on the demo tenant: avgCSAT, revenueWeightedCSAT and revenueAwaitingCSAT are all non-zero, and at least one account is flagged Failing CSAT.
  • All 11 vendor deep-dive pages and the vendor dashboard render populated on the demo tenant at both the default window and last-12-months — no empty state, no all-zero panel, and the prior-window deltas are non-zero.
  • Each of the six persona dashboards shows a populated slice of the same world: the same account names, the same people, and figures that reconcile between views.
  • The PR carries a fill-or-flag table covering the 11 deep-dives, the vendor dashboard, the six persona dashboards and the project workspace; every flagged surface has a matching stub in pipeline/intake/demo-tenant/.
  • The generator still writes only through models — no notifications, no emails, no external calls — and populateDemoLeads still appends rather than clearing.
  • Paul's sign-off on the storyline tables is recorded on the PR.
  • The seed has been re-run against the demo tenant, so the signed-off storyline is live.
  • The provisioning runbook records the six persona identities so a rebuild reproduces them.

Out of scope

  • Purge and reset — the storyline is reproducible here; building purge-and-reseed is demo-reset-ops. A consequence to accept meanwhile: because seeding stays append-only, a second run against an already-seeded tenant duplicates the engagement graph (the accounts and catalogue upsert, the leads do not). Until demo-reset-ops lands, the demo tenant is seeded once.
  • New product features or pages — surfaces that render empty for want of product code are flagged into intake, not built.
  • Any change to the analytics services themselvesvendorService, sdm-* and expert-workbench are read as-is; if a page stays empty with correct data, that is a flag.
  • Behaviour change for non-demo tenants — nothing here is gated on isDemo, and nothing changes for a tenant that seeds today.
  • The view-as switcher (demo-presenter-switching), canned LLM mode (demo-canned-agents), scope visuals (demo-scope-visuals).
  • Real prospect or client data — fictional only, ever.
  • The "generate example leads" narrative screen (report F) — out of scope for the whole batch.

Open questions

  • Non-blocking: apps/docs/app/technical/demo-environment/page.mdx carries two pieces of drift this run makes visible — it claims seeded data includes CSAT (true only after this run), and its Login Credentials section lists demo+customer1..10@apoyar.eu accounts that the foundation runbook never provisioned and this run does not need (the nine extra accounts are company records, not sign-ins). Ship's docs-sync corrects both in this PR.
  • Non-blocking, and not this run's to fix: the same page commits plaintext passwords for the demo logins and for the shared demo@apoyar.eu inbox. Worth a separate /pipeline bug — flagged, not touched here.

04_build/output/notes.md

Build notes: demo-seed-storyline

  • commits: 4e4c72a deterministic swappable storyline + CSAT + backdated tail; <runbook> persona identities in the provisioning runbook
  • demo: none (no Design run for this batch — the cut was report-based)

What changed

  • packages/services/src/db/services/demo-data/storyline/ (new): the swap point. types.ts is the contract, default.ts is the authored Calderon world from the spec's tables, index.ts exports the active storyline, default.test.ts asserts its invariants.
  • packages/services/src/db/services/demo-data/index.ts: the generator keeps the mechanics and holds no content. Math.random() is gone entirely — variation now comes from an FNV-1a hash of each record's own identity (spread/at/slice), so the same storyline produces the same world every run. Adds CSAT writes, the backdated closed-engagement tail, and upsert-by-email creation of the storyline's customers and experts. populateDemoLeads takes an optional storyline argument, defaulting to the active one.
  • apps/web/.../demo-data-button.tsx: the confirm dialog claimed "It does not create any users", which this change makes false — reworded, and the summary gained CSAT responses, closed engagements, and customers/experts created.
  • apps/docs/.../demo-environment/page.mdx: the runbook's persona step now carries the six storyline identities as a table, with the reason they are set in Clerk rather than by the seeder (upsertFromClerk would undo a rename on next sign-in).

Swappability (your build-time request)

The storyline is a data module behind a type, not content threaded through the generator. To change the demo world: author a module satisfying Storyline and re-point activeStoryline in storyline/index.ts — or pass one to populateDemoLeads(tenantId, createdBy, storyline) for a one-off. The generator has no company names, people, titles, values or scores in it, so a swap touches no logic. demo-reset-ops inherits this: it reseeds whatever storyline is active.

Design decisions worth reviewing

  • Determinism by hash, not by seeded PRNG. A PRNG would make every value depend on call order, so inserting one engagement would reshuffle the whole world. Hashing each record's own identity keeps a record's values stable no matter what else changes around it.
  • Persona users are matched, never written. resolveUser throws if a storyline persona is missing rather than inventing one — a missing persona is a provisioning error, and inventing a user would produce a half-anonymous world that looks seeded but cannot be signed into.
  • The one write against a persona user is filling vendor when it is unset, because the vendor analytics attribute revenue through it and Clerk provisioning does not set it. It never overwrites an existing value.
  • Backdating works because Mongoose's timestamps only sets createdAt when unset, so an explicit createdAt survives. Verified against the installed Mongoose 8 source (lib/helpers/timestamps/setDocumentTimestamps.js: the guard is !doc.$__getValue(createdAt)), not assumed — the whole tail depends on it. The same function seeds updatedAt from createdAt on insert, so backdated rows are consistent on both fields.

Acceptance criteria status

  • No Math.random() in the generator; two runs of a storyline produce identical content — grep over demo-data/** returns only the comment explaining the change.
  • Every engagement exists on the demo tenant with its authored account, title, value and CSAT score — the generator writes them, but the criterion asserts tenant state, so it needs the seed run.
  • Nine customer records and five expert records upsert by email — resolveUser finds before it creates, so a second run reports 0 created.
  • The six persona users are matched by email; the seeder neither creates nor renames them.
  • CSAT records written for every engagement past survey_sent except the storyline's four authored exceptions, one per lead, sub-scores varying ±1 around the overall.
  • The generator still writes only through models — no notification, email or external call added — and populateDemoLeads still appends.
  • The provisioning runbook records the six persona identities.
  • vendorService's CSAT-driven reads return non-zero on the demo tenant — needs the seed run; the data that feeds them is written, but the assertion is against a live tenant.
  • All 11 vendor deep-dives + the vendor dashboard render populated — same: needs the seed run against the demo tenant.
  • Six persona dashboards show a coherent slice — same.
  • The fill-or-flag table — cannot be produced from source alone; it is an observation of the seeded tenant. Static analysis below is the head start, not the deliverable.
  • Paul's sign-off recorded on the PR — his call, not mine.
  • The seed re-run against the demo tenant — an ops step against production, outside what Build does from a branch.

Six criteria are open and all six need the same thing: the seed run against the demo tenant, and then someone looking at it. That is Verify's DoD smoke pass plus the ops half of the run — the same shape demo-tenant-foundation had. Nothing is blocked on code.

Static analysis — what now feeds each headline surface

Read from source, not from a running tenant. Every row is a claim to check during Verify, not a result.

Surface Reads Now fed by
/vendor/dashboard envelope, funnel, top customers, health, TTV, cohorts tail leads + paid invoices + CSAT across 18 months
/vendor/revenue getRevenueRetention, getRevenueFlow backdated paid invoices
/vendor/csat csatByRevenue off the envelope new CSAT records
/vendor/health getCustomerHealth CSAT + per-account paid revenue
/vendor/risk at-risk rows, Failing CSAT flag Saltmarsh's 6/7/6 (3.2 normalised, under 4.0)
/vendor/retention getRevenueRetention, getRetentionCohorts repeat accounts across consecutive windows
/vendor/renewals declining/lapsed accounts Tanner Rowe's decline, nothing closed since M-4
/vendor/customers per-customer aggregation 10 accounts with varied revenue
/vendor/funnel windowed leads incl. rejected 25 live + 36 tail leads, spread by createdAt
/vendor/delivery intake→delivered timings tail spans (60–120 days, deterministic)
/vendor/pipeline open pipeline the 25 live engagements
/vendor/metrics metricDictionaryService.grid() not seed-driven — renders regardless; expected to be unaffected

Notes for Verify

  • Two ACs assert things only a seeded tenant can show. Don't tick them off the diff.
  • The tail is ~36 × (lead + history + proposal + quote + 4 milestones + 4 invoices + messages + rating + CSAT). With the 25 live engagements that is a substantially longer seed run than before — worth timing when it is run, since the admin action is a synchronous server action and could hit a platform timeout. If it does, that is a real finding, not a flake.
  • Re-running still appends, so a second run duplicates the engagement graph (accounts and catalogue upsert; leads do not). Spec accepts this until demo-reset-ops; the demo tenant should be seeded once.
  • default.test.ts asserts the storyline's referential integrity and its three narrative beats. It does not exercise the generator — that needs the integration tier, which the config does not have (CONVENTIONS.md → Testing: don't improvise a tier).
  • The docs page's Login Credentials section still lists demo+customer1..10@apoyar.eu accounts that do not exist, and commits plaintext passwords including the shared inbox. The spec routes the first to Ship's docs-sync; the second is flagged for a separate /pipeline bug.

Context budget: within band. One extra read outside the Inputs table — the vendor analytics pages and vendorService — to establish which surfaces the CSAT gap actually empties, since the spec's claim needed confirming against source before building to it.

05_verify/output/verify.md

Verify: demo-seed-storyline

  • production-readiness: run — database trigger matched (the seeder writes user, lead and CSAT documents). Verdict: safe to merge, not safe to run blind. 6 findings; 4 fixed on branch, 1 carried into the ops runbook, 1 pre-existing → intake.
  • code-review: high effort (spec complexity: complex; the CI Claude review is not enabled — ENABLE_CLAUDE_REVIEW is unset, so its check run reports skipped). 4 findings, all 4 fixed on branch. One was a release-blocking defect in this run's own diff.
  • security-review: run — the diff creates user records and (after the soft-delete fix) can revive archived ones, which is enough to trigger it. No HIGH or MEDIUM findings.
  • playwright: TODO — manual DoD smoke performed instead.

What the reviews caught, and what changed

The code review found the defect that mattered most, and it was mine: the backdated tail landed a full window early. monthsAgo(1, …) resolved to now − 30d − [0..27]d, so every month-1 engagement fell outside the last-30-days window the vendor analytics default to. The current window would have read empty and the documented positive deltas would have rendered negative — precisely the failure the tail exists to prevent, in the feature whose entire purpose is that the windows are populated. Nothing in the storyline data could have fixed it, and no test would have caught it, because the semantics were never pinned.

Fixed in ecb410e: month n now spans days [(n−1)×30, n×30), so month 1 is the current window. The date helpers moved to demo-data/deterministic.ts — a pure module with no database imports — and deterministic.test.ts now pins the window semantics, the no-future-dates property and the month ordering. CSAT is dated on close rather than three days after, because the most recent engagements now close today and a positive offset would have produced future-dated responses.

Findings & cleanup

Fixed on branch:

  • Tail placed a window early (ecb410e) — as above. Release-blocking; found by review, not by CI, and not by me.
  • Partial writes on a tenant that cannot be seeded (ecb410e) — user resolution ran after the catalogue upserts, so a tenant without the storyline's personas kept ~70 catalogue rows from a run that then threw. There is no transaction, so ordering is the only guard; resolution now runs before any write.
  • Silent degradation on a missing vendor or CSM (ecb410e) — vendorId undefined meant every expert rating was skipped and no quote, invoice or CSAT carried an attribution, while the run still reported success: a world that looks seeded and reads as zero. Both are now required and fail loudly, like the customer and expert personas.
  • Unrecoverable timeout risk (3d3bdfb) — the seed is ~1,600 sequential round-trips inside a synchronous server action that inherited the platform default duration. Server actions inherit from the route that invokes them, so maxDuration = 300 is declared on admin/settings/demo-data/page.tsx. This matters more than a slow button: the writes are append-only with no transaction, so a timeout mid-tail can only be duplicated, not resumed, until demo-reset-ops lands the purge.
  • Soft-deleted user aborted the run (3d3bdfb) — tenantId_email_role_unique is not partial on isDeleted, so an archived storyline user was hidden from the lookup and then collided on create. resolveUser now revives through the raw collection, the same shape upsertNamed already used one function above it.
  • Live-pipeline CSAT was not authorable (3d3bdfb) — the four survey/completed scores were hash-derived in a 7–9 band, so no storyline could express an unhappy live engagement. That is a hole in the swap point this module claims to be. StorylineEngagement.csat is now optional-with- fallback and the four scores are authored in default.ts.
  • Admin copy understated the action (3d3bdfb) — the page described only "demo leads", with no mention of the backdated history, the users, the CSAT responses, or that there is no undo.

Accepted / carried forward:

  • The seed run is one-way. No purge until demo-reset-ops, no transaction, and a re-run appends rather than resumes. Spec accepts this; the readiness audit's point is that the PR didn't say so where an operator would see it. Now stated here and in the admin copy: run it once, against the demo tenant only, and verify before announcing.
  • maxDuration = 300 assumes the Vercel plan allows it. The repo's next-highest is 120 (api/brd/chat). If the plan caps lower, the value silently clamps — confirm before the run.
  • Pre-existing, for intake, not this release: admin/demo-data/actions.ts revalidates /admin/demo-data, which is now only a redirect stub, so the page the admin is actually on is never revalidated after seeding. actions.ts is unchanged in this diff.

DoD smoke (on the preview — each line says who verified it)

  • The preview serves and routes unauthenticated traffic to sign-in — HTTP 200 at web-git-claude-demo-seed-storyline-pipeline-41es39-sustentus.vercel.app, redirecting to /sign-in?redirect_url=%2F (agent)
  • No Math.random() remains in the generator — grep over demo-data/** returns only the comment explaining its removal (agent)
  • The generator writes only through models; no notification, email or external call added — traced across the diff (agent)
  • Window semantics, hash determinism and slice bounds — pinned by deterministic.test.ts; storyline referential integrity and the three narrative beats by default.test.ts; both green in the Quality workflow's Run tests step (agent, via CI)

Not demonstrable on the preview — and the reason is structural, not an omission. The remaining criteria all assert the state of the demo tenant, whose six persona users live in Clerk against production. The preview deploys against the preview database, which has no such tenant, so signing in there demonstrates nothing about this storyline. demo-tenant-foundation had the same shape and resolved it the same way: the ops half ran against production.

Awaiting the operator (each needs the seed run against the demo tenant):

  • auth: the six personas still sign in and reach their ROLE_HOME (operator)
  • every engagement present with its authored account, title, value and CSAT score (operator)
  • avgCSAT, revenueWeightedCSAT and revenueAwaitingCSAT non-zero; at least one account flagged Failing CSAT (operator)
  • all 11 vendor deep-dives + the vendor dashboard populated at the default window and last-12-months, with non-zero prior-window deltas (operator)
  • the six persona dashboards showing the same accounts, people and reconciling figures (operator)
  • the fill-or-flag table posted on the PR, with a stub in pipeline/intake/demo-tenant/ for anything flagged (operator + agent)
  • payments: not touched by this diff — no test transaction required
  • notifications: none expected. The generator writes directly through models by design, so nothing should fire; worth a glance at the demo admin's inbox after the run to confirm the seed stayed silent (operator)
  • Paul's sign-off on the storyline tables, recorded on the PR (operator)

Notes

Timing the first seed run and recording the number on the PR is worth doing — it is the only evidence that maxDuration = 300 is enough, and the cost of being wrong is a half-seeded tenant with no purge.

Context budget: within band. One deliberate overrun — the vendor analytics services and the Mongoose timestamp helper were read outside the Inputs table, to confirm the window arithmetic the tail depends on rather than assume it. That read is what surfaced the release-blocking defect.

06_ship/output/investor-update.md

The demo tenant now tells one believable business story

Who it's for: Anyone running a demo — all six persona dashboards What shipped: Demo data is now an authored storyline with a backdated history and satisfaction scores, so every vendor analytics page renders populated. Why it matters: Refine the Bridge / Q2 Objective 1 — Establish Product-Market Fit with Vendor Partners: the demo is the product.

Two runs produce the same world; swapping it is a data change.

Dig deeper: https://github.com/sustentus/sustentus/pull/795

06_ship/output/release.md

Ship: demo-seed-storyline

  • pr: #795 · merged: yes — squash, on the ticked Ready to merge box and green checks
  • CI: green — Quality Project success on the head commit; Review diff against CONVENTIONS.md skipped (ENABLE_CLAUDE_REVIEW unset) and Migrate production database skipped, both by design
  • technical docs: apps/docs/app/technical/demo-environment — the runbook's persona-identity table (Build) and, here, the whole Populating Demo Leads section: what the seed writes and in what order, the backdated tail and its 30-day windows, the CSAT records, determinism replacing Math.random(), a new Swapping the storyline subsection, and the warning that the run has no undo and no transaction
  • business docs: no business docs impact — the seeder changes what the demo tenant contains, not what the platform does; no persona capability, journey step or headline capability moved
  • release notes: ship-note-only — no end-user note. This is demo tooling behind an admin-only page on a flagged tenant; nothing a customer reads the help-centre changelog for changed. Recorded here rather than published as an empty entry.
  • sent: ship note queued to #product-update on merge (2026-08-12) — .github/workflows/ship-note.yaml

The note's Dig deeper line was filled before the merge, not after. The workflow fires on the merge and emails the file the merge brought in, so a link filled afterwards would never reach the inbox. The PR URL is stable across the squash and there is no changelog entry to link, so there was nothing the merge needed to reveal first.

Acceptance check (vs spec)

Established by the diff, verified in Verify:

  • The generator contains no Math.random(); two runs against an equivalent empty tenant produce identical content — verified by grep over demo-data/** (only the comment explaining its removal remains) and pinned by deterministic.test.ts
  • The seeder upserts the nine non-persona customer companies and five non-persona experts by email; a second run creates zero of them — resolveUser, which Verify also hardened to revive a soft-deleted match rather than collide on the unique index
  • The six Clerk persona users are matched by email, never created or renamed, and carry their storyline engagements — isPersona on the storyline account/expert types
  • CSAT records for every engagement past survey_sent bar the authored exceptions, sub-scores varying around the overall — createCSAT
  • The generator still writes only through models — no notifications, emails or external calls — and still appends — traced across the diff in Verify
  • The provisioning runbook records the six persona identities so a rebuild reproduces them — technical/demo-environment, with the note that a storyline swap must update it

Not established by the diff — all seven need the same thing, the seed run against the demo tenant, whose personas live in Clerk against production. The preview deploys against the preview database and has no such tenant, so nothing on the preview could have demonstrated them. Verify recorded this as structural, not an omission, and it does not change on merge:

  • every engagement present with its authored account, title, value and CSAT score
  • avgCSAT, revenueWeightedCSAT and revenueAwaitingCSAT non-zero, with at least one account flagged Failing CSAT
  • all 11 vendor deep-dives + the vendor dashboard populated at the default window and last-12-months, prior-window deltas non-zero
  • the six persona dashboards showing the same accounts, people and reconciling figures
  • the fill-or-flag table on the PR, with a stub in pipeline/intake/demo-tenant/ per flagged surface
  • Paul's sign-off on the storyline tables recorded on the PR
  • the seed re-run against the demo tenant, so the signed-off storyline is live

The code shipped; the storyline is not live until that run happens. Two things to settle first, both carried from Verify: maxDuration = 300 assumes the Vercel plan allows it (the repo's next-highest is 120 — it clamps silently if not), and the run is one-way, with no purge until demo-reset-ops. Time the first run and put the number on the PR — it is the only evidence 300 is enough, and the cost of being wrong is a half-seeded tenant with nothing to purge it.

Also carried forward, for intake rather than this release: admin/demo-data/actions.ts revalidates /admin/demo-data, now only a redirect stub, so the page the admin is actually on is never revalidated after seeding. Unchanged in this diff.

Context budget: within band. No overrun — the OKR and initiative pages were read for the ship note's tie-in, both named by the Inputs table.