Plan: Demo Dataset Seed Profile

On this page

Status

Step Description Status

1

Baseline orphan-ref audit against current cargo xtask seed

Done (2026-05-26) — docs/modules/ROOT/pages/plans/demo-dataset-baselines/random-seed-baseline-2026-05-26.txt (71 orphans / 300 rows on random seed)

2

Curate 24 persona archetypes + program-cohort matrix

Done (2026-05-26) — tools/canopy-seed/src/demo/personas.rs, 401 instances across 24 archetypes, narrative list confirmed by user before SQL generation

3

Implement demo-profile generator (tools/canopy-seed/src/demo/)

Done (2026-05-26) — orchestrator (generate.rs) + 8 phases (persons, applications, eligibility, verification, renewals, notices, wic-appointments, appeals + IPV). Net-new writers for verifications/ievs_hits/wic_appointments in demo/sql_extras.rs. Program-service row deferral: SNAP/TANF/Medicaid/CAPS/WIC program-service-local tables (snap_applications, tanf_*, medicaid_*, caps_authorizations, wic_participants) deferred to a follow-up MR — they’re not in the 10 verifier checks, and the worker portal queries through `canopy-eligibility’s combined view for cases-search, so the demo flow renders correctly without them. Filed as follow-up #579.

4

Commit generated .sql to devstack/demo-dataset/ + CI drift gate

Done (2026-05-26) — 13 SQL files committed under devstack/demo-dataset/; cargo xtask demo check-drift re-runs the generator into a tempdir and `diff`s against committed output.

5

Wire cargo xtask seed --profile demo (default behavior unchanged)

Done in MR-a (!376 / db421ff)

6

Wire cargo xtask demo verify cross-service ref checker

Done in MR-a (!376 / db421ff); MR-b proves all 10 checks pass on the curated dataset (0 orphans / 1,863 rows).

7

E2E coherence spec + Playwright multi-size sweep against demo profile

Not started (MR-c)

8

Runbook + reviewer sign-off

Not started (MR-c)

9

Workflow integrity (MR-d): 9c cases-search status fix → 9a Run Determination → 9e Action ▾ dropdown → 9d Pending Hearings → 9b chain-through banners

Done (2026-05-26) — sub-steps 9c (!377 + !378), 9a, 9e, 9d, 9b shipped together.

MR-d sub-step ordering (ratified 2026-05-26): 9c → 9a → 9e → 9d → 9b. Critical-path first (9c fixes the most visible regression: archetypes 4–7 showing "Active" everywhere); 9a establishes the Run Determination primitive that 9b banners reference; 9e is small; 9d is backend-heavy; 9b banners land last so they cite an already-built button.

Epic: none (one-shot pre-UAT demo support)
Issues: this plan; follow-ups #574 (ADR-025 rollout to remaining 10 services), #575 (canopy-medicaid CMD auto-cascade), #576 (change-report auto-redetermination), #577 (random-seed orphan-ref fixes)
Branches (four-MR slice, ratified 2026-05-26):

  • feat/demo-dataset-mr-a-plumbing — Steps 1 + 5 + 6 (verifier skeleton, --profile flag, DATABASES const expansion). No dataset yet; baseline orphan-count captured against random-seed default.

  • feat/demo-dataset-mr-b-data — Steps 2 + 3 + 4 (24 archetypes, generator, committed SQL, drift-gate CI job).

  • feat/demo-dataset-mr-c-specs — Steps 7 + 8 (E2E coherence spec, multi-size screenshot spec, runbook).

  • feat/demo-dataset-mr-d-workflow-integrity — Step 9 (5 sub-tasks: 9a Run Determination action, 9b chain-through banners on income/change-report/CMD/IEVS-resolve, 9c auto-issuance subscriber + cases-search status fix, 9d Pending Hearings backend endpoint, 9e Action ▾ dropdown wired with 4 common actions).

Each MR is independently reviewable. Demo-day risk localised to MR-b + MR-d. MR-d sub-steps 9a + 9c are the critical path (without them archetypes 4-7 show "Active" everywhere); 9b + 9d + 9e are polish that can land in a same-day follow-up commit if MR-d slips. Per the 2026-05-26 user directive ("if we surface buttons/views, they should work"), no demo-day workarounds are accepted — the dead "Action ▾" button and the 5 partial/broken workflows all get real fixes, not hides.

Context

A stakeholder demo is scheduled for ~2026-05-28. Today, cargo xtask seed runs tools/canopy-seed with a random RNG seed and writes ~50 households across 11 service databases via raw INSERT statements (no API validation, no cross-service consistency check). The output is adequate for E2E test fixtures — the same seed reliably produces a working golden path — but it is inadequate for a demo for three discrete reasons:

  1. New dashboard tables are unpopulated. canopy-verification.verifications, canopy-verification.ievs_hits, and canopy-wic.wic_appointments are the data sources for the four worker-dashboard feeds #519, #520, #521, #522, #523 (see services.md Feature Status). The current generator writes only into the canopy-snap-side IEVS tables (canopy_snap.ievs_match_results) and skips the canopy-verification tables and wic_appointments entirely. Result: those panels render empty for any seed.

  2. Click-through reveals orphan refs. A row in a dashboard panel typically encodes (case_id, household_id, person_id). The case-detail page (canopy-web /cases/{id}) loads the household from canopy-persons and the determination from the program service. Because the random generator picks IDs phase-by-phase and inter-phase wiring is partial (notices reference determinations that may not exist; some appeals reference notices that weren’t generated), clicking through from a panel reveals empty household lists, missing determinations, or 404`s. The user’s auto-memory `project_demo_dataset_next_session.md documents this concretely.

  3. Random data has no narrative. A demo audience benefits from recognisable archetypes (e.g., "Sarah Johnson, single mom of two, SNAP-expedited, also on Medicaid, has an IEVS discrepancy"). A random distribution of synthetic names has no such hooks and forces the demo presenter to improvise.

The cross-service validator (ADR-025, merged 2026-05-26 as !374) closes one half of the long-term gap by rejecting orphan refs at the HTTP boundary. But it only protects the canopy-verification adopter today, and even when it covers all 11 services it does not fix data that’s already been INSERTed below the API layer — which is what xtask seed does. The demo dataset operates below the validator, so it must mint internally-consistent IDs by construction.

This plan delivers a hand-curated dataset that is invoked only by explicit flag (--profile demo), leaves the existing random-seed flow unchanged for E2E tests, and is verified against a cross-service-ref join script before commit. The deliverable is one-shot — a frozen, auditable artefact — rather than ongoing infrastructure.

Scope

In scope:

  • 24 hand-curated persona archetypes, each instantiated 5–25× to give ~400 unique households across 13 service databases.

  • Per-program target cohort sizes: SNAP ~200 determinations, TANF ~100, Medicaid ~250 (across multiple COAs), CHIP ~50, CAPS ~100 authorizations, WIC ~125 participants. Cross-program enrolment is encouraged — a single household can carry SNAP + Medicaid + CAPS, counted in each program’s panel.

  • Coverage of every dashboard-feed table: canopy_verification.{verifications, ievs_hits}, canopy_wic.wic_appointments, canopy_renewals.snap_certifications with overdue rows, canopy_eligibility.program_determinations with cross-program-alert-eligible statuses, canopy_appeals.appeal_requests at varied hearing stages.

  • Deterministic generator (tools/canopy-seed/src/demo/) producing one .sql file per service DB.

  • Committed .sql output at devstack/demo-dataset/*.sql + CI drift gate (cargo xtask demo check-drift).

  • cargo xtask seed --profile demo flag that loads committed SQL via the existing psql-pipe loader.

  • cargo xtask demo verify cross-service-ref auditor: joins across every (service A, service B) FK boundary documented in Cross-service ref ledger below; exits non-zero on any orphan.

  • New E2E spec tests/e2e/specs/demo-dataset-coherence.spec.ts that walks every dashboard panel + clicks one row into case-detail + asserts non-empty render.

  • Per-program-cohort Playwright multi-size screenshot sweep, mirroring the existing multi-size-screenshots.spec.ts but against the demo profile.

  • Runbook docs/modules/ROOT/pages/runbooks/demo-dataset.adoc documenting how to load + reset the demo data.

Out of scope:

  • Fixing the random-seed generator’s orphan-ref bugs (separate concern; would land in canopy-seed datagen.rs per a different plan).

  • Replacing cargo xtask seed’s default behavior (E2E tests rely on random-seed determinism via `--seed).

  • Production-grade PII or realistic statistical distributions — this is a demo, not benchmark data.

  • Rolling ADR-025 validators out to the remaining 10 services (tracked separately; see pending followup in this session’s memory).

  • Federal-reporting fixture rows (CMS-64, CMS-416, T-MSIS, FNS-388, ACF-199) — these read from already-populated determination tables, so the existing reporting pipelines will produce CSVs from demo data without dataset-side help.

  • Migrating the dataset to API-driven seeding (per ADR-025 spirit). Recorded as a follow-up; out of scope under the 2-day deadline.

