Skip to content

Launch demo fixture: rich synthetic signals for testing + screenshots #482

@jayzalowitz

Description

@jayzalowitz

Launch demo fixture: rich synthetic signals for testing + UI/UX screenshots

Context

Specs 01-08 build the digest read layer and UI. To verify them end-to-end and to
capture launch screenshots, someone needs to boot the full stack pre-populated with a
believable, varied signal set that exercises every surface — to-dos, FYI clusters,
deadlines, commitments, security alerts, entity cross-links, multi-source rows. Today
the seed creates only 3 users and 6 decisions, which is too thin to make the digest
look real. This spec adds a dedicated, heavy, deterministic demo fixture.

The hard requirement (this is invariant #0, not a nice-to-have): this fixture
must NEVER run for a real user — not grandma, not a brand-new user, not in production
— unless someone explicitly and deliberately asks for it. It is opt-in, isolated to
reserved demo identities, and refuses to run in any context where it could touch real
data. A demo seeder that auto-fires on an empty database would dump fake breach
alerts and fake commitments into a real new user's twin. That must be impossible by
construction, not by convention.

Current State

Verified 2026-06-06.

  • packages/db/src/seeds/seed.ts — deterministic seed: 3 fixed-UUID users (demo user
    a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d), 6 decisions across 8 domains, policies,
    preferences. Too thin to populate a convincing digest; not source-varied.
  • bin/skytwin-dev — boot flow runs migrate then "seed if no users exist." A
    brand-new install / fresh user is exactly the empty-DB case, so anything wired into
    this path runs for new users automatically. The demo fixture must NOT be wired
    here.
  • apps/api/src/routes/demo.ts:30 — demo user is gated to localhost + NODE_ENV=
    development (auth bypass). Good precedent for environment gating to reuse.
  • apps/api/src/routes/events.ts:178POST /api/events/ingest runs the full
    pipeline for a { userId, source, type, data, timestamp? } signal. The fixture can
    push synthetic signals through the real pipeline (most realistic) rather than
    INSERTing decisions directly.
  • packages/connectors/src/mock-email-connector.ts:14-194,
    mock-calendar-connector.ts:12-178 — hardcoded template signals, poll-based,
    RawSignal-shaped. Reusable building blocks; currently email + calendar only.
  • apps/worker/src/jobs/briefing-generator.ts — accepts { cadence, userIds, llmClient }; can be invoked directly to force a briefing (needed so the digest is
    populated at screenshot time rather than waiting for the 24h cron).
  • packages/memory-gbrain/src/__tests__/fixtures/persona-sam-patel.ts — an existing
    rich synthetic persona (6-week founder storyline, 100+ tagged signals). Reuse its
    authoring style; do not reuse it verbatim (it's memory-test-shaped).
  • ABSENT: any explicit DEMO_FIXTURE flag, any heavy demo dataset, any source-varied
    (filesystem/voice) synthetic signals, any one-command "populate for screenshots."

Proposed Change

A dedicated, opt-in demo:fixture command that (1) refuses to run unless explicitly
requested and safe, (2) writes only to reserved demo identities, (3) pushes a rich
source-varied synthetic signal set through the real ingest pipeline, and (4) forces
briefing generation so every digest surface is populated and screenshot-ready.

Safety invariant #0 — opt-in + isolation (implement first, test hardest)

A three-gate guard runs BEFORE the fixture writes anything. All three must pass:

  1. Explicit request. The fixture runs only via its own command
    (pnpm demo:fixture) or an explicit SKYTWIN_DEMO_FIXTURE=1. It is NEVER called
    from bin/skytwin-dev, from the "seed if no users exist" path, from onboarding,
    or from any startup hook. No code path reaches it implicitly.
  2. Environment safety. Refuse (non-zero exit, clear message) if NODE_ENV === 'production'. Refuse if the DB connection target is not localhost / not an
    allowlisted demo host. Both refusals require a deliberate, loud override to bypass:
    --i-understand-this-writes-demo-data AND a re-typed confirmation token — and even
    then production stays hard-blocked (there is no flag that lets it run in prod).
  3. Identity isolation. The fixture only ever writes under a reserved demo-user
    namespace (the existing demo UUID plus a small set of reserved demo UUIDs, all
    carrying a is_demo = true marker / known-prefix). It must never create, mutate,
    or attach signals to any user it did not itself create as a demo user. Before
    writing, it asserts the target user is demo-namespaced; if not, abort.

Grandma test: a real person installs SkyTwin, signs up (empty DB), boots the app.
Result: zero demo data, because (1) onboarding never calls the fixture, (2) the
auto-seed path doesn't include it, and (3) even if mis-invoked, identity isolation
refuses to write to her real user. Three independent reasons it can't happen.

Implementation Details

  1. Commandpackages/db/package.json script "demo:fixture": "tsx src/seeds/demo-fixture.ts"; root "demo:fixture": "pnpm --filter @skytwin/db demo:fixture". Flags: --reset (delete + recreate demo data only),
    --users=N (default 1 — the primary demo user), --seed=<int> (determinism).
  2. The guardpackages/db/src/seeds/demo-guard.ts, a pure assertDemoSafe(env, dbTarget, flags) returning a typed result; called as the first line of
    demo-fixture.ts. Unit-tested in isolation so the gates can't silently regress.
  3. Reserved demo identities — a fixed set of demo UUIDs + a DB-level is_demo
    boolean (new nullable column, default false / false-for-existing) on the users
    table so isolation is checkable in SQL, not just by UUID convention. Migration adds
    the column; real users are is_demo = false.
  4. Synthetic signal setdemo-fixtures/signals.ts: a deterministic,
    source-varied corpus (all fictional, no real names/companies/amounts) designed to
    light up every spec:
    • email: a few that produce to-dos (escalated actions), several FYI/newsletter,
      a couple with embedded deadlines, one or two security-alert shaped (spec 06).
    • google_calendar: invites, a conflict, an event description containing a
      user-authored commitment (spec 02/07).
    • filesystem (idle-miner shape): a project file with a TODO + deadline comment
      (spec 03/07).
    • voice (transcript shape): a note where the user says "I'll do X" (spec 02/07,
      the multi-source showcase).
    • Entities deliberately recur across several signals so spec 05 cross-linking has
      something to collapse, and span ~4-5 life domains so spec 04 clustering produces
      a believable set.
    • Volume target: enough that the digest shows ~5-7 to-dos and ~6-8 topic clusters —
      matching the dense-but-scannable look the UI is designed for.
  5. Ingestion — push each synthetic signal through POST /api/events/ingest (real
    pipeline → real decisions/candidates/explanations), not direct INSERTs, so
    screenshots reflect true system output. Fall back to a direct repository path only
    if the API isn't running, behind the same guard.
  6. Determinism — timestamps are computed as offsets from a single reference
    instant passed in (--ref=<iso>, default a fixed date) so re-running yields
    identical relative ages ("2 days ago"), and screenshots are reproducible across
    runs and machines. --seed controls any ordering jitter.
  7. Force briefing — after ingest, invoke briefingGeneratorJob({ cadence: 'daily', userIds: [demoUserId] }) directly so /api/twin-briefings/latest
    returns a fully populated digest immediately (no 24h wait).
  8. Teardown--reset deletes only is_demo = true users and their owned rows.
    Cleanup is scoped by the same isolation predicate, so it can never delete real data.
    8b. Idempotent — all writes go through the shared seedUpsert helper from spec 10
    (INSERT ... ON CONFLICT), so re-running without --reset converges to the same
    state rather than erroring on duplicate keys or doubling rows. Combined with --ref
    determinism, run-twice == run-once.
  9. Docs — a short docs/demo-fixture.md (run command, what it creates, the
    safety gates, the screenshot checklist of surfaces to capture). Cite
    demo-guard.ts as the source of truth for the gating, per the CLAUDE.md doc rule.

Acceptance Criteria

  1. pnpm demo:fixture on a local dev DB populates the demo user with a source-varied
    signal set and a generated briefing; /api/twin-briefings/latest returns a digest
    with ≥5 to-dos and ≥6 topic clusters.
  2. The fixture never runs implicitly: booting via bin/skytwin-dev on an empty
    DB creates only the normal lightweight seed, NOT the demo fixture (asserted by a
    test that boots the seed path and checks no is_demo heavy dataset exists).
  3. Onboarding/new-user safe: simulating a brand-new user (empty DB, sign-up flow)
    produces zero demo signals.
  4. assertDemoSafe refuses when NODE_ENV='production' (no override unblocks prod),
    refuses for a non-local DB target without the explicit override + confirmation
    token, and passes for local dev with the command invoked.
  5. Identity isolation: the fixture aborts rather than write to any user with
    is_demo = false; --reset deletes only is_demo = true rows (verified a real
    user row survives a reset).
  6. Re-running with the same --ref and --seed produces an identical dataset
    (deterministic AND idempotent via seedUpsert — no duplicate rows, no PK errors;
    screenshots reproducible).
  7. Signals cover all four sources (email, calendar, filesystem, voice) and exercise
    specs 02/03/04/05/06 (each capability has at least one triggering signal).
  8. All fixture content is synthetic — no real names, companies, amounts, or dates
    (same bar as the rest of the epic).
  9. Tests written and passing. No degradation of existing functionality.

Testing Plan

Layer What Count
Unit assertDemoSafe: prod refuse, non-local refuse, override token, local pass +6
Unit Identity isolation: abort on non-demo target; reset scoped to is_demo +3
Unit Determinism: same --ref/--seed → identical dataset +2
Integration bin/skytwin-dev empty-DB boot creates lightweight seed only, no fixture +1
Integration New-user/onboarding flow produces zero demo signals +1
Integration Full run → digest has ≥5 to-dos, ≥6 clusters, all 4 sources represented +2
Integration --reset removes demo data, leaves a real user row intact +1

Rollback Plan

The command is standalone and opt-in; removing it has zero effect on the running
system (nothing calls it implicitly). The is_demo column is additive and nullable
(real users default false). Roll back by deleting the script + migration; no
production code path depends on it. Demo data is removable any time via --reset.

Effort Estimate

  • demo-guard.ts + is_demo migration: ~3h
  • Synthetic source-varied corpus: ~4h
  • Ingest orchestration + force-briefing: ~3h
  • Determinism + --reset teardown: ~2h
  • Docs + screenshot checklist: ~1h
  • Tests (guard + isolation are the priority): ~4h

Total: ~2 days.

Files Reference

File Change
packages/db/src/seeds/demo-fixture.ts New: orchestrator (guarded)
packages/db/src/seeds/demo-guard.ts New: assertDemoSafe (3-gate)
packages/db/src/seeds/demo-fixtures/signals.ts New: synthetic source-varied corpus
packages/db/src/migrations/0NN-users-is-demo.sql New: is_demo column
packages/db/package.json + root package.json demo:fixture script
apps/worker/src/jobs/briefing-generator.ts Reference (force-invoke for demo)
bin/skytwin-dev Reference — confirm fixture is NOT added to boot/seed path
docs/demo-fixture.md New: run + safety + screenshot checklist

Out of Scope

  • Automated screenshot capture (Playwright). This spec makes the data; capturing is a
    follow-up (could use the /qa or browser tooling).
  • A production "demo account" feature for prospects. This is a local launch/testing
    fixture, not a hosted sandbox.
  • Generating media (avatars, real attachments). Text signals only.

Related

  • Populates the surfaces built by specs 01-08; pairs with 08 (UI) for screenshots.
  • Reuses the demo-user/env-gating precedent in apps/api/src/routes/demo.ts.
  • Synthetic-content bar matches the whole epic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions