storyline-full-coverage-generatorrun.md02_define/output/spec.mdThe Phase 3 coverage matrix (.icm/intake/demo-data-quality/_source/audit-report.md) proves that
whole surfaces open dark on the demo tenant after every reset: the invoice approvals queue, disputed
and rejected invoices, quote draft/rejected and the awaiting_confirmation → quotation_process
loop-back, a blocked milestone, expert evidence in any state, every persona's notifications, SDM
outreach dispositions, the CSM day-over-day delta, and overdue/SLA-breached work. A demo that opens
dark on the approvals queue or an empty notification bell cannot carry a vendor conversation, which
is what Q2-2026 Objective 1 (Establish Product-Market Fit with Vendor Partners) needs the bridge to
do. Separately, the generator hardcodes the tiered catalogue (platforms → products → services →
skills) as module constants, so the ICP reframe that saas-vendor-storyline (stub 10) must perform
cannot be done as a content change — which is the whole promise of the storyline contract.
Extend the Storyline contract and the generator so that mechanics stay in the generator and
all content lives in the storyline module. Two halves, both landing in this run:
1. Content migration (no behaviour change).
demo-data/index.ts module constants
(CATALOGUE_PLATFORMS, UNIVERSAL_SERVICES, SPECIALIST_SERVICES, CATALOGUE_SKILLS) and onto
the Storyline type as an authored catalogue block. The generator reads it; the default
(Calderon) storyline carries today's values verbatim, so the seeded catalogue is byte-identical
before and after this change.StorylineEngagement and
StorylineClosedEngagement gain optional per-record copy fields (proposal title/summary,
milestone names, blocker label/impact, action-item labels, change description, customer/team
messages, CSAT appreciation/suggestion). Where a record authors nothing, the generator falls back
to the existing StorylineCopy pools exactly as today — so the Calderon storyline keeps working
unchanged through the transition, and stub 10 authors the real values against a contract that
already accepts them (D8).2. New authored coverage, all deterministic under the existing deterministic.ts guarantees
(hash-of-identity, never Math.random()), all tenantId-scoped, all idempotent:
| Dark surface | What gets seeded |
|---|---|
/finances/approvals queue |
one engagement carrying an invoice_draft invoice |
| invoice chips | one invoice_disputed and one invoice_rejected invoice, on distinct engagements |
| quote states + loop-back | one quote_draft; one quote_rejected whose lead's status history runs … → quotation_process → awaiting_confirmation → quotation_process (the rejection loop) |
milestone blocked |
one blocked milestone on a work-in-progress engagement |
| expert evidence | rows in all four EXPERT_EVIDENCE_STATUSES (draft, submitted, approved, rejected); the rejected row carries a rejectionReason, reviewed rows a reviewedAt/reviewedBy |
| notifications | per-persona rows with a read/unread mix, authored type/title/body/link (types drawn from the existing typed enums) |
| SDM outreach | sdm_outreach_states rows covering all four OUTREACH_ACTION_STATES |
CSM changeVsYesterday |
one prior-day csm-portfolio-snapshot per CSM that differs from today's rollup by a storyline-authored delta |
| overdue / SLA breach | overdue action items (dueDate in the past, still open) and a backdated in-flight engagement whose status-history age breaches its SLA band |
DemoDataResult gains a counter per new entity family so the admin demo-data page and the seed
summary report what was written.
These follow from the code as it stands and correct or narrow the stub where it was written from the audit report rather than from the current source. Each changes what gets built.
demo-reset/policy.ts classifies csm-portfolio-snapshot
under KEPT_MODEL_FILES (deliberately — purging it blanked changeVsYesterday). expert-evidence,
notification and sdm-outreach-state are correctly in PURGED_MODEL_FILES. Consequence: the
snapshot write must be idempotent on its own, because reset will not clear it. The generator
therefore seeds it through the existing
csmPortfolioService.captureDailySnapshots(tenantId, snapshotDate), which already upserts on
{ tenantId, csm, snapshotDate }, rather than inserting rows directly. No change to
policy.ts classifications is needed and none is made.changeVsYesterday is
group.atRiskNow - prior. Capturing yesterday from the world just seeded makes prior equal to
today, so the delta renders 0 and the surface still tells no story. The storyline therefore
authors the prior-day offset (an at-risk delta, with the matching RAG shift), which the generator
applies to the captured prior-day row so the CSM portfolio shows real day-over-day movement.sla_definitions stay out. The gaps table lists the SLA config card as dark, but
sla-definition is in KEPT_MODEL_FILES and is owned by the baseline db:seed tenant defaults,
which survive reset. The demo seeder writing its own would duplicate a second writer into a
collection reset deliberately preserves. Out of scope below.50 rows each (crossing the page boundary) with an unread mix; Admin, Vendor and Customer get a smaller populated set. Activity passes 25 rows for the personas whose feed is a daily surface.
Storyline type; demo-data/index.ts holds no
catalogue content constants, and seeding the default storyline produces the same platforms,
products, services, skills and product↔service links as before the change (counts and names
asserted in a test).StorylineEngagement and StorylineClosedEngagement accept per-record copy; a record that
authors none falls back to the StorylineCopy pools, and the default storyline — which
authors none — seeds an unchanged world.invoice_draft, invoice_disputed and
invoice_rejected invoice; a quote_draft and a quote_rejected; a blocked milestone;
expert evidence in all four states; notifications for all six personas; sdm_outreach_states
in all four action states; a prior-day CSM snapshot; overdue action items./finances/approvals queue, the /expert-evidence review queue, the SDM outreach
controls and the notifications bell each render populated for their personas on the demo
tenant after a reset (verified on the preview at Verify).rejectionReason; the quote_rejected lead's
status history contains awaiting_confirmation followed by a return to quotation_process.changeVsYesterday is non-zero for at least one CSM after a seed+reset.tenantId; the existing cross-tenant reset test still proves a reset
cannot reach another tenant's rows.sla-stage-map-engine-alignment merged as #811).demo-reset/policy.test.ts stays green with no classification changes, and the reset result
reports non-zero purged counts for expert-evidence, notification and sdm-outreach-state.DemoDataResult reports a count for each new entity family.saas-vendor-storyline (stub 10) authors it against this
contract. This run authors only enough new content for the default (Calderon) storyline to light
every surface.sla_definitions — owned by the baseline db:seed tenant defaults and kept across reset
(R3).assertTransition enforcement for milestone and expert-evidence transitions — a real doctrine
gap, explicitly not selected for this round (D1/D5); the seeder writes states directly as it does
today.demo-reset/policy.ts classifications.Context budget: within band. Read the stub, _source/decisions.md, _source/audit-report.md
Phase 3, breakdown.md, and — as targeted confirmation of where things live — the storyline
contract, deterministic.ts, demo-reset/policy.ts, the four target model schemas, and
csm-portfolio's snapshot capture. No scope.md exists for this epic by design (the cut was made
from _source/, per breakdown.md).
03_build/output/notes.md075280d (contract + coverage + tests)demo-data/storyline/types.ts: added StorylineCatalogue (platform → product
→ service → skill), StorylineEngagementCopy, the invoice/quote state unions,
StorylineEvidence, StorylineNotifications, StorylineOutreachState and
StorylinePriorDay. Storyline gains catalogue, evidence,
notifications, outreach and priorDay. Engagements gain copy,
extraInvoiceStates, extraQuoteStates, blockedMilestone,
overdueActionItems and staleDays. The evidence/outreach/notification enums
are imported from the models rather than restated, so they cannot drift.demo-data/index.ts: the four catalogue constants are gone — the generator
reads storyline.catalogue. Added a textFor(authored) resolver that the
eight prose-writing helpers now take, so authored copy wins field by field over
the pools. New writers for expert evidence, notifications, SDM outreach and the
prior-day snapshot; new createOverdueActionItems; createLead accepts
extraStatuses (the loop-back) and the delivery graph accepts
blockedMilestone / extraInvoiceStates. Resolves the SDM and admin persona
seats, which no storyline had ever attached anything to.demo-data/notification-fill.ts (new): the one piece of the new coverage with
a decision in it — how a seat's list is padded, and which rows read as read.
Pure, so it is unit-testable; the generator has no DB-free tier.csm-portfolio/index.ts: added captureBackdatedSnapshots(tenantId, daysBack).
The tenant-local date derivation stays here, beside the one
captureDailySnapshots already does, rather than being copied into the seeder.storyline/default.ts: the catalogue moved in verbatim; authored the evidence,
notification, outreach and prior-day content, and tagged six engagements with
the coverage flags.apps/web/.../demo-data-button.tsx: the summary table is a hand-kept list, so
the five new counters were added or they would not render.default.test.ts asserts the tiers are populated, every skill points at a
defined service, every service is covered by a skill, and names are unique.
The Calderon values moved across verbatim, so the seeded catalogue is
unchanged.textFor falls back field by field;
an empty authored list counts as unauthored. Calderon authors none.default.test.ts.rejectionReason; the rejected quote's lead
returns to quotation_process via extraStatuses.padTo
56/54/52, asserted in both test files.changeVsYesterday non-zero — the storyline authors atRiskDelta: 2,
applied on top of the captured prior-day row. Asserted non-zero in the
storyline test; the end-to-end number is a preview check.deterministic.ts (no Math.random(), no counters).
But the generator itself cannot be unit-tested: the repo configures only the
unit tier (node, no DB), so "reset twice and diff the collections" has no
tier to run in. I did not improvise one — see Notes for Verify.tenantId — all five writers set it explicitly;
the existing cross-tenant reset test is untouched.staleDays: 70 on Aldercroft.
Delivery breaches at 35 business days (~49 calendar), so the test
asserts staleDays > 49 rather than merely present; a smaller number would
compute amber and the criterion would pass while the surface stayed orange.policy.test.ts green with no classification changes — none made. All
three new collections were already in PURGED_MODEL_FILES with getters
wired in demo-reset/index.ts.DemoDataResult reports a count per new family — five counters added, and
surfaced on the admin page.batch (a fresh ObjectId, in
every quoteId) and allocateNextLeadRequestId. So "identical worlds" has
always meant "identical content", not identical ids. Worth confirming on the
preview by reseeding twice and comparing counts.updateMany moves atRiskNow and rag.red together and clamps both at 0.extraInvoiceStates wraps onto the milestone list (i % milestones.length)
because a WIP engagement has two default invoices and four milestones. That is
deliberate — an invoice with no milestone renders an empty approvals row — but
it does mean one milestone can carry two invoices.notifyX
wrappers, so no Ably publish and no email fires for seeded data. That is what
makes seeding them safe, and it is why the bell reads them fine.Quality check run on the PR.Context budget: within band. Read the spec, CONVENTIONS.md,
packages/services/AGENTS.md, the demo-data and demo-reset sources named in
touches:, and — to get the numbers right rather than guess — the four target
model schemas, the notification type enums, sla/stage-map.ts +
seed/tenant-defaults.ts (for the breach threshold behind staleDays), and
csm-portfolio's snapshot capture.
04_verify/output/verify.mdcomplex) — 7 findings, all verified
against source, all 7 fixed on branchEverything an agent can carry is done: three passes run, ten findings fixed, CI
green (Quality Project success at c052466, covering format, lint, typecheck
and the Run tests step). What is not done is the half of the Definition of
Done that needs preview credentials and a signed-in browser. I have no Clerk
session and no inbox, so I am not able to claim those lines, and the gate must not
pass on unverified ones.
Three of the ten findings were defects that would have aborted the seed in production. Two more were surfaces that seeded successfully but could never be seen. None of them were visible to CI, because the repo configures only the unit tier — nothing exercises a real database write. That is the single most important fact about this run: a green suite told us almost nothing about whether this feature works.
Agent-run:
default.test.ts /
notification-fill.test.ts (agent)tenantId; requireDemoTenant still precedes the
first write (agent — read at demo-data/index.ts)apps/web/app/(app) (agent — checked against the route tree)demo-reset/policy.ts unchanged; the three new collections were already
purged with getters wired in demo-reset/index.ts (agent)Operator-demonstrated — all outstanding:
maxDuration.expert-evidence, notification, sdm-outreach-state./finances/approvals shows the invoice_draft row; disputed and rejected
chips render./expert-evidence review queue shows four rows, one per state, the rejected
one with its reason.changeVsYesterday is non-zero.Fixed on branch (0eb8332, c3bd196, 5c74d08, c052466):
quote.ts holds a global
unique {lead, proposal}. The first quoting engagement aborted the entire
Populate. Each extra quote now gets its own proposal, which also reads better:
the rejected quote was priced against the proposal that had to be replaced.invoice.ts holds a global unique {milestone}. Latent today (all extras sat
on in_progress) but immediate for any storyline authoring one on a done
stage. Stops instead of wrapping, and the storyline test now asserts extras
only sit where a spare milestone exists.(platform, product, region, language) off each lead, and seeded leads never
set location.region — so every cell was Unspecified and the authored
coordinates matched nothing. Accounts now carry a region that reaches the lead,
and the generator derives the coordinates from the world it just wrote; the
storyline authors only the dispositions. This is a contract change made at
Verify, and it is the right home for it: authoring a coordinate meant guessing
which catalogue path the generator would pick.staleDays sat on a non-persona expert. The expert work queue filters to
the signed-in expert, so the red SLA band existed in the database where nobody
demoing could see it. Moved to the expert persona's engagement./csm/blockers, /sdm/bids, /expert/projects, …) and would bounce off
deny-by-default. All remapped to real pages.insertMany (safe: the
plugin trio hooks queries and find, not save).staleDays, so a breached engagement no longer reads
"72 days in delivery" beside a project that started three weeks ago.readAt can no longer land in the future.Accepted, recorded rather than fixed:
blockedMilestone does not drive the health at-risk driver.
customer-project/health.ts keys milestone criticality on
blocksGoLive && overdue && status !== done; it has no blocked branch, and
createMilestones never sets blocksGoLive. The milestone state seeds and the
workspace icon renders, which is what this spec's acceptance criterion asks for
— the stub's parenthetical about the health driver was never in the spec. Making
health read blocked is a product change and belongs in its own scope.listOpenByLead), so they surface on the lead rather than in a cross-account
overdue view. The criterion ("overdue action items seeded") is met; a queue is
new scope.Routed to Ship, not done here:
apps/docs/.../demo-environment/page.mdx is stale in three places: the
"writes, in order" list omits all five new entity families; the catalogue is
still described as generator-supplied; the append-only claim needs the
idempotency nuance. Ship's docs-sync owns this.changelog-entry owns it.Rollback note for the PR: no migrations, so code reverts cleanly. The prior-day
snapshot step mutates csm_portfolio_snapshots, which a reset deliberately
preserves — a reverted deploy does not undo the authored atRiskDelta shift. It
is self-correcting (capture $sets the position wholesale before the delta is
re-applied, and the row leaves the "yesterday" window next day) and
demo-tenant-only.
Context budget: over band, deliberately. The reviews surfaced claims about live
unique indexes and about which surface reads which field, and each one had to be
checked against the model and the reading service before I acted — taking a
review finding on trust is how a wrong fix lands. Files read beyond the Inputs
table: quote.ts, invoice.ts, sdm-outreach-state.ts, sdm-risk/index.ts,
customer-project/health.ts, lead.ts, the apps/web route tree, and the
plugin trio.