Design

Two-layer architecture: generator + committed output

The dataset is produced by a deterministic Rust generator and the generator’s output is also committed to the repo. Both are first-class artefacts.

  • Generator (tools/canopy-seed/src/demo/, invoked by tools/canopy-seed/src/bin/demo.rs): single source of truth for content. Defines personas, mints UUIDs from a fixed RNG seed (0xDE40_DA7A_5EED), and renders SQL via the existing tools/canopy-seed/src/sql.rs writers. Byte-identical re-runs require two pins, not one: the fixed RNG seed (UUIDs) and a fixed reference date — DEMO_REFERENCE_DATE (demo/mod.rs), which replaced Utc::now() so emitted dates no longer churn daily (Plan 4 G6). The non-sql.rs tables (canopy_tanf, canopy_medicaid, and the appended persona/ELE-consent supplements) are rendered by tools/canopy-seed/src/demo/sql_extras.rs; in particular the Express Lane Eligibility ele_consents rows are emitted by render_ele_consents_supplement (they were once hand-edited into the committed canopy_medicaid.sql, which demo regenerate would silently drop).

  • Committed SQL (devstack/demo-dataset/{canopy_persons,canopy_applications,…}.sql, 13 files): the artefact cargo xtask seed --profile demo actually loads. Human-reviewable. Frozen between regenerations.

  • Drift gate (cargo xtask demo check-drift): in CI, re-runs the generator into a temporary directory and diff`s against the committed `.sql. Fails if they diverge. Forces every content change to land as a regenerated commit.

Why both? The committed .sql is what gets loaded into Postgres — auditable line-by-line. The generator is what gets edited when a persona narrative changes or a schema migration renames a column. The drift gate keeps them honest.

Persona archetypes

Each archetype is a Rust struct (tools/canopy-seed/src/demo/personas.rs) with the following shape:

struct Archetype {
    slug: &'static str,                  // e.g. "snap-expedited-single-mom"
    narrative: &'static str,             // human-readable summary surfaced in case-detail header
    program_enrolments: &'static [Program],
    household_shape: HouseholdShape,     // size, ages, relationships
    cohort_size: usize,                  // number of instances to mint
    determination_status: DeterminationStatus, // canopy_reference::DeterminationStatus
    extras: ArchetypeExtras,
}

/// Per-archetype panel-targeting hooks. Each field is None unless the
/// archetype's narrative requires that panel to render a row.
/// Phases consult these fields to stamp the right dates / statuses /
/// linked records.
struct ArchetypeExtras {
    /// Stamps `snap_certifications.certification_end_date` to a date in
    /// the past (overdue) or near future. Drives #520 overdue-cases.
    snap_certification_end_offset_days: Option<i32>,
    /// Stamps `snap_certifications.interim_contact_due_date`. Drives
    /// the second branch of #520.
    interim_contact_due_offset_days: Option<i32>,
    /// Mints a `verifications` row with `status = 'pending'` for the
    /// archetype's HoH. Drives #519 pending-verifications.
    pending_verification: Option<VerificationType>,
    /// Mints an `ievs_hits` row with `status = 'unreviewed'`. Drives
    /// #522 ievs-discrepancies.
    ievs_discrepancy: Option<IevsHitFlavor>,
    /// Mints a `wic_appointments` row with `appointment_at` in the
    /// next N days. Drives #521 wic-upcoming-appointments.
    wic_appointment_offset_days: Option<i32>,
    /// Mints an `appeal_requests` row at the named stage. Drives
    /// appeals panel + cross-program-alerts.
    appeal_stage: Option<AppealStage>,
    /// Mints an `ipv_cases` row. Drives IPV slice of appeals panel.
    ipv_status: Option<IpvStatus>,
    /// Overrides the determination's denial reason — used for
    /// archetype 4 (Denied — over income) to display a real reason.
    denial_reason_code: Option<&'static str>,
    /// For archetype 7 (ABAWD), the month count to stamp into
    /// `abawd_tracking.month_count`. None = no ABAWD row written.
    abawd_month_count: Option<i32>,
}

Enum variants for VerificationType, IevsHitFlavor, AppealStage, IpvStatus are added to tools/canopy-seed/src/demo/mod.rs and mirror the existing canopy-reference reference types where they exist. Each variant maps directly to a literal string the SQL writer stamps into the corresponding status / type column.

The 24 initial archetypes (final list to be confirmed in Step 2; this is a working draft):

# Archetype Cohort size Primary panel(s) it populates

