Plan: Demo Dataset Seed Profile
On this page
- Status
- Context
- Scope
- Design
- Steps
- Step 1: Baseline orphan-ref audit
- Step 2: Persona archetype curation
- Step 3: Generator implementation
- Step 4: Commit generated SQL + CI drift gate
- Step 5: Wire
--profile demointo xtask seed - Step 6: Cross-service-ref verifier
- Step 7: E2E coherence spec
- Step 9: Case-detail workflow integrity (MR-d)
- Step 8: Runbook + reviewer sign-off
- Files Touched
- Verification
- Documentation Updates
- Risks
Status
| Step | Description | Status |
|---|---|---|
1 |
Baseline orphan-ref audit against current |
Done (2026-05-26) — |
2 |
Curate 24 persona archetypes + program-cohort matrix |
Done (2026-05-26) — |
3 |
Implement demo-profile generator ( |
Done (2026-05-26) — orchestrator ( |
4 |
Commit generated |
Done (2026-05-26) — 13 SQL files committed under |
5 |
Wire |
Done in MR-a (!376 / db421ff) |
6 |
Wire |
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,--profileflag, 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:
-
New dashboard tables are unpopulated.
canopy-verification.verifications,canopy-verification.ievs_hits, andcanopy-wic.wic_appointmentsare 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 andwic_appointmentsentirely. Result: those panels render empty for any seed. -
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, or404`s. The user’s auto-memory `project_demo_dataset_next_session.mddocuments this concretely. -
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_certificationswith overdue rows,canopy_eligibility.program_determinationswith cross-program-alert-eligible statuses,canopy_appeals.appeal_requestsat varied hearing stages. -
Deterministic generator (
tools/canopy-seed/src/demo/) producing one.sqlfile per service DB. -
Committed
.sqloutput atdevstack/demo-dataset/*.sql+ CI drift gate (cargo xtask demo check-drift). -
cargo xtask seed --profile demoflag that loads committed SQL via the existing psql-pipe loader. -
cargo xtask demo verifycross-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.tsthat 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.tsbut against the demo profile. -
Runbook
docs/modules/ROOT/pages/runbooks/demo-dataset.adocdocumenting 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.rsper 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 bytools/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 existingtools/canopy-seed/src/sql.rswriters. 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 replacedUtc::now()so emitted dates no longer churn daily (Plan 4 G6). The non-sql.rstables (canopy_tanf,canopy_medicaid, and the appended persona/ELE-consent supplements) are rendered bytools/canopy-seed/src/demo/sql_extras.rs; in particular the Express Lane Eligibilityele_consentsrows are emitted byrender_ele_consents_supplement(they were once hand-edited into the committedcanopy_medicaid.sql, whichdemo regeneratewould silently drop). -
Committed SQL (
devstack/demo-dataset/{canopy_persons,canopy_applications,…}.sql, 13 files): the artefactcargo xtask seed --profile demoactually loads. Human-reviewable. Frozen between regenerations. -
Drift gate (
cargo xtask demo check-drift): in CI, re-runs the generator into a temporary directory anddiff`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
v7timestamp prefixes diverge by years). -
cargo xtask seed(no flag) andcargo xtask seed --profile democannot accidentally produce overlapping data. -
Identical demo seed → identical UUIDs → committed
.sqlis 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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NOT seeded — federal-reporting pipelines aggregate from already-populated determination tables. Demo coverage is via downstream queries, not seeded rows. |
|
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). |
|
NOT seeded by demo — rulesets are loaded at service startup from |
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 |
|---|---|---|
|
|
application → household |
|
|
application → submitter |
|
|
determination → household |
|
|
determination → application |
|
|
verification → household |
|
|
verification → application |
|
|
verification → person |
|
|
ievs_hit → household |
|
|
ievs_hit → person |
|
|
certification → household |
|
|
certification → application |
|
|
certification → determination |
|
|
notice → household |
|
|
notice → person |
|
|
notice → application |
|
|
appeal → household |
|
|
appeal → requestor |
|
program-service |
appeal → determination |
|
|
appeal → notice |
|
|
ipv → household |
|
|
ipv → person |
|
|
snap_app → household |
|
|
snap_app → application |
|
|
snap_det → household |
|
|
tanf_app → household |
|
|
tanf_det → household |
|
|
medicaid_app → household |
|
|
medicaid_det → household |
|
|
caps_app → household |
|
|
caps_auth → child |
|
|
caps_auth → provider |
|
|
wic_det → household |
|
|
wic_part → person |
|
|
wic_appt → household |
Schema notes captured at plan time (verify before implementation; migrations may have moved):
-
canopy_persons.householdshas nohead_of_household_idcolumn. Head-of-household is identified viahousehold_members.relationship = 'head_of_household'(existing seed convention — seetools/canopy-seed/src/datagen.rs:490). The demo phase that mints applications stampsapplications.submitted_by = <HoH person_id>per the same convention. -
canopy_wic.wic_appointments(services/canopy-wic/migrations/20260512000000_create_wic_appointments.sql) has onlyhousehold_id+certification_idas ID columns.certification_idisUUIDwith no declared FK and is not enforced — the demo treats it as denormalized metadata, not a verifier-tracked ref. Adding aparticipant_idcolumn is a separate schema change out of this plan’s scope. -
canopy_caps.caps_authorizations.provider_idisUUID 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— writesprogram_determinations+combined_resultsfor cross-program-alert coverage -
demo/verification_phase.rs— NEW vs existing canopy-seed: writescanopy_verification.verifications+ievs_hitsfor #519 + #522 panel rows -
demo/renewals_phase.rs— writessnap_certificationswith overdue dates for #520 -
demo/notices_phase.rs— writesnoticeslinked to determinations + households -
demo/appeals_phase.rs— writesappeal_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.rs— NEW: writeswic_appointmentsfor #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 |
|---|---|
|
Random-seed default (today’s behavior, unchanged) |
|
Replay specific seed, unchanged |
|
TRUNCATE then run random-seed default |
|
Load |
|
TRUNCATE then load committed demo SQL (the canonical demo invocation) |
Implementation in xtask/src/cmd/seed.rs:
-
Add
--profile demobranch before thecargo run -p canopy-seedinvocation. When demo, skip the canopy-seed generator call entirely and setoutput_dirto<repo-root>/devstack/demo-dataset/. -
Add
--resetbranch: issue per-tableTRUNCATE … RESTART IDENTITY CASCADEagainst 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). -
Expand the
DATABASESconst (currentlyxtask/src/cmd/seed.rs:73-87) to includecanopy_verification,canopy_tanf, andcanopy_medicaid. Without this expansion the demo-profile loader silently skips those.sqlfiles 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). -
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:
-
beforeAll:cargo xtask seed --profile demo --resetviatests/e2e/lib/seed-loader.ts. -
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.
-
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 statemarkers. -
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}/determinereads the case’shousehold_id+application_id, then calls canopy-eligibility/v1/eligibility/determine(same endpointapprove_applicationalready hits atservices/canopy-web/src/api/applications.rs:459). -
Updated
tab_determination.htmladds an htmx form with a singleRun Determinationbutton. 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_reviewso 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):
-
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. -
Change report submission —
canopy-renewalsalready setsrequires_redetermination = trueatservices/canopy-renewals/src/api/mod.rs:418; the banner closes the loop until the cascade is wired in a separate plan. -
Medicaid CMD ingest —
canopy-medicaidrecords the event but does not invokecmd_cascade(the module exists atservices/canopy-medicaid/src/cmd_cascade.rsbut is unreferenced fromcmd_handlers.rs:78). The banner is a workaround, not a fix; auto-cascade wiring is tracked in follow-up #575. -
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
2026-05-26 scope split + closure: Step 9c shipped as two MRs —
-
Step 9c-A — cases-search status badges (new
/v1/eligibility/case-statusendpoint + parallel fetch + 5 colored chips). Merged as !377 / 18a6965 on 2026-05-26. -
Step 9c-B — auto-issuance subscriber (extends
publish_determination_completedwithmonthly_allotment+effective_date; canopy-enrollment subscriber auto-creates the firstsnap_benefit_issuancesrow 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_enrollmentsrow via thedetermination.completed.snapsubscriber atservices/canopy-enrollment/src/main.rs:161but never creates the matchingsnap_benefit_issuancesrow — that requires a separatePOST /v1/enrollments/{id}/issue_benefitscall no one in the demo flow makes. Add a downstream subscriber in canopy-enrollment that listens forenrollment.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 atservices/canopy-web/src/api/cases.rs:130with reading the actual determination status fromcanopy-eligibility.program_determinations.status. Render terminal-state badges: greenActive, amberPending, redDenied/Terminated/Sanctioned, greyClosed. 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 queriesappeal_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_panelatservices/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.htmlalready 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):
-
Run Determination — htmx POST to the Step 9a handler (
POST /cases/{household_id}/run-determination). -
Request Verification — opens an inline mini-form that POSTs to a new wrapper handler
POST /cases/{household_id}/request-verificationin canopy-web, which forwards to canopy-verification’sPOST /v1/verifications. (Plan-draft revision: no existing canopy-web handler routed to that endpoint; built one.) -
File Appeal — deep-link to the Appeals tab where the existing
file_appealform lives. -
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).
-
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/scheduleendpoint orinterview_scheduled_atcolumn 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)
-
cargo nextest run -p canopy-web -p canopy-enrollment -p canopy-appeals— new tests pass -
Manual against the demo profile:
-
Open archetype 4 → click Run Determination → status flips to Denied inline → Notices tab shows the auto-generated denial NOA → click NOA → PDF downloads
-
Open archetype 9 (IEVS discrepancy) → click Resolve on a row → row disappears inline (no full reload)
-
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
-
Open archetype 1 (SNAP Active) → cases-search shows "Active" badge; archetype 4 shows "Denied" badge; archetype 5 shows "Sanctioned" badge
-
Dashboard → Pending Hearings panel shows ≥5 rows (from archetype 23)
-
Case detail → click Action ▾ → dropdown lists 4 actions, each routes to a working handler
-
-
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 |
|---|---|
|
NEW — demo module entry |
|
NEW — 24 archetypes |
|
NEW — curated name list |
|
NEW — GA county/ZIP pool |
|
NEW — orchestrator |
|
NEW |
|
NEW |
|
NEW |
|
NEW — fills #519 + #522 source tables |
|
NEW — fills #520 source table |
|
NEW |
|
NEW |
|
NEW |
|
NEW — net-new vs existing seed coverage |
|
NEW — net-new vs existing seed coverage |
|
NEW |
|
NEW |
|
NEW — fills #521 source table |
|
MODIFIED (MR-d 9a) — new |
|
MODIFIED (MR-d 9a) — |
|
NEW (MR-d 9a) — handler + integration test |
|
MODIFIED (MR-d 9b) — chain-through banner on income save |
|
MODIFIED (MR-d 9b) — chain-through banner on change-report submit |
|
MODIFIED (MR-d 9b) — chain-through banner on CMD ingest |
|
MODIFIED (MR-d 9b) — emit banner partial on success |
|
MODIFIED (MR-d 9b) — emit banner + htmx row-swap on resolve_discrepancy |
|
MODIFIED (MR-d 9b) — emit banner on CMD ingest |
|
MODIFIED (MR-d 9c) — new |
|
MODIFIED (MR-d 9c) — auto-issuance helper |
|
NEW (MR-d 9c) — subscriber test |
|
MODIFIED (MR-d 9c) — replace hardcoded |
|
MODIFIED (MR-d 9d) — new |
|
MODIFIED (MR-d 9d) — query function for hearings_scheduled |
|
NEW (MR-d 9d) — endpoint test |
|
MODIFIED (MR-d 9d) — new |
|
MODIFIED (MR-d 9d) — wire to real data shape |
|
MODIFIED (MR-d 9e) — replace dead |
|
NEW (MR-d 9e) — dropdown partial with 4 case actions |
|
NEW or MODIFIED (MR-d 9d) — |
|
NEW — bin entry |
|
MODIFIED — add writers for verifications, ievs_hits, wic_appointments, tanf_*, medicaid_* |
|
MODIFIED — |
|
|
|
NEW — committed generator output |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
NEW |
|
MODIFIED — |
|
NEW — |
|
MODIFIED — register |
|
NEW |
|
NEW |
|
MODIFIED — |
|
MODIFIED — |
|
NEW |
|
MODIFIED — link runbook + this plan |
MODIFIED — note the demo profile |
|
|
MODIFIED — Unreleased entry |
Verification
-
cargo nextest run -p canopy-seed— generator unit tests pass -
cargo xtask validate— workspace-wide gate green -
cargo xtask dev start -
cargo xtask seed --profile demo --reset— loads cleanly -
cargo xtask demo verify— exit code 0, zero orphan refs -
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) -
Manual: open the worker portal, walk through one instance of each of the 24 archetypes, confirm no
404or empty state -
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 demoin the commands -
CHANGELOG.adoc— entry under== Unreleasedmentioning 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.adocStatus table — march through Step 1 → 8 as work proceeds
Risks
-
Schema migration mid-flight invalidates committed SQL. If a per-service migration lands after generation but before demo day, the committed
.sqlmayERRORagainst 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. -
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
extrasare tuned and SQL regenerated. -
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.
-
Demo profile accidentally loads in CI. If a developer runs
cargo xtask seed --profile demoand forgets to switch back, E2E specs may fail in confusing ways. Mitigation: default profile is unchanged (default);--profile demois explicit and surfaces in the seed-output capture file (test-results/seed/last.txt). -
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 explicitDemo-prefix on case files surfaced in the UI (e.g., "Sarah Johnson — Demo Case 0001"). -
ADR-025 validator coverage gap is real and unmentioned during the demo. Only
canopy-verificationenforces cross-service ref validation today (rolled out as!374on 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. -
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. -
Existing canopy-seed generator’s orphan-ref bugs are unresolved. The
cargo xtask seedrandom-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.