saas-vendor-storylinerun.md02_define/output/spec.mdMarketing pitches Sustentus at SaaS vendors who sell licences rather than services, while the demo
world every prospect actually sees models an IT consultancy — Calderon Technology Partners, whose
catalogue is SAP, Oracle, Salesforce and Microsoft. The audit recorded the contradiction as ICP
ambiguity A1 (.icm/intake/demo-data-quality/_source/audit-report.md, Phase 2) and Jamie settled it
in decision D6: reframe the demo world as a software vendor whose services arm delivers
implementations of its own product. Until that lands, the demo argues against the pitch on the
initiative it exists to serve — Refine the bridge / Q2-2026 Objective 1, Establish Product-Market Fit
with Vendor Partners — and two smaller data defects ride along with it: every seeded lead carries
languages: ["GB"] (a country code in a language field), and shared copy pools recycle visibly once
enough records are on one screen (D8).
Stub 9 (#815, merged) extended the Storyline contract and the generator so every dark surface
seeds from content alone. This run supplies that content.
Author a new storyline module against the merged contract and re-point the swap point at it. The world is a fictional B2B software vendor: the catalogue is its own product's modules, and every engagement is an implementation, integration, migration, training or optimization of that product for one of its customers — mirroring the service menu marketing already publishes (Rapid Implementation, Team Training, Ongoing Optimization, Premium Support).
What the ICP research validated carries over unchanged: a Northern-European customer base across
recognisable sectors, EUR throughout, roughly €30k–€150k fixed-price engagements, ~25 live
engagements plus a ~36-engagement closed tail over 18 months, and the three narrative accounts the
analytics depend on — an anchor account, a failing-CSAT account, and a declining at-risk account.
Every name is fictional and every address sits on an .example domain (RFC 2606), so nothing can
receive mail by accident. The six Clerk-provisioned persona logins (demo+…@apoyar.eu) are carried
across verbatim and matched by email; the storyline never creates or renames them.
Per-engagement copy is authored for every engagement — blockers, action items, customer and team messages, change controls and CSAT verbatims — so no single screen shows the same sentence twice (D8). The shared pools stay as the contract's fallback but no engagement relies on them.
Two code changes ride with the content, both minimal:
language joins the contract. languages: ["GB"] is hardcoded in the generator
(demo-data/index.ts:1590), so it cannot be fixed by data alone. StorylineAccount gains an
optional language (ISO 639-1, lowercase) and the generator reads account.language ?? "en" at
the lead-create site and in the SDM coverage cell it derives. That axis is not cosmetic: outreach
cells are keyed (platform, product, region, language), and one hardcoded value collapses every
seeded lead into a single language column. Calderon keeps working untouched via the default.storyline/default.test.ts asserts ~40
invariants against defaultStoryline by name. It is re-pointed at activeStoryline so the suite
always guards whichever world is live, and its three narrative-account keys (saltmarsh,
tannerrowe, meridian) become named constants at the top of the file, updated to the new
module's keys.The authored content tables go in the PR description for Jamie's explicit sign-off before merge —
the same gate Calderon's tables passed (.icm/runs/demo-seed-storyline/03_define/output/spec.md).
demo-data/storyline/ exports a Storyline for the SaaS-vendor world, and
storyline/index.ts re-points activeStoryline at it. defaultStoryline (Calderon) is still
exported and still compiles.StorylineAccount gains an optional language (ISO 639-1, lowercase); the generator reads
account.language ?? "en" for lead.languages and for the coverage cell it derives. No other
generator behaviour changes.closedMonthsAgo: 1) and the prior one (2), and some paid
engagements left without a CSAT response so revenueAwaitingCSAT is non-zero.in_progress
engagement and at most two per engagement; quote_draft plus a quote_rejected on a quoting
engagement; at least one blocked milestone on an in_progress engagement; at least one
engagement with overdue action items and one with staleDays above 49; all four expert-evidence
states with a reason on every rejected row; exactly four outreach dispositions covering all four
action states.priorDay.atRiskDelta is non-zero.copy — no engagement falls back to the
shared pools for blockers, action items, messages, change descriptions or CSAT verbatims..example domains, with distinct emails; each account
carries a country, a sales region and a language; exactly one account and one expert are
marked isPersona, matching the existing Clerk logins by email.activeStoryline with the narrative keys as constants, and
passes; the determinism tests pass, and the module contains no Math.random, Date.now or
other non-reproducible value, so two resets produce the same world.activeStoryline is the agreed trade-off:
one home for the contract invariants, and the spare is no longer asserted against.account.language; the contract from #815 is taken as
given and is not extended further.03_build/output/notes.mdfeat: saas-vendor-storyline — author the Orrery Software world,
feat: saas-vendor-storyline — read the account language from the contract,
test: saas-vendor-storyline — point the contract suite at the active storylinedemo-data/storyline/saas-vendor.ts (new): the Orrery Software world — a fictional B2B software
vendor whose services arm implements its own product. Ten Northern-European accounts across five
regions and five languages, six delivery consultants, a 25-engagement live pipeline covering all
nine journey stages, and a 36-engagement backdated tail across eighteen months. Every one of the
61 engagements authors its own copy (blockers, action items, customer and team messages, change
controls, CSAT verbatims), so nothing on a single screen repeats — 145 distinct blocker labels
with no duplicate.demo-data/storyline/index.ts: activeStoryline re-pointed at the new module. defaultStoryline
(Calderon) still exported as the reference/spare world.demo-data/storyline/types.ts: StorylineAccount gains an optional language (ISO 639-1).
Defaults to en when a storyline omits it, so Calderon is unaffected.demo-data/index.ts: the lead-create site reads account?.language ?? "en" instead of the
hardcoded ["GB"]. That value is one of the four SDM coverage-cell dimensions, so the literal
collapsed every seeded lead into a single language column; it was also a country code sitting in
a language field.demo-data/storyline/default.test.ts → active.test.ts: the contract suite now runs against
activeStoryline, with the three narrative account keys as named constants at the top of the
file. Five tests added for criteria the inherited suite did not cover (window delta, account
completeness, .example domains, authored copy on every engagement, blocker-label uniqueness),
and the persona test now asserts the Clerk logins by address rather than a storyline-specific key.Storyline; index.ts re-points activeStoryline; defaultStoryline
still exported — Calderon is untouched on disk.StorylineAccount.language added; generator reads account?.language ?? "en" for
lead.languages and the derived coverage cell. No other generator behaviour changed.in_progress engagement; quote_draft
plus quote_rejected on a quoting engagement; one blocked milestone on in_progress; two
engagements with overdue action items and one at staleDays: 70 (past the 49-day threshold);
four evidence rows covering all four states with a reason on the rejected one; exactly four
outreach dispositions.priorDay.atRiskDelta is 2..example domains with distinct emails (16, no
duplicates); every account carries country, region and language; exactly one persona account
and one persona expert, both matching the existing Clerk addresses.activeStoryline with narrative keys as constants. Module contains no
Math.random, Date.now or new Date( — verified by grep, so two resets reproduce the world.active.test.ts, which CI runs. I did not run the suite locally —
the Quality workflow is the signal.activeStoryline (spec → Out of scope). It still compiles, and tsc covers
that much; if it is ever reactivated, the suite follows it automatically.account?.language. The fallback keeps Calderon
identical, but the SDM coverage grid is the surface that changes shape most — it should now show
five language columns (nl, sv, en, de, da, fr) instead of one, and the outreach dispositions cycle
across more cells than before..example assertion deliberately exempts the two persona rows, whose addresses are real
Clerk logins on apoyar.eu.04_verify/output/verify.mdcomplexity: complex) — 5 findings. 2 fixed on branch, 2 escalated
to Jamie and then fixed on his call, 1 recorded as latent.link target, which is a data value read by an existing policy, not a
policy. All seeded people and companies are fictional on RFC 2606 .example domains.Agent-run
web-git-claude-saas-vendor-storyline-pipeline-447zaj returns 200 and
redirects to /sign-in, so every acceptance criterion below this line is behind auth (agent)Storyline; activeStoryline re-pointed; defaultStoryline still
exported — traced in storyline/index.ts, asserted by the contract suite (agent)StorylineAccount.language read by the generator for lead.languages and the coverage cell —
traced at demo-data/index.ts lead-create site (agent)active.test.ts, green in CI (agent)Math.random, Date.now or new Date( in the module (agent, grep)Operator-demonstrated — NOT PERFORMED; waived at Ship
Jamie authorised the merge without this half (see
05_ship/output/release.md). The lines below stay unticked because they were never run — not because they passed. The reset-and-approve matching check is the one to do first after deploy.
The agent has no preview credentials, so none of the following is claimed. Each needs a demo-tenant
reset first (admin → demo data → reset), because the storyline swap only takes effect on a reseed
and a Populate-without-reset leaves stale sdm-outreach-state rows keyed on the old language.
fitScore: 0 (operator). This is
the one that would have caught the blocking finding, and nothing in CI exercises it.notify* wrappers, so no email or Ably publish fires from a reseed
(agent-traced; operator to confirm the bell renders and paginates past 50 rows)Fixed on branch
16c23b2) — the storyline's own regression, and the
most serious finding of the pass. filterEligibleExperts treats language as a hard filter; it had
been passing by accident because every lead said GB and every expert inherited the ["gb"]
schema default. Giving leads real codes broke the accident. Worse than an empty list:
runLeadExpertMatcher fires on BRD approval and upserts onto the same key the seeder wrote good
matches to, so approving a BRD would have overwritten seeded matches with fitScore: 0. Fixed by
adding StorylineExpert.languages, authoring it on the six consultants, and writing it in the
generator — as an explicit update, not just the create factory, because Clerk-backed personas
survive every reset and would otherwise keep ["gb"] forever. Two contract tests now pin the
invariant.9fb3247) — isAvailable defaults to false and was
create-only, so the signed-in expert was excluded from matching on any pre-existing tenant. Same
root cause, same fix, folded into the same update.9fb3247) — /vendor/csat is
roles: ["vendor"], so the CSM clicking their own bell was bounced. Inherited from the Calderon
storyline. Now /csat, permission-gated on csat.view.["GB"] language defaults, platform-wide (6ca2623) — escalated to Jamie as a scope
decision; his call was to fix it here rather than file it. lead.languages and user.languages
both move to ["en"], and they must move together — changing either alone excludes every expert
from every lead, since today's data passes the gate only because both sides say gb. Migration
1786924800000-language-codes-not-country-codes backfills, deliberately narrow: only rows still
holding the exact single-element default, so authored values survive and down reverses a precise
set. Renamed off a timestamp that collided with #817's migration after merging main.6ca2623) — escalated to Jamie; his call was to
retitle. The generator derives each lead's product from a hash of account:title, so titles
naming a module ("Orrery Core rollout…") disagreed with the chip beside them on roughly half the
board. Fifteen titles now describe the work; module names stay in the proposal prose, where
nothing contradicts them. PR content tables updated to match.Accepted / recorded — not fixed here
utils/languages.ts is a trap, not a bug. Its 22 entries are uppercase and include no Swedish
or Danish, and getLanguageByCode matches case-sensitively — so sv, da and even nl would
miss. Harmless today: all three helpers have zero call sites outside the package's own build
output, which the production-readiness and code-review passes disagreed about until I checked. The
moment anything renders a language label the seeded world goes blank. Worth either extending and
case-normalising that list or deleting it as dead code — its own small chore, not this run.SdmOutreachState is upserted on a key that
includes language, so a tenant seeded before this change keeps rows on the old value and a
Populate writes new ones beside them. They render nothing (cells derive from seeded leads) but
accumulate. resetDemoTenant purges the collection, so the mitigation is to reset, not
populate, on the first deploy of this storyline — noted in the smoke steps above and worth a
line in the ship note.tsc still proves it satisfies Storyline, and it now benefits from the same ["en"] fallback on
both sides, so it is consistent rather than newly broken. Delete it if it is not genuinely a spare.Notes for Ship
apps/docs/app/technical/demo-environment/page.mdx describes the demo world in Calderon's terms
and needs updating for Orrery Software — docs-sync's job at Ship.activeStoryline re-point and the
world returns to Calderon. The language migration is separate and has a working down.main moved twice during this run (#817, #819) and was merged in at 6ca2623.Context budget: went beyond the Inputs table into matching/filter-eligible-experts.ts,
models/{user,lead}.ts, demo-reset/index.ts, route-policies.ts and utils/languages.ts to
verify the reviews' findings rather than take them on trust. Two of the five code-review findings did
not survive that check as stated.
05_ship/output/investor-update.mdWho it's for: every persona — the demo tenant is what prospects see What shipped: the demo world is now a software vendor implementing its own product, not a consultancy integrating other vendors'. Why it matters: Refine the Bridge, Q2 Objective 1 — the demo no longer argues against the pitch.
Ten Northern-European customers, 25 live engagements, 18 months of authored history.
Dig deeper: https://github.com/sustentus/sustentus/pull/818 · https://help.sustentus.com/changelog/2026-08-17-saas-vendor-storyline
05_ship/output/release.mdResource not accessible by integration to both the session token and the GitHub MCP path,
and the same condition failed four consecutive Quality runs inside dorny/paths-filter's
listFiles call — before any check body ran, and not clearable by re-running. The workflow
requested pull-requests: read correctly, so the fault was above it; 2defd54 removes the
dependency by passing base, so the step diffs against the base branch with git instead. Jamie
confirmed the resulting run green, and GitHub's own mergeable_state moved blocked → clean.
The agent could not read the check runs directly at any point — that 403 never lifted.apps/docs/app/technical/demo-environment/page.mdx — persona runbook table
(vendor, expert and anchor-customer rows), the storyline description, the swap procedure, and a
new rule recording that languages must line up or the world matches nobody.apps/help/app/changelog/2026-08-17-saas-vendor-storyline/page.mdx and the ship note at
05_ship/output/investor-update.md.Structural criteria are asserted by active.test.ts and were verified locally by script before
each commit. They have not been confirmed by a green CI run on the current head, and the
signed-in half of the Definition-of-Done smoke has not been performed at all.
Storyline; activeStoryline re-pointed; defaultStoryline still
exported — traced in storyline/index.tsStorylineAccount.language added; generator reads account?.language ?? "en".example; distinct emails; country, region and language on every accountactiveStoryline; no Math.random, Date.now or new Date(The Definition of Done asks for every acceptance criterion demonstrated on the preview. The
signed-in half never was: no persona sign-in, no demo-tenant reset, no walkthrough of the analytics,
workspace, finances or coverage surfaces, and — the one that matters most — no BRD-approval
matching check. Every operator line in 04_verify/output/verify.md is still unticked, and they are
unticked because they were not done, not because they failed.
That check is not ceremony. The blocking finding this run produced (16c23b2) was invisible on every
static screen and would only have shown itself by approving a BRD on a seeded lead and watching the
match list collapse to fitScore: 0. Nothing in CI exercises the seeder against a database, so the
suite cannot stand in for it. The fix is believed good and is pinned by two contract tests, but
"believed good" is what it remains.
Jamie was told this twice and authorised the merge anyway; that is his call to make and it is
recorded here rather than quietly dropped. First action after deploy should be the reset-and-
approve check — if it fails, revert is one line (activeStoryline back to defaultStoryline in
storyline/index.ts); the language migration is independent and has a working down.
Everything else Ship owed is in this PR: docs synced, changelog written, ship note written with both
Dig deeper links filled. The merge carries all of it and ship-note.yaml sends the note to
#product-update.