1

SNAP Active — single-adult employed

25

cases-search, recent-applications

2

SNAP Active — single mom, expedited

20

cases-search

3

SNAP Pending — verification outstanding

20

pending-verifications (#519)

4

SNAP Denied — over income

10

recent-determinations

5

SNAP Sanctioned — failed work requirement

10

cross-program-alerts (#523)

6

SNAP Terminated — moved out of state

10

cross-program-alerts (#523)

7

SNAP ABAWD — exceeded 3-month clock

8

cross-program-alerts (#523)

8

SNAP At-Renewal — cert ends in 5 days

15

overdue-cases (#520)

9

SNAP IEVS-Discrepancy — unmatched income

12

ievs-discrepancies (#522)

10

TANF Active — 2-parent

20

cases-search

11

TANF Sanctioned

10

cross-program-alerts (#523)

12

TANF Time-Limit-Exceeded

8

cross-program-alerts (#523)

13

Medicaid MAGI Pregnant Woman

25

cases-search

14

Medicaid Adult Group

25

cases-search

15

Medicaid SSI-related ABD

20

cases-search

16

Medicaid MN Spenddown

15

cases-search

17

CHIP — child eligible

30

cases-search

18

CAPS — authorized in-care

25

cases-search

19

CAPS — waitlist

10

cases-search

20

WIC — certified, upcoming appointment

20

upcoming-appointments (#521)

21

WIC — recert due in 14 days

15

upcoming-appointments (#521)

22

Cross-program: SNAP + Medicaid + CAPS narrative

25

cases-search (all 3 panels)

23

Fair hearing — scheduled

15

appeals

24

IPV — under investigation

8

appeals

Total: ~400 archetype instances. Many appear in multiple panels (an archetype 22 cross-program household renders one row in each of three panels).

UUID strategy

The demo generator uses a separate RNG seed (0xDE40_DA7A_5EED) from the random-seed default (rand::random). This ensures:

  • Demo UUIDs are recognisably distinct from random-seed UUIDs (their v7 timestamp prefixes diverge by years).

  • cargo xtask seed (no flag) and cargo xtask seed --profile demo cannot accidentally produce overlapping data.

  • Identical demo seed → identical UUIDs → committed .sql is reproducible.

UUID assignment per archetype follows the existing DeterministicUuidGenerator pattern in tools/canopy-seed/src/uuid.rs (124 lines, single struct with seeded next() method that mints UUIDv7-shaped IDs from BASE_EPOCH_MS = 1_704_067_200_000 + per-call counter_ms increment). The demo generator constructs one DeterministicUuidGenerator instance, seeds it with 0xDE40_DA7A_5EED, and mints all IDs through it. No new UUID infrastructure is required.

Every cross-service reference flows from a single UUID minting site. A household_id minted in the persons-phase is then read (not re-minted) by every downstream phase that wants to reference that household. This eliminates the orphan-ref class of bug by construction.

Per-service-DB content map

For each service DB, the dataset must contain:

Table names below come from each service’s migrations/ directory (verified at plan time). The demo generator MUST write rows into every entry; the verifier (Step 6) joins through the cross-service-ref ledger above.

Service DB Tables seeded

canopy_persons

households, household_members, persons, addresses, income, assets, expenses

canopy_applications

applications, application_programs, authorized_representatives, household_assignments

canopy_eligibility

eligibility_requests, program_determinations, combined_results

canopy_verification

verifications, ievs_hits (net-new vs existing canopy-seed coverage — source of #519 + #522)

canopy_enrollment

snap_enrollments, snap_benefit_issuances

canopy_renewals

snap_certifications, snap_change_reports (source of #520 overdue rows)

canopy_notices

notices, notice_appeal_rights

canopy_appeals

appeal_requests, appeal_timeline_events, ipv_cases, ipv_timeline_events

canopy_snap

snap_applications, snap_determinations, ievs_match_results, ievs_discrepancies, ievs_verification_data, snap_program_participations, snap_student_status, snap_disqualification_screenings, citizenship_verifications, abawd_tracking, abawd_monthly_activity, abawd_waiver_areas, abawd_time_clock, abawd_discretionary_exemptions, abawd_discretionary_exemption_grants, snap_tsnap_certifications (existing seed covers the headline subset; demo enforces the full list above so cohort-level joins resolve)

canopy_tanf

tanf_applications, tanf_determinations, tanf_household_snapshots, tanf_income, tanf_work_requirements, tanf_work_activities, tanf_time_limits, tanf_lump_sum_periods, tanf_grg_payments, tanf_personal_responsibilities, tanf_discrepancies (net-new vs existing canopy-seed)

canopy_medicaid

medicaid_applications, medicaid_determinations, magi_household_snapshots, magi_income, non_magi_factors, medicaid_eligible_categories, chip_applications, peachcare_premium_schedule, clinical_assessments, tanf_tma_coverage (net-new vs existing canopy-seed; sanction lifecycle was added as columns on tanf_work_requirements per 20260511000000_add_sanction_lifecycle.sql, not a separate table — generator stamps the columns rather than creating a row)

canopy_caps

caps_applications, caps_determinations, caps_authorizations, caps_providers

canopy_wic

wic_determinations, wic_participants, wic_nutritional_risk_assessments, wic_appointments (wic_appointments is net-new vs existing canopy-seed — source of #521)

canopy_reporting

NOT seeded — federal-reporting pipelines aggregate from already-populated determination tables. Demo coverage is via downstream queries, not seeded rows.

canopy_security

NOT seeded by demo — audit chain is initialised on service boot and appended at runtime. Loading static audit rows would break the hash chain (ADR-014).

canopy_rules

NOT seeded by demo — rulesets are loaded at service startup from rulesets/ files.

Tables not enumerated above are intentionally excluded. event_outbox / event_inbox tables in each service exist for at-runtime event dispatch and are populated by handlers, not seed data. fti_audit_log / fti_audit_log_archive carry the FTI hash chain (ADR-014) — loading static rows would break verification; demo skips these. fti_tax_data, ssa_match_results, fdsh_results, express_lane_evaluations, pathways_qualifying_activities, pathways_hipp_referrals, cmd_cascade_log, medicaid_cmd_events, overpayment_claims / repayment_plans / recoupment_ledger (across SNAP/TANF/Medicaid): tracked as a follow-up — the demo dataset adds them only if a dashboard panel reads from them; today none do.

Cross-service ref ledger

The verifier (Step 6, cargo xtask demo verify) walks this exact list. Every (source, target) pair is a join the verifier runs across DBs; any missing target is an orphan and fails verify.

Source row Target row Lookup direction

canopy_applications.applications.household_id

canopy_persons.households.id

application → household

canopy_applications.applications.submitted_by

canopy_persons.persons.id (when submitted_by_role = 'applicant')

application → submitter

canopy_eligibility.program_determinations.household_id

canopy_persons.households.id

determination → household

canopy_eligibility.program_determinations.application_id

canopy_applications.applications.id

determination → application

canopy_verification.verifications.household_id

canopy_persons.households.id

verification → household

canopy_verification.verifications.application_id

canopy_applications.applications.id (when non-NULL)

verification → application

canopy_verification.verifications.person_id

canopy_persons.persons.id (when non-NULL)

verification → person

canopy_verification.ievs_hits.household_id

canopy_persons.households.id

ievs_hit → household

canopy_verification.ievs_hits.person_id

canopy_persons.persons.id

ievs_hit → person

canopy_renewals.snap_certifications.household_id

canopy_persons.households.id

certification → household

canopy_renewals.snap_certifications.application_id

canopy_applications.applications.id

certification → application

canopy_renewals.snap_certifications.determination_id

canopy_snap.snap_determinations.id

certification → determination

canopy_notices.notices.household_id

canopy_persons.households.id

notice → household

canopy_notices.notices.recipient_person_id

canopy_persons.persons.id

notice → person

canopy_notices.notices.application_id

canopy_applications.applications.id (when non-NULL)

notice → application

canopy_appeals.appeal_requests.household_id

canopy_persons.households.id

appeal → household

canopy_appeals.appeal_requests.requestor_person_id

canopy_persons.persons.id

appeal → requestor

canopy_appeals.appeal_requests.determination_id

program-service *_determinations.id (one of the 5 program DBs)

appeal → determination

canopy_appeals.appeal_requests.notice_id

canopy_notices.notices.id (when non-NULL)

appeal → notice

canopy_appeals.ipv_cases.household_id

canopy_persons.households.id

ipv → household

canopy_appeals.ipv_cases.person_id

canopy_persons.persons.id

ipv → person

canopy_snap.snap_applications.household_id

canopy_persons.households.id

snap_app → household

canopy_snap.snap_applications.application_id

canopy_applications.applications.id

snap_app → application

canopy_snap.snap_determinations.household_id

canopy_persons.households.id

snap_det → household

canopy_tanf.tanf_applications.household_id

canopy_persons.households.id

tanf_app → household

canopy_tanf.tanf_determinations.household_id

canopy_persons.households.id

tanf_det → household

canopy_medicaid.medicaid_applications.household_id

canopy_persons.households.id

medicaid_app → household

canopy_medicaid.medicaid_determinations.household_id

canopy_persons.households.id

medicaid_det → household

canopy_caps.caps_applications.household_id

canopy_persons.households.id

caps_app → household

canopy_caps.caps_authorizations.child_person_id

canopy_persons.persons.id

caps_auth → child

canopy_caps.caps_authorizations.provider_id

canopy_caps.caps_providers.id (intra-DB FK enforced by schema)

caps_auth → provider

canopy_wic.wic_determinations.household_id

canopy_persons.households.id

wic_det → household

canopy_wic.wic_participants.person_id

canopy_persons.persons.id

wic_part → person

canopy_wic.wic_appointments.household_id

canopy_persons.households.id

wic_appt → household

Schema notes captured at plan time (verify before implementation; migrations may have moved):

  • canopy_persons.households has no head_of_household_id column. Head-of-household is identified via household_members.relationship = 'head_of_household' (existing seed convention — see tools/canopy-seed/src/datagen.rs:490). The demo phase that mints applications stamps applications.submitted_by = <HoH person_id> per the same convention.

  • canopy_wic.wic_appointments (services/canopy-wic/migrations/20260512000000_create_wic_appointments.sql) has only household_id + certification_id as ID columns. certification_id is UUID with no declared FK and is not enforced — the demo treats it as denormalized metadata, not a verifier-tracked ref. Adding a participant_id column is a separate schema change out of this plan’s scope.

  • canopy_caps.caps_authorizations.provider_id is UUID NOT NULL REFERENCES caps_providers(id) (post-#396). Intra-DB FK is enforced by Postgres at INSERT time — the verifier still walks it for cohort-level completeness ("did we generate enough providers?") but a literal orphan will surface as a load error, not a silent verify failure.

This is the closed list. If a new dashboard feed lands that adds a cross-service ref, both the dataset and the verifier must be updated together — the verifier’s job is to catch ref drift, not silently pass.

CLI surface

xtask seed grows one new optional flag. Existing usage is unchanged.

cargo xtask seed                         # Random-seed default, unchanged
cargo xtask seed --seed 12345            # Replay specific seed, unchanged
cargo xtask seed --profile demo          # NEW: load committed devstack/demo-dataset/*.sql
cargo xtask seed --profile demo --reset  # NEW: TRUNCATE all tables first, then load

Two new subcommands under a new xtask demo namespace:

cargo xtask demo regenerate              # Re-run generator → write devstack/demo-dataset/*.sql
cargo xtask demo verify                  # Run cross-service-ref auditor against running devstack
cargo xtask demo check-drift             # CI gate: re-run generator into tempdir, diff vs commit

--profile demo skips the generator entirely and only copies/pipes the committed SQL. This is the only path used at demo time — generation is a developer-side activity.

Persona ordering and TRUNCATE strategy

cargo xtask seed --profile demo --reset issues TRUNCATE … RESTART IDENTITY CASCADE against every table in Cross-service ref ledger order (deepest dependent first) before loading. Without --reset, the loader assumes empty tables and proceeds; UNIQUE-constraint violations on re-runs surface as load errors rather than silently corrupting data.

The committed SQL files use explicit INSERT … ON CONFLICT DO NOTHING only on stable seed tables (reference data: programs, jurisdictions). Domain inserts (households, determinations, etc.) use plain INSERT so re-loading without --reset fails loudly.

Drift gate vs review burden

The drift gate fires whenever the generator and committed SQL disagree. Schema migrations that change the demo’s table set require two commits: (a) the migration itself, (b) a cargo xtask demo regenerate regeneration commit. A pre-push hook check is not added in this plan — the CI gate is sufficient.

Steps

Step 1: Baseline orphan-ref audit

Files: xtask/src/cmd/demo.rs (new), invoked against the existing random-seed seed.

Implement the demo verify skeleton first (the join-walker, before the dataset exists). Run it against current cargo xtask seed output. Record the orphan counts as a "before" baseline in the plan’s Verification section. This step is the empirical justification for the work — without it, "the dataset has orphan refs" is a claim, not a measurement.

The verifier is a stand-alone script:

struct OrphanCheck {
    name: &'static str,
    source_db: &'static str,
    source_query: &'static str,   // SELECT id, target_id FROM source_table WHERE target_id IS NOT NULL
    target_db: &'static str,
    target_query: &'static str,   // SELECT id FROM target_table
}

fn run(check: &OrphanCheck, source_pool: &PgPool, target_pool: &PgPool) -> Result<Vec<Uuid>>;

Each OrphanCheck corresponds to one row of the cross-service-refs ledger. Outputs a report grouped by source row, listing missing target IDs. Exit code 0 if all clean, 1 if any orphan found.

Step 2: Persona archetype curation

Files: tools/canopy-seed/src/demo/personas.rs (new — ~600 lines for 24 archetypes), tools/canopy-seed/src/demo/names.rs (curated name lists), tools/canopy-seed/src/demo/addresses.rs (Georgia ZIPs spread across counties).

Define each archetype with its narrative, program enrolments, household shape, cohort size, and the dashboard panels it must populate. The cohort-size column above is the working starting point; final numbers tuned in this step against the panel-coverage requirements.

Critical detail: each archetype also declares what extras it carries. E.g., archetype 8 (At-Renewal) sets extras.snap_certification_end_date = today + 5 days. The phase that writes snap_certifications reads the archetype’s extras and stamps the right date. This is the mechanism that makes panel-specific data deterministic without bolting "magic households" onto every panel by hand.

Step 3: Generator implementation

Files: tools/canopy-seed/src/demo/mod.rs (entry), tools/canopy-seed/src/demo/generate.rs (orchestrator), tools/canopy-seed/src/bin/demo.rs (binary), plus phase-specific modules:

  • demo/persons_phase.rs — mints persons + households for each archetype instance

  • demo/applications_phase.rs — mints applications, links to household + HoH

  • demo/eligibility_phase.rs — writes program_determinations + combined_results for cross-program-alert coverage

  • demo/verification_phase.rsNEW vs existing canopy-seed: writes canopy_verification.verifications + ievs_hits for #519 + #522 panel rows

  • demo/renewals_phase.rs — writes snap_certifications with overdue dates for #520

  • demo/notices_phase.rs — writes notices linked to determinations + households

  • demo/appeals_phase.rs — writes appeal_requests + IPV cases

  • demo/snap_phase.rs, demo/tanf_phase.rs, demo/medicaid_phase.rs, demo/caps_phase.rs, demo/wic_phase.rs — per-program tables. canopy-tanf and canopy-medicaid are net-new vs existing canopy-seed coverage.

  • demo/wic_appointments_phase.rsNEW: writes wic_appointments for #521

The orchestrator wires phases sequentially in dependency order (persons → applications → determinations → notices → appeals). Each phase receives a &DemoContext (the cross-cutting UUID + persona registry) and returns row collections that the SQL writer consumes.

Reuse tools/canopy-seed/src/sql.rs’s existing per-table writers wherever the demo phase writes into a table the random-seed generator also covers. Net-new tables (`verifications, ievs_hits, wic_appointments, TANF/Medicaid tables) get new writer functions in sql.rs.

Step 4: Commit generated SQL + CI drift gate

Files: devstack/demo-dataset/*.sql (13 files, generated), xtask/src/cmd/demo.rs (check-drift subcommand), .gitlab-ci.yml (new job).

After Step 3 produces a clean run, commit the output. The drift-gate CI job:

demo-dataset-drift:
  stage: validate
  script:
    - cargo xtask demo check-drift
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

Tempdir output is diffed against committed; non-zero exit fails the job. Pipeline message: "demo dataset is stale — run cargo xtask demo regenerate and commit the result."

Step 5: Wire --profile demo into xtask seed

Files: xtask/src/cmd/seed.rs (modify).

Add a --profile flag (variant enum: Default | Demo) and an independent --reset flag. The combinations are all legal:

Invocation Behavior

cargo xtask seed

Random-seed default (today’s behavior, unchanged)

cargo xtask seed --seed 12345

Replay specific seed, unchanged

cargo xtask seed --reset

TRUNCATE then run random-seed default

cargo xtask seed --profile demo

Load devstack/demo-dataset/*.sql without TRUNCATE — assumes empty DBs

cargo xtask seed --profile demo --reset

TRUNCATE then load committed demo SQL (the canonical demo invocation)

Implementation in xtask/src/cmd/seed.rs:

  1. Add --profile demo branch before the cargo run -p canopy-seed invocation. When demo, skip the canopy-seed generator call entirely and set output_dir to <repo-root>/devstack/demo-dataset/.

  2. Add --reset branch: issue per-table TRUNCATE … RESTART IDENTITY CASCADE against every table in the cross-service-refs ledger source list before the load loop. Order: deepest-dependent first (appeals → notices → renewals → verification → eligibility → applications → persons, then program services last).

  3. Expand the DATABASES const (currently xtask/src/cmd/seed.rs:73-87) to include canopy_verification, canopy_tanf, and canopy_medicaid. Without this expansion the demo-profile loader silently skips those .sql files and the verifier (Step 6) reports orphans on every row touching those services. This expansion is required for both profiles (the random-seed default also benefits if/when those services get seeded later).

  4. The container-routing function container_for_db (lines 94-110) already covers all 13 DBs through its per-program / shared-postgres branching — no change required there.

Step 6: Cross-service-ref verifier

Files: xtask/src/cmd/demo.rs (extend Step 1’s skeleton).

Already drafted in Step 1. After the dataset exists, this command’s primary use is asserting demo-profile coherence. Run as the final gate in `--profile demo’s success path:

cargo xtask seed --profile demo && cargo xtask demo verify

Exit-code 0 from verify is the demo-readiness signal.

Step 7: E2E coherence spec

Files: tests/e2e/specs/demo-dataset-coherence.spec.ts (new), playwright.config.ts (new demo-dataset project).

The spec uses the existing seed-replay flow but pinned to demo profile:

  1. beforeAll: cargo xtask seed --profile demo --reset via tests/e2e/lib/seed-loader.ts.

  2. For each of the 9 dashboard panels (cases-search, applications-list, notices-list, appeals-list, renewals-queue, cross-program-alerts, overdue-cases, ievs-discrepancies, upcoming-appointments, pending-verifications): assert ≥5 rows visible.

  3. For each panel: click the first row; assert case-detail page loads with non-empty household-members list, non-empty determination summary, no 404/empty state markers.

  4. The spec runs as its own Playwright project (demo-dataset) — not added to the default test matrix. Invocation: cargo xtask e2e — --project=demo-dataset.

A second new spec, tests/e2e/specs/demo-dataset-multi-size.spec.ts, mirrors the existing multi-size-screenshots.spec.ts across the same 9 panels for the demo profile. Output goes to test-results/screenshots/demo-dataset/.

Step 9: Case-detail workflow integrity (MR-d)

Three audits on 2026-05-26 (Explore agents) confirmed the demo’s worker-portal click paths almost flow:

  • Hops 3–6 work on main: rules engine → determination event → notice subscriber → Typst render → S3 → case-detail Notices tab → PDF download.

  • 48 of 50 click surfaces (per the surface-level audit) wire correctly and produce visible effects.

  • 5 of 10 multi-endpoint workflows (per the chain audit) are partial or broken — every breakage shares the same shape: a recording action saves data but does not auto-trigger re-determination.

MR-d absorbs four related fixes under one branch (feat/demo-dataset-mr-d-case-detail-action) because the user-visible demo narrative ("click Run Determination → see denial + NOA inline") is the unifying primitive. Each sub-step is independently verifiable.

Step 9a: Run Determination handler + button

Files: services/canopy-web/src/cases.rs (new handler), services/canopy-web/templates/cases/tab_determination.html (modify), services/canopy-web/tests/case_detail_run_determination_test.rs (new test).

  • New handler POST /cases/{application_id}/determine reads the case’s household_id + application_id, then calls canopy-eligibility /v1/eligibility/determine (same endpoint approve_application already hits at services/canopy-web/src/api/applications.rs:459).

  • Updated tab_determination.html adds an htmx form with a single Run Determination button. Response swaps the determination tab fragment inline showing the new status + denial reasons + benefit amount + a banner pointing the worker at the Notices tab where the auto-generated NOA appears within ~1s.

  • For demo narrative coherence, archetype 4 (SNAP Denied — over income) starts in pending_review so the presenter produces the denial live rather than seeing a pre-baked one.

Step 9b: Chain-through banners (income / change-report / CMD ingest)

Files: services/canopy-web/templates/cases/tab_income.html, tab_renewals.html, tab_determination_medicaid.html, services/canopy-web/src/api/income.rs, actions.rs, actions_medicaid.rs.

Each recording action’s success response now includes an inline <div class="u-banner u-banner-info"> with text "Saved. Eligibility may have changed — click Run Determination on the Determination tab to recompute." and an htmx-attribute that auto-scrolls to the determination tab.

Workflows affected (all four currently leave the worker on stale data):

  1. Income edit / add / remove — by-design no-auto-re-eval per income.rs:7-11; the banner makes the next step explicit instead of implicit.

  2. Change report submissioncanopy-renewals already sets requires_redetermination = true at services/canopy-renewals/src/api/mod.rs:418; the banner closes the loop until the cascade is wired in a separate plan.

  3. Medicaid CMD ingestcanopy-medicaid records the event but does not invoke cmd_cascade (the module exists at services/canopy-medicaid/src/cmd_cascade.rs but is unreferenced from cmd_handlers.rs:78). The banner is a workaround, not a fix; auto-cascade wiring is tracked in follow-up #575.

  4. Resolve IEVS discrepancy — the resolve POST succeeds in the DB but the row stays visible until full page reload. The htmx form now responds with an empty fragment that targets the discrepancy <tr> for removal — row disappears inline.

Step 9c: Auto-issuance subscriber + cases-search status fix

NOTE

2026-05-26 scope split + closure: Step 9c shipped as two MRs

  • Step 9c-A — cases-search status badges (new /v1/eligibility/case-status endpoint + parallel fetch + 5 colored chips). Merged as !377 / 18a6965 on 2026-05-26.

  • Step 9c-B — auto-issuance subscriber (extends publish_determination_completed with monthly_allotment + effective_date; canopy-enrollment subscriber auto-creates the first snap_benefit_issuances row atomically in the inbox transaction). Closes Workflow 1 from the demo audit.

Both ship the full Step 9c intent; the plan body below is unchanged design narrative.

Files: services/canopy-enrollment/src/main.rs (new event subscriber), services/canopy-enrollment/src/issuance.rs (helper), services/canopy-web/src/api/cases.rs:130 (modify the hardcode).

  • The current approve flow creates an snap_enrollments row via the determination.completed.snap subscriber at services/canopy-enrollment/src/main.rs:161 but never creates the matching snap_benefit_issuances row — that requires a separate POST /v1/enrollments/{id}/issue_benefits call no one in the demo flow makes. Add a downstream subscriber in canopy-enrollment that listens for enrollment.created.snap (emitted by the existing enrollment handler) and auto-creates the first benefit issuance for the proration period. Production-realistic behaviour; closes Workflow 1’s "approve doesn’t issue benefits" gap.

  • Replace the hardcoded status: "Active" rendering at services/canopy-web/src/api/cases.rs:130 with reading the actual determination status from canopy-eligibility.program_determinations.status. Render terminal-state badges: green Active, amber Pending, red Denied / Terminated / Sanctioned, grey Closed. Required for archetypes 4–7 (Denied / Sanctioned / Terminated / ABAWD-exceeded) to display their real lifecycle state on the cases-search page — without this fix every archetype shows "Active" regardless of underlying status, which is the most confusing single demo regression.

Step 9d: Pending Hearings panel backend

Files: services/canopy-appeals/src/api/mod.rs (new endpoint), services/canopy-web/src/api/dashboard.rs (new panel handler), services/canopy-web/templates/dashboard/panels/pending_hearings.html (modify).

  • New endpoint GET /v1/appeals/hearings/upcoming?days={n} on canopy-appeals queries appeal_requests WHERE status = 'hearing_scheduled' AND hearing_date BETWEEN now() AND now() + interval '{n} days' ORDER BY hearing_date. Returns rows with (id, household_id, hearing_date, applicant_name, program, hearing_location).

  • New panel handler in canopy-web matches the existing dashboard-panel handler pattern (e.g., render_overdue_cases_panel at services/canopy-web/src/api/dashboard.rs — exact line TBD by implementer). Calls the new endpoint, passes rows to the existing template.

  • The existing template pending_hearings.html already renders a list shape — confirm the panel renders ≥5 rows from archetype 23 (Fair hearing — scheduled) after the dataset loads.

Step 9e: Wire "Action ▾" dropdown on case-detail

Files (as built): services/canopy-web/templates/case_detail/top_bar_actions.html (new shared partial), services/canopy-web/templates/case_detail/shell{tabs,scroll,card_grid}.html (include the partial via {% block top_bar_actions %}), services/canopy-web/src/case_detail/templates.rs (add csrf_token field to all three shell template structs), services/canopy-web/src/api/case_detail.rs (new request_verification handler), services/canopy-web/src/api/mod.rs (route registration), services/canopy-web/templates/cases/detail.html (deleted — dead since Stage 5 MR4b).

Replace the dead <button>Action ▾</button> placeholder with an Alpine.js dropdown of common case actions. As implemented, the dropdown carries five entries (one more than the plan’s working draft):

  1. Run Determination — htmx POST to the Step 9a handler (POST /cases/{household_id}/run-determination).

  2. Request Verification — opens an inline mini-form that POSTs to a new wrapper handler POST /cases/{household_id}/request-verification in canopy-web, which forwards to canopy-verification’s POST /v1/verifications. (Plan-draft revision: no existing canopy-web handler routed to that endpoint; built one.)

  3. File Appeal — deep-link to the Appeals tab where the existing file_appeal form lives.

  4. Submit Change Report — deep-link to the Renewals tab (replaces the plan’s draft "Request Verification" slot when that one was unreachable; left here as an additional working entry).

  5. Record Interim Contact — deep-link to the Renewals tab.

Deferred per plan directive ("if no handler exists, drop this menu entry and file an issue"):

  • Schedule Interview — no interview/schedule endpoint or interview_scheduled_at column on canopy-applications exists. Filed as follow-up #580.

Each dropdown entry is a single htmx-decorated form so the click cascade matches the existing form behaviour on the Determination tab.

Verification (whole-MR-d)

  1. cargo nextest run -p canopy-web -p canopy-enrollment -p canopy-appeals — new tests pass

  2. Manual against the demo profile:

    1. Open archetype 4 → click Run Determination → status flips to Denied inline → Notices tab shows the auto-generated denial NOA → click NOA → PDF downloads

    2. Open archetype 9 (IEVS discrepancy) → click Resolve on a row → row disappears inline (no full reload)

    3. Open archetype 22 (cross-program) → income tab → edit an income row → see the "click Run Determination" banner → switch to Determination tab → click Run Determination → see updated determination

    4. Open archetype 1 (SNAP Active) → cases-search shows "Active" badge; archetype 4 shows "Denied" badge; archetype 5 shows "Sanctioned" badge

    5. Dashboard → Pending Hearings panel shows ≥5 rows (from archetype 23)

    6. Case detail → click Action ▾ → dropdown lists 4 actions, each routes to a working handler

  3. E2E spec extension in MR-c covers the deny-flow + chain-through narrative.

Step 8: Runbook + reviewer sign-off

Files: docs/modules/ROOT/pages/runbooks/demo-dataset.adoc (new), nav update in docs/modules/ROOT/nav.adoc.

Runbook covers: how to load demo data, how to reset, how to regenerate after a schema change, what each archetype represents (for demo presenters), and the click-through script suggested for the stakeholder demo.

Reviewer sign-off: dispatch a contextless reviewer subagent against this plan before Step 1 begins, per the contextless-implementability quality bar. Sign-off captured as a memory entry; deviations during implementation update the plan’s Design section per ADR-013 precommit Q4/Q6.

Files Touched

File Change

tools/canopy-seed/src/demo/mod.rs

NEW — demo module entry

tools/canopy-seed/src/demo/personas.rs

NEW — 24 archetypes

tools/canopy-seed/src/demo/names.rs

NEW — curated name list

tools/canopy-seed/src/demo/addresses.rs

NEW — GA county/ZIP pool

tools/canopy-seed/src/demo/generate.rs

NEW — orchestrator

tools/canopy-seed/src/demo/persons_phase.rs

NEW

tools/canopy-seed/src/demo/applications_phase.rs

NEW

tools/canopy-seed/src/demo/eligibility_phase.rs

NEW

tools/canopy-seed/src/demo/verification_phase.rs

NEW — fills #519 + #522 source tables

tools/canopy-seed/src/demo/renewals_phase.rs

NEW — fills #520 source table

tools/canopy-seed/src/demo/notices_phase.rs

NEW

tools/canopy-seed/src/demo/appeals_phase.rs

NEW

tools/canopy-seed/src/demo/snap_phase.rs

NEW

tools/canopy-seed/src/demo/tanf_phase.rs

NEW — net-new vs existing seed coverage

tools/canopy-seed/src/demo/medicaid_phase.rs

NEW — net-new vs existing seed coverage

tools/canopy-seed/src/demo/caps_phase.rs

NEW

tools/canopy-seed/src/demo/wic_phase.rs

NEW

tools/canopy-seed/src/demo/wic_appointments_phase.rs

NEW — fills #521 source table

services/canopy-web/src/cases.rs

MODIFIED (MR-d 9a) — new POST /cases/{id}/determine handler

services/canopy-web/templates/cases/tab_determination.html

MODIFIED (MR-d 9a) — Run Determination action form + htmx swap target

services/canopy-web/tests/case_detail_run_determination_test.rs

NEW (MR-d 9a) — handler + integration test

services/canopy-web/templates/cases/tab_income.html

MODIFIED (MR-d 9b) — chain-through banner on income save

services/canopy-web/templates/cases/tab_renewals.html

MODIFIED (MR-d 9b) — chain-through banner on change-report submit

services/canopy-web/templates/cases/tab_determination_medicaid.html

MODIFIED (MR-d 9b) — chain-through banner on CMD ingest

services/canopy-web/src/api/income.rs

MODIFIED (MR-d 9b) — emit banner partial on success

services/canopy-web/src/api/actions.rs

MODIFIED (MR-d 9b) — emit banner + htmx row-swap on resolve_discrepancy

services/canopy-web/src/api/actions_medicaid.rs

MODIFIED (MR-d 9b) — emit banner on CMD ingest

services/canopy-enrollment/src/main.rs

MODIFIED (MR-d 9c) — new enrollment.created.snap subscriber for auto-issuance

services/canopy-enrollment/src/issuance.rs

MODIFIED (MR-d 9c) — auto-issuance helper

services/canopy-enrollment/tests/auto_issuance_test.rs

NEW (MR-d 9c) — subscriber test

services/canopy-web/src/api/cases.rs

MODIFIED (MR-d 9c) — replace hardcoded status: "Active" at line 130 with real determination status; render terminal-state badges

services/canopy-appeals/src/api/mod.rs

MODIFIED (MR-d 9d) — new GET /v1/appeals/hearings/upcoming endpoint

services/canopy-appeals/src/store.rs

MODIFIED (MR-d 9d) — query function for hearings_scheduled

services/canopy-appeals/tests/hearings_upcoming_test.rs

NEW (MR-d 9d) — endpoint test

services/canopy-web/src/api/dashboard.rs

MODIFIED (MR-d 9d) — new render_pending_hearings_panel handler

services/canopy-web/templates/dashboard/panels/pending_hearings.html

MODIFIED (MR-d 9d) — wire to real data shape

services/canopy-web/templates/cases/detail.html

MODIFIED (MR-d 9e) — replace dead Action ▾ button with Alpine dropdown

services/canopy-web/templates/cases/_action_dropdown.html

NEW (MR-d 9e) — dropdown partial with 4 case actions

crates/canopy-contracts-appeals/src/hearings.rs

NEW or MODIFIED (MR-d 9d) — HearingUpcoming row type

tools/canopy-seed/src/bin/demo.rs

NEW — bin entry

tools/canopy-seed/src/sql.rs

MODIFIED — add writers for verifications, ievs_hits, wic_appointments, tanf_*, medicaid_*

tools/canopy-seed/src/lib.rs

MODIFIED — pub mod demo;

tools/canopy-seed/Cargo.toml

MODIFIED — name = "canopy-seed-demo"

devstack/demo-dataset/canopy_persons.sql

NEW — committed generator output

devstack/demo-dataset/canopy_applications.sql

NEW

devstack/demo-dataset/canopy_eligibility.sql

NEW

devstack/demo-dataset/canopy_verification.sql

NEW

devstack/demo-dataset/canopy_enrollment.sql

NEW

devstack/demo-dataset/canopy_renewals.sql

NEW

devstack/demo-dataset/canopy_notices.sql

NEW

devstack/demo-dataset/canopy_appeals.sql

NEW

devstack/demo-dataset/canopy_snap.sql

NEW

devstack/demo-dataset/canopy_tanf.sql

NEW

devstack/demo-dataset/canopy_medicaid.sql

NEW

devstack/demo-dataset/canopy_caps.sql

NEW

devstack/demo-dataset/canopy_wic.sql

NEW

xtask/src/cmd/seed.rs

MODIFIED — --profile + --reset flags

xtask/src/cmd/demo.rs

NEW — verify, regenerate, check-drift subcommands

xtask/src/main.rs

MODIFIED — register demo subcommand

tests/e2e/specs/demo-dataset-coherence.spec.ts

NEW

tests/e2e/specs/demo-dataset-multi-size.spec.ts

NEW

playwright.config.ts

MODIFIED — demo-dataset project

.gitlab-ci.yml

MODIFIED — demo-dataset-drift job

docs/modules/ROOT/pages/runbooks/demo-dataset.adoc

NEW

docs/modules/ROOT/nav.adoc

MODIFIED — link runbook + this plan

Service Catalog

MODIFIED — note the demo profile

CHANGELOG.adoc

MODIFIED — Unreleased entry

Verification

  1. cargo nextest run -p canopy-seed — generator unit tests pass

  2. cargo xtask validate — workspace-wide gate green

  3. cargo xtask dev start

  4. cargo xtask seed --profile demo --reset — loads cleanly

  5. cargo xtask demo verify — exit code 0, zero orphan refs

  6. cargo xtask e2e — --project=demo-dataset — coherence spec passes (all 9 panels render ≥5 rows; each first-row click-through loads case-detail with non-empty data)

  7. Manual: open the worker portal, walk through one instance of each of the 24 archetypes, confirm no 404 or empty state

  8. cargo xtask demo check-drift — committed SQL matches generator output

Baseline measurement (Step 1 output captured before generator implementation): orphan-ref count against current random-seed cargo xtask seed. The final-state target is zero orphans against the demo profile.

Documentation Updates

  • Local Development — note cargo xtask seed --profile demo in the commands

  • CHANGELOG.adoc — entry under == Unreleased mentioning the new profile + verifier

  • docs/modules/ROOT/pages/runbooks/demo-dataset.adoc — full runbook

  • docs/modules/ROOT/nav.adoc — link runbook + this plan

  • docs/modules/ROOT/pages/plans/demo-dataset-seed.adoc Status table — march through Step 1 → 8 as work proceeds

Risks

  1. Schema migration mid-flight invalidates committed SQL. If a per-service migration lands after generation but before demo day, the committed .sql may ERROR against the migrated schema. The canopy-verification first-domain-DB migration (20260526001500_create_verification_tables.sql) landed yesterday — schemas have ~zero days of stabilisation behind them. Mitigation: pre-demo dress rehearsal (Step 7 E2E spec) catches this; the drift-gate CI job catches it at MR time. Honest assessment: if any of {verification, tanf, medicaid, applications, persons} ship a destructive migration in the next 48h, the dataset must be regenerated.

  2. Cohort sizes underweight a panel. If a panel queries with strict filters (e.g., "renewals due in next 7 days only"), the archetype cohort assigned to it may not pass the filter. Mitigation: Step 7 E2E spec asserts ≥5 rows per panel; if it fails, the responsible archetype’s extras are tuned and SQL regenerated.

  3. Cross-service refs grow without verifier updates. A new dashboard feed adds a (service A → service B) FK that the verifier doesn’t know about. Mitigation: the cross-service-refs ledger in Cross-service ref ledger is the closed list; new entries land via plan update + verifier update + regenerated dataset, together.

  4. Demo profile accidentally loads in CI. If a developer runs cargo xtask seed --profile demo and forgets to switch back, E2E specs may fail in confusing ways. Mitigation: default profile is unchanged (default); --profile demo is explicit and surfaces in the seed-output capture file (test-results/seed/last.txt).

  5. Persona narratives reference real-looking names that collide with actual constituents. Mitigation: name list (tools/canopy-seed/src/demo/names.rs) drawn from public-domain US Census top-100 first + surnames with explicit Demo- prefix on case files surfaced in the UI (e.g., "Sarah Johnson — Demo Case 0001").

  6. ADR-025 validator coverage gap is real and unmentioned during the demo. Only canopy-verification enforces cross-service ref validation today (rolled out as !374 on 2026-05-26). The other 10 services (canopy-applications, the 5 program services, renewals, enrollment, appeals, notices) still accept orphan IDs at the HTTP boundary. The demo dataset is internally consistent by construction — but live demo edits via the worker portal can still create orphans through endpoints whose backend hasn’t adopted the validator yet. Mitigation: the runbook (Step 8) warns presenters to limit live edits to verification flows; full validator rollout is tracked as a separate follow-up.

  7. Two-day deadline pressure. The plan covers 24 archetypes × 13 service DBs × generator + verifier + 2 E2E specs + runbook. The user’s standing guidance is "we don’t scope down under pressure" — so the mitigation is not a fallback to fewer archetypes, it is honest sequencing. Recommended implementation order under deadline: Step 1 (verifier skeleton) → Step 5 (--profile + DATABASES const) → Step 3 with priority archetypes 9, 8, 20, 3, 5 (covering #519, #520, #521, #522, #523 in that order) → Step 4 (commit + drift gate). Steps 7 + 8 can land same-day; the multi-size screenshot spec (Step 7’s second spec) can land post-demo if Step 7’s coherence spec passes.

  8. Existing canopy-seed generator’s orphan-ref bugs are unresolved. The cargo xtask seed random-seed default still produces orphan refs in the new dashboard-feed tables (verifications, ievs_hits, wic_appointments). This is out of scope here per [scope] but it means E2E tests that depend on those panels rendering non-empty data either skip the panels or load the demo profile explicitly. Track separately.

Edit this page · default