Plan: Constraint-Driven Generative Seed Harness (ADR-033, re-specs #716)

On this page
NOTE

Implements ADR-033. Re-specs #716 (seed-profile convergence) and absorbs the root cause behind #595 / #610 / #577 / #636. Epic &61 MR4 (journey harness, #760) depends on this plan’s MR3 — the step-primitive and endpoint-driven given libraries land here first. Grounding below is code-verified (2026-06-10). Issues are cut from the Status rows per ADR-013 once this plan lands.

Status

MR Description Status

MR1 (constraint schema + policy resolution + self-audit)

The load-bearing seams. canopy-seed gains a constraint module + a canopy-policy dependency: a declarative precondition schema (relation ∈ at-least/at-most/just-above/just-below/equal-count/has-attribute…, param = a policy-parameter NAME resolved at seed time against the target jurisdiction’s jurisdiction.toml + rulesets/federal/, margin), and a satisfiability checker that fails loud on contradictory or invariant-violating constraint sets (a finding, never retry-until-timeout). ScenarioEntry gains an optional preconditions table (the inventory row is the registry of seeding targets — one artifact, one audit). cargo xtask scenarios audit extends to the harness self-audit: a constraint citing a retired policy parameter or unknown election key is a finding (ADR-033 §7). Literal policy values in constraints are a SCHEMA error (ADR-033 §8).

Done (2026-06-11) — canopy-policy::precondition (the Precondition schema + Relation/Subject/SubjectKind; shape rules + the §8 literal-value check) + canopy-policy::params::PolicyParams (dotted-name resolution reusing the ADR-011 TOML flattener); ScenarioEntry gains preconditions. cargo xtask scenarios audit self-audits parameter names per corpus against that corpus’s jurisdiction.toml — a retired/mistyped name emits UNKNOWN-PARAM and exits 1 (proven live). canopy-seed::constraint::check_satisfiable is the fails-loud satisfiability checker (member-count ranges, subset-vs-household, attribute-needs-a-member, unit-grouped money bounds), plus the new canopy-policy dep. Canonical worked example on the georgia Senior SNAP 36-month scenario. Gate clean at 95/59/418, no regression; 22 new unit tests. Schema-home refinement: see Design.

MR2a (per-program domain-invariant registry + always-on enforcement + chaos seam)

Re-home the endpoint controls: a per-program domain-invariant registry (≥1 member per household; referential integrity; non-negative money; ordered dates; program-specific gates) enforced always-on for happy-path generation — no monolithic validator, no match program in core (ADR-033 §6). Each program contributes an invariants/{program}.rs INVARIANTS slice; the enforcer folds them via REGISTRIES and runs universal + present-program invariants, failing loud on any violation before SQL is written. The chaos seam (--violate <id>) disables a named invariant per run — wired, documented, unused on the happy path (sad-path seeding is future).

Done (2026-06-11) — canopy-seed/src/invariants/{mod,core,snap}.rs (3 universal + 2 SNAP invariants); main.rs enforces post-generate() before writing SQL; --violate flag. Explicit const-slice registry, not linkme (see Design). 7 tests: registry collection, generator output is invariant-clean, 3 violation-injection catches (zero-member / orphan ref / negative money), --violate skip, absent-program skip; binary smoke-verified.

MR2b (constrained generation + #577 dashboard tables)

Rewrite datagen.rs generation to satisfy the MR1 scenario preconditions and the always-on invariant layer (deterministic StdRng preserved; jurisdiction + program-subset parameterized per ADR-005/006) — the seeder produces households that match the inventory’s seeding targets. #577 correction: that issue is missing rows in three random-seed dashboard tables (verifications, ievs_hits, wic_appointments) — not orphan FKs — so it is fixed here by adding those generation phases (reusing the per-household UUIDs), not "as a by-product" of the invariant layer.

Done (2026-06-11) — both halves landed. #577 dashboard tables: datagen phase 13 generates pending verifications (#519) + ievs_hits (#522) + upcoming wic_appointments (#521) reusing per-household UUIDs; render_verification (new canopy_verification.sql, auto-loaded) + render_wic wic_appointments; the MR2a core.referential-integrity invariant extended to guard all three tables; rows index-placed so >=3 per panel hold for any seed. Constrained generation: canopy-seed::constraint::bounds_for resolves a satisfiable precondition set into GenerationBounds (count ranges + required attributes + resolved MoneyTarget`s); `canopy-seed::targets::load reads the jurisdiction’s conformance pack, runs check_satisfiable fail-loud + bounds_for per precondition-bearing scenario; datagen phase 1b appends one targeted household per scenario after the free-random bulk (bulk stays byte-identical — targeting adds seeding targets, never perturbs the corpus), satisfying count/attribute/income bounds; income uses the SAME size-indexed federal::fpl_for_size the determination’s income test uses so the generation spec == the eligibility oracle (ADR-033 §2). Money/attribute vocabulary the generator cannot place yet (e.g. disabled) is a fail-loud gap, not a silent skip. Targeted households flow through phases 2–13 as ordinary households and stay invariant-clean. 8 new tests (4 bounds_for, 3 loader, 1 generator end-to-end); the two scale_100_households count asserts generalized to the per-household invariant (== households.len()) + CAPS/WIC ranges keyed to households.len(), both robust to the appended targets.

MR3 (endpoint-driven given library + backdating)

The §4 execution model’s constructor: per-program setup helpers that drive the real service endpoints — apply → screen → determine → certify — under service tokens, registered per program (Rust in canopy-test-lib for integration tests; TS in tests/e2e/lib/ for Playwright), returning the created ids for the when/then. Signed determinations make this the only honest constructor (ADR-028). Tier-1 time: backdating support — helpers accept effective/start dates so "approved 11 months ago, cert expires next month" is constructed under the real clock through the contracts' existing effective_date params. Bulk background caseload stays SQL and is never asserted. &61 MR4 unblocks here. Split into MR3a (Rust SNAP foundation) → MR3b (TS mirror, the actual MR4 unblocker) → other-program builders.

In progress — MR3a Done (2026-06-11): the Rust SNAP given-library lands in canopy-test-lib::givengiven::snap::SnapCaseBuilder (fluent: .as_of(date) backdate anchor, .members(..)/.add_member(..), .certified(start,end)) drives persons (household+member+income) → apply → determine (orchestrator) → certify against the live devstack and returns the shared given::GivenCase (household / person / application / determination / certification ids). Backdating is tier-1 only (no clock fakery): every step threads effective_date/received_at, and the cert certification_start_date/end_date are the lever. Recon-verified the determination’s own effective_date is engine-computed (not a request field) — so the backdated certification is the construct, not a forged determination date. Per-program seam = each program adds a given/{program}.rs builder (SNAP first; the async-construction registry is deferred to the 2nd program per the "cheap now, rewrite later" principle). Proven by services/canopy-eligibility/tests/given_snap_test.rs against the live stack: an eligible household built 11 months back is approved + certified, and the round-trip confirms the persisted certification_start_date is the backdated value; an over-income household is denied + uncertified. MR3b Done (2026-06-12): the TypeScript mirror lands in tests/e2e/lib/givenSnapCaseBuilder (fluent .asOf(date) / .members(..) / .addMember(..) / .certified(start,end)) drives the same persons → apply → determine → certify lifecycle over Playwright’s APIRequestContext and returns the same-shape GivenCase, so a journey-*.spec.ts constructs its prerequisites through the real endpoints (the planned replacement for the manifest-backed fixtures.ts helpers — this is the &61 MR4 / #760 unblocker). Config resolution mirrors the Rust TestConfig (CANOPY_TEST*_URL env + .ports.env walk-up); the canopy-e2e compose service gains in-network CANOPY_TEST{KEYCLOAK,PERSONS,APPLICATIONS,ELIGIBILITY,RENEWALS}_URL so the library reaches the services by docker DNS from inside the container. Proven in-network by its own service-only given-library Playwright project (specs/given-snap.spec.ts, default pre-push gate): eligible-11mo-back → approved + certified with the persisted backdated start date round-tripped; over-income → denied + uncertified. TANF/Medicaid/CAPS/WIC TS builders remain (sibling modules).

MR4 (demo convergence — #716 realized)

The demo profile becomes a generated cast of the default engine (login-capable applicant households the seed guarantees into the required states, credentials published to the manifest — never persona constants in specs; ADR-033 §3/§8). Migrate the demo-gated specs off constants onto manifest-driven structural assertions, fold the authed applicant-portal WCAG coverage into the default gate, retire devstack/demo-dataset/*.sql + the demo profile, and close the #595/#610/#636 ambient-profile failure class (one axis = seed number). Split into MR4a-1…MR4f below (7 sub-slices; hard order 4a-1→4a-2→4a-3→4b→4c before 4d, 4d before 4e, 4f last).

Done (2026-07-04) — #716 closed; !753–!762 (MR4a-1…MR4f). Follow-ups: #969 (renewal-due hero, still open/deferred); #971 (vestigial TANF/Medicaid seed scaffolding) — resolved: the dead model structs + inert invariant branches removed.

MR4a-0 (seed-load integrity — prerequisite)

Fail-loud seed loader (psql -v ON_ERROR_STOP=1) + the three silently-aborting default-seed files: application_programs.status submittedpending (phase4 mirrors the determination outcome + linkage); render_security opens the ADR-014 canopy.audit_maintenance window instead of a guard-blocked bare TRUNCATE; SNAP ievs_match_results deduped to one wage match per person (idx_unique_ievs_match). Two new generator invariants (core.application-program-status-valid, snap.ievs-match-unique) fail generation loud on regression. Discovered mid-4a via a clean devstack load scan (#967); all 15 files now load clean under ON_ERROR_STOP. Also fixed a bundled WCAG AA audit-event contrast defect the loaded audit stream exposed (#968).

Done (2026-07-03) — !755

MR4a-1 (cast model + manifest + fixtures surface)

Manifest-facing CastMember struct + SeedData.cast; the manifest cast block; tests/e2e/lib/fixtures.ts role finders (findConfidential / findApprovedWithIssuances / findSubmittedWithVerifications / findRenewalDueSoon / findEleConsented). Behaviour-neutral (empty cast). The DB-facing credential/consent structs + Application confidentiality fields land in MR4a-2 with their writers — a written-but-unread field trips dead_code.

Done (2026-07-02) — !753

MR4a-2 (SQL + loader plumbing)

ApplicationIdCode/PasscodeHash/EleConsent structs + SeedData vecs; Application confidentiality/recovery/notify fields; credential + default canopy_medicaid (ele_consents) SQL writers with TRUNCATE (they do not FK-cascade from applications); confidentiality columns in the applications writer; loader wiring; credential/consent domain invariants. Behaviour-neutral (empty vecs → TRUNCATE only; SnapOnly skips the medicaid file).

Done (2026-07-02) — !754

MR4a-3 (cast generation)

A phase14_cast that unconditionally builds 4 credentialed states into SeedData + cast (approved+issuances+letters; submitted+verifications+IEVS, es-locale head; confidential; SNAP+TANF+ELE+identity-verification, full-stack), rendered by the seal-aware render_* writers; fixed per-role credential constants (deterministic — no Utc::now/OS-random hash in the seed); find-by-attribute (no position coupling); cast tests (roles/credentials/state) + cast_passcode_hashes_verify. Deviation: the wall-clock-relative renewal-due state is NOT seeded (a deterministic no-Utc::now seed can’t express "soon relative to today"); MR4b discovered it also can’t be built via the given-library (the portal hero reads the determination’s Utc::now()-pinned renewal_date against real today), so it is DEFERRED to #969. findRenewalDueSoon removed.

Done (2026-07-03) — !756

MR4b (migrate SNAP-only applicant specs + fold onto the default seed)

applicant-portal / visual-applicant / portal-recover off constants → manifest cast finders + structural assertions. De-gating folded in here (was MR4d): the cast lives only on the default seed, so applicant-portal + portal-recover move from the demoProfile project spread onto the default pre-push gate — folding applicant-portal’s authed axe/WCAG audit into the standard battery (the #716 headline win); `vb-applicant/vb-applicant-dark are migrated to the cast but stay on-demand (their --project move is MR4d). Renewal-due DEFERRED (#969): the portal renewal-hero is derived from the determination’s Utc::now()-pinned renewal_date vs real today, so it is not deterministically constructible per-case (no given-library extension or gated clock reaches it) — the renewal-hero tests are skipped with a #969 reference; the far-off-renewal control (approved-with-issuances cast) still asserts the fresh-approval hero. MR4d keeps only the residual demo-machinery removal.

Done (2026-07-04) — !757

MR4c (migrate worker-determination-ele)

Re-scoped to worker-determination-ele ONLY (visual specs moved to MR4d — see below). Off constants → the ELE cast (findEleConsented; discovery by household-id via a new findInQueueByHousehold, since the cast’s random surname is not a unique queue marker) + the select[name=document_id]label.choice-row radio / input[name=file] / input[name=document_type] workflow drift (mirrors MR4b’s portal-DOM fixes). Gate flips demoProfile && fullStackfullStack (the cast lives only on the default seed, so it can no longer be --profile demo-gated). Prerequisite #970 (merged !758): migrating this walk surfaced that the seed gave every application one received_at and never advanced applications.status past submitted, making the worker My Queue nondeterministic — fixed first (owner-directed, architecturally-correct) so the ELE cast reliably heads the worklist. Validated on cargo xtask e2e --devstack-profile full (the SnapOnly pre-push hook does not run this fullStack-gated spec).

Done (2026-07-04) — !759

MR4d (on-demand visual mechanism + journey→fullStack; keeps the --profile demo switch)

On-demand vb- mechanism + the visual-spec migration (folded from MR4c — a cast-migrated spec must run on the default seed, which for the visual baselines means the on-demand mechanism, so they are inseparable). Adds a purely-additive xtask e2e --visual (sets CANOPY_E2E_VISUAL_BASELINE=1) + --seed/--households to pin a capture’s full dataset shape; playwright.config.ts gates the seven vb- projects on visualBaseline (replacing the demoProfile gate) and flips journey demoProfile && fullStackfullStack (its given-library cases never needed the demo personas). visual-case-rich moves off its OKAFOR_HH constant → findApproved() (the force_all household 0), whose richness across all eight captured sections is an enforced deterministic seeder guarantee (new datagen test first_approved_household_is_rich_across_all_visual_sections, across two seeds). The --profile demo switch is deliberately NOT removed here — removing the public flag one MR before deleting the demo tree/docs (MR4e) would leave nav-linked runbooks documenting hard-erroring commands, so the flag + demo dataset + all docs retire atomically in MR4e. Merge only after 4a–4c.

Done (2026-07-04) — !760

MR4e (ATOMIC --profile demo retirement — switch + demo tree + full docs sweep)

Remove the switch (seed.rs SeedProfile enum + --profile arg + the Demo dispatch arm + switch tests; e2e.rs --profile/reset_for_demo/e2e_seed_profile + the now-dead CANOPY_E2E_SEED_PROFILE push) + delete the demo tree (devstack/demo-dataset/*.sql, tools/canopy-seed/src/{demo,bin/demo.rs}, the canopy-seed-demo bin, demo regenerate/check-drift; fix the stale model.rs "demo-only" comment) + a FULL docs/runbooks/nav sweep of every live --profile demo / CANOPY_E2E_SEED_PROFILE reference (residue gate: rg -e "profile demo" -e "CANOPY_E2E_SEED_PROFILE" over non-archive/ docs+code). Keep demo-review/demo-review-dark (default-seed). Discovered mid-implementation: the now-orphaned TANF/Medicaid determination model scaffolding in canopy-seed (unpopulated after the demo generator’s removal) — comments corrected to state reality here, and the scaffolding itself removed in #971 (the dead model structs + inert invariant branches).

Done (2026-07-04) — !761

MR4f (repurpose demo verify → seed-verify)

Renamed cargo xtask demo verify → the top-level cargo xtask seed-verify (kebab, matching xtask’s check-docs/plan-lint convention — a seed verify subcommand would have forced restructuring the heavily-used flat cargo xtask seed; deviation from the plan’s loose "seed verify" phrasing, recorded here). Flattened off the demo subcommand wrapper and made SKIP-TOLERANT: a check whose source/target DB container is down (the program DBs canopy_{tanf,medicaid,caps,wic} on a SnapOnly stack, incl. the ele_consents.* medicaid checks) is skipped, not failed — so it runs on both SnapOnly + full stacks. Closes #716.

Done (2026-07-04) — !762

MR5 (time tiers 2-3 + the effective-date ratchet)

Audit first: enumerate every decision-driving wall-clock read (recon 2026-06-10: ~70 Utc::now sites, most are timestamp-stamping; the known decision-drivers are canopy-appeals/src/clock.rs (90-day hearing clock) and canopy-snap/src/store/abawd_clock.rs (month counter)) and classify: has effective-date seam (tier 2, done) / needs one (extend the contract) / genuinely needs the gated clock (tier 3). Extend the existing clock structs behind a test-clock compile feature — devstack-only, stripped from production builds (a settable production clock is an audit-integrity hazard; ADR-033 §5). Wire the ratchet: a quality-budgets counter on ungated decision-driving Utc::now reads — new code must take an effective date; the tier-3 surface only shrinks.

In progress. Audit done (2026-06-13): 173 non-test wall-clock reads classified across all services — ~125 STAMP, ~19 SEAM-DONE, 8 NEEDS-SEAM (tanf sanction-expiry / cert-start, medicaid TMA-phase / ELE-age, wic cert-eff-date ×2, eligibility household-age, appeals disqualification-period, enrollment benefit-expiry), 10 GATED-CLOCK (appeals 90-day clock.rs + ADH-notice-window + active-disqualification-query; renewals due / overdue / interim-contact schedulers; enrollment expungement). The web/portal/infra/shared layer is provably clean (session / token expiry is auth-infra). Two audit corrections to the plan’s priors: the ABAWD month counter is NOT a clock driver (check_time_limit is deterministic over historical activity rows, never reads now()); and most GATED-CLOCK sites are reachable by backdating the anchor under the real clock (the given-library already backdates cert / appeal / issuance dates), so the gated clock’s genuine forward-advance need is small. Slices: MR5a mechanism → MR5b devstack opt-in + e2e proof → MR5c the 8 seams → MR5d the ratchet. MR5a Done (2026-06-13): canopy_common::clock::{now,today} is the tier-3 accessor — Utc::now() in prod; under the devstack-only test-clock feature, the real clock shifted by a process-global offset set via the /test/clock control router, mounted once in the shared canopy_api::ApiServer::router (unauthenticated, sibling of /livez). All 9 lifecycle GATED-CLOCK consumers wired (canopy-appeals / renewals / enrollment); the 10th audit GATED-CLOCK — canopy-wic’s upcoming-appointments query window — is a transient list-convenience read (not a decision gate), deliberately left on the real clock and out of the tier-3 surface. Prod-strip verified (the default build has no override path or route); unit + in-process control-router tests. MR5c Done (2026-06-13): the NEEDS-SEAM reads now take an explicit evaluation date (the count is 9, not 8 — the audit prose under-counted by one; enrollment benefit-expiry is the ninth). Determination path (6): an as_of: Option<NaiveDate> field threads through the eligibility / tanf / medicaid / wic ApplicationContext contracts — the orchestrator resolves the day once via clock::today() and stamps it into every dispatched context (all programs score the same date), and each handler falls back to clock::today() when absent (a direct, orchestrator-bypassing caller). The field is additive + optional ([serde(default)]; no context sets deny_unknown_fields), so a pre-seam body deserializes to None and a program that never models it (SNAP/CAPS) ignores the extra field — no coordinated deploy. Standalone (3): appeals disqualification-period start, enrollment benefit-issuance expiry, medicaid ELE age-out scheduler route straight through clock::today() (no request param). test-clock feature now also wired into canopy-eligibility / tanf / medicaid / wic. Tests: contract wire-compat (missing→None, present round-trips, None omitted) + a live /v1/determine proof (TANF cert start == supplied past as_of, deterministic effective_date; omitted → today). tanf + medicaid OpenAPI snapshots regenerated (request-body schema gained as_of). MR5b Done (2026-06-13): the gated clock is proven end-to-end. The root Dockerfile gains ARG CARGO_FEATURES="" — empty by default (production / CI images are byte-for-byte unchanged, NO settable clock — the ADR-014 invariant); --build-arg CARGO_FEATURES=canopy-api/test-clock feature-unifies the devstack-only clock across the single workspace build, mounting /test/clock on each service. Prod-strip verified live (test-clock build serves /test/clock 200; default build 404s). The tracer gained tracer_gated_clock_advances_overdue_classification ([ignore]; run cargo nextest run --run-ignored all -E 'test(gated_clock)' against an opted-in devstack): build a future-dated cert (not overdue), advance canopy-renewals' clock 25 days past it via POST /test/clock, watch the same case flip to overdue (boundary still its own cert end) and flip back on DELETE reset. Design finding for MR6: the gated clock is a process-global offset ⇒ a clock-advancing journey needs exclusive access to the affected service (the test resets before asserting + documents isolation; MR6 formalizes serial execution). Opt in (legit path, NEVER direct docker): CANOPY_CARGO_FEATURES=canopy-api/test-clock cargo xtask dev refresh — the compose build.args.CARGO_FEATURES + a features staleness marker make it rebuild the whole stack with the feature and stick. Sticky-marker refinement (2026-06-13): the marker is the source of truth when the var is unset — resolve_features(env_value, dir) keeps the last-built feature set on an env-less dev refresh / validate, so a test-clock devstack is never reverted by a routine command; an explicit value still wins (incl. CANOPY_CARGO_FEATURES= to opt back out). Split pure for unit-testability (set_var is unsafe in edition 2024); 4 cases pinned. MR5d Done (2026-06-13) — MR5 complete: the effective-date ratchet is quality budget B8 (cargo xtask quality-budgets), counting canopy_common::clock::{now,today}() CALL sites in production src/, locked at 13, shrink-only. The trailing-( requirement excludes the determination seams' unwrap_or_else(clock::today) fn-pointer fallbacks (tier-2 — they take an as_of), so only NEW ambient reads grow the surface (gate flags ⇒ thread an effective date or justify+raise). Sidesteps the B4/B3b doc-comment trap (skips pure-comment lines) + excludes canopy-common/src/clock.rs + the canopy-contracts-* DTO crates. Wired into validate [13i/15]; unit test pins the behaviour. Tier-1 + tier-2 seams + tier-3 gated clock + the ratchet are all in place. NEXT = MR6 (journey step-primitives — must formalize serial execution for clock-advancing journeys, the process-global-clock constraint MR5b surfaced — + seed-sweep), then &61 MR4 (the journey harness, tracer is the template).

MR6 (journey step-primitives + seed sweep)

The composable journey vocabulary (ADR-033 §6): step-primitives (drive-endpoint / advance-time / assert-invariant — consistency, conservation, monotonicity, state-machine, derived-classification per §3) so a journey is a data-described sequence and a new lifecycle transition is additive. Seed-sweep mode: run the invariant suite across N seeds, report the failing seed for deterministic replay (the property-testing bridge). Handoff artifact: &61 MR4 builds the first multi-life-event journey (#849-#854 work-list) entirely from these primitives.

In progress. Pre-MR6 thesis validation (2026-06-13): a hand-wired tracer bullet (tracer_journey_test.rs) proved the load-bearing claims compose against the live stack before the vocabulary was built (given-library construction + cross-source read-back consistency + relational/derived assertions + a [ignore] gated-clock flip MR5b proved end-to-end). Zero friction (first-try green). MR6a Done (2026-06-13): the step-primitive vocabulary lands in canopy_test_lib::journey — a Journey context with the three primitives (construct = drive-endpoint; advance_clock = advance-time tier-3; check over the five §3 invariants consistency/conservation/monotonicity/state_in/state_membership/derived_classification). Data, not code (§6/§8): lifecycle transitions + per-program "drive to approved" helpers are data in a journey::registry const-fold dispatched by .find(), no match program (mirrors MR2a); typed Observation enum (no serde_json::Value, B3a); the gated clock is touched only over HTTP (no canopy_common::clock import, B8 = 0). Both tracer tests rebuilt as journeys (tracer_journey_v2_test.rs) with no expressive loss — tier-1 partition green live; 5 invariant checks unit-tested; the gated journey [ignore] (process-global clock ⇒ run isolated), skips when /test/clock absent, time::advance resets-before-advancing (the MR5b serial-exec finding made structural). The v2 gated journey is now live-proven against a test-clock devstack stood up the legit way — CANOPY_CARGO_FEATURES=canopy-api/test-clock cargo xtask dev refresh (the devstack opt-in plumbed through compose build.args + a features staleness marker, replacing the fragile MR5b docker-direct recipe). MR6b (slice 1 — seed-sweep) Done (2026-06-13): the portability property (§3/§7 "any random valid instance must pass") as a runnable test. canopy_test_lib::journey::sweep runs a journey across N deterministic seeds — a per-seed Choices RNG (StdRng::seed_from_u64, the tools/canopy-seed model) draws valid-envelope choices (days_in), the SAME relational invariants assert for every seed, and a failing seed is the replay handle (Choices is a pure fn of the seed). The runner is non-panicking (Outcome::from_checks, the sweep sibling of Journey::check): a failing seed becomes an Outcome::Fail so the SweepReport names EVERY failing seed instead of aborting; all_skipped() is kept distinct from clean so offline reads as skipped (never vacuously green). Proven both ways — a pure unit test plants a failure for a known seed and asserts the report names exactly it (the MR6 acceptance, default-gate, no devstack); and a live journey_seed_sweep_test sweeps the tracer’s tier-1 partition across two seeds with seed-random backdate windows (verified live: a run grew snap_certifications by 4 = two cases × two seeds, not a vacuous skip), the partition holding for every random instance (value-decoupling demonstrated). MR6b remaining slices: per-program registry expansion (TANF/Medicaid/CAPS/WIC siblings — needs per-program given builders) + a cross-process serializer for multiple gated-clock journeys (premature until ≥2 clock journeys exist). The gated clock’s process-global offset is the one constraint (clock-advancing journeys run serially / isolated).

Design — grounded current state (code-verified 2026-06-10)

  • The generator is already deterministic: tools/canopy-seed/src/datagen.rs derives every decision from StdRng::seed_from_u64(config.seed) (uuid + data RNGs split at :143-144); reproducibility is solved — a failure replays from its seed. What’s missing is the constraint layer: today’s generation is free-random within hardcoded ranges (random_range(1..=2) adults etc.), unconstrained by policy parameters or domain invariants (#577’s orphan refs are the symptom).

  • The demo dataset is hand-curated SQL: devstack/demo-dataset/*.sql + tools/canopy-seed/src/demo/ (personas.rs: 24 archetypes; names.rs; generate.rs; sql_extras.rs). Mutually exclusive with the default profile — the #716 cluster’s root cause.

  • Value-coupling is concentrated: visual-applicant.spec.ts hardcodes MARIA_CODE/PRIYA_CODE/CARLOS_CODE; portal-recover.spec.ts hardcodes HH-c0ffee42. Most other literal assertions are HTTP status codes (fine). The conversion surface is bounded.

  • Valid-time plumbing already exists: effective_date on persons-income, snap/caps/wic/tanf determination contracts, authorized-reps, notices; as_of on persons batch reads — the ADR-027 substrate tier-2 time rides on.

  • Clock seams already exist where it matters: canopy-appeals/src/clock.rs, canopy-snap/src/store/abawd_clock.rs — the two hardest time-driven engines have structs to extend behind the feature gate.

  • Determinations are unforgeable: ES256 JWS (ADR-002) + input snapshots (ADR-028) — SQL cannot fabricate a valid mid-lifecycle state, forcing (and validating) the endpoint-driven given.

Design — decisions

  • The inventory row is the precondition registry. Scenario preconditions live on ScenarioEntry, not in a parallel file — the &61 inventory already enumerates the situation classes, and scenarios audit already parses these files, so the self-audit (§7) is one extension, not a new gate.

  • Schema-home refinement (MR1, vs the MR1 row’s original "`canopy-seed` gains the schema" framing). The Precondition schema + the PolicyParams resolver live in canopy-policy, not canopy-seed. Two forces require it: ScenarioEntry (a canopy-policy type) must embed preconditions, and the §7 self-audit runs inside cargo xtask scenarios audit (xtask → canopy-policy), which cannot depend on the canopy-seed binary crate. So canopy-policy owns schema + name-resolution + the audit-facing validation (validate_preconditions); canopy-seed owns only the satisfiability checker (constraint::check_satisfiable) and — MR2 — the generator, consuming the schema via a new canopy-policy dependency. Same seams as designed, homed where the audit can reach them.

  • Invariant registry — explicit const slices, not linkme (MR2a, vs the "auto-discovered distributed_slice" framing). Each invariants/{program}.rs exposes a pub static INVARIANTS: &[DomainInvariant], folded by a single REGISTRIES const; a new program is one module + one REGISTRIES entry (a registration point, never a match on program data). linkme::distributed_slice (the ADR-021 plugin mechanism) was the intended tool, but it emits a (correct) [unsafe(link_section)] whose unsafe attribute trips the workspace unsafe_code = "deny" lint on direct use; the plugin path compiles only because that lint is exempt for external-proc-macro-generated code. The const registry keeps the crate genuinely unsafe-free (no exemption, no [allow(unsafe_code)]) while giving the same no-match shape. (Verified 2026-06-11, correcting the earlier "edition-2024 hard error" note in the MR2a commit/CHANGELOG; the plugin-side governance gap is tracked in #855 and dissolves under the v2 WASM model.)

  • Param-by-name is the agility guarantee (ADR-033 §2). The harness re-derives constraints and oracles from the policy layer on every run; annual indexing (COLA/FPL/SMI) changes zero test code. Only structural policy change (new deduction type, new program) adds constraint vocabulary — registered per program, touching no core.

  • Endpoint-first, SQL-for-density-only (§4). Asserted state is constructed through real endpoints (reachability by construction); raw SQL is reserved for unasserted dashboard/search bulk.

  • Backdate-first time (§5). Tiers 1-2 cover the large majority of journeys with zero clock fakery; the gated clock is a shrinking, audited exception — never a global freeze.

  • Demo is a pinned seed, not a dataset. The stable cast for humans is --seed <pinned> + a generated credential manifest; no committed SQL, no asserted personas.

Verification

  • MR1: satisfiability unit tests (contradictory set → loud finding; invariant-conflicting set → loud finding; literal-value constraint → SCHEMA error); param-resolution tests against georgia + a fixture jurisdiction; self-audit catches a retired-param reference (mutate a fixture policy file).

  • MR2a (done): invariant unit tests — the generator’s own output passes every invariant; injected zero-member / orphan-ref / negative-money each fail loud; --violate disables a named invariant.

  • MR2b: property tests — N seeds × invariant suite, zero violations; cargo xtask seed-verify (ADR-025 cross-service auditor) green over generated output; the three #577 dashboard tables render ≥3 rows on a random seed.

  • MR3: per-program given-helper round-trips against the devstack (apply→determine→certify returns verifiable JWS); a backdated cert lands in GET /v1/renewals/overdue-adjacent windows correctly.

  • MR4: full default-gate e2e green with NO demo dataset loaded; the four migrated spec families pass against two different seeds (proving value-decoupling); axe coverage of authed surfaces runs in the default gate.

  • MR5: ratchet counter seeded; test-clock proven absent from release builds (compile-gate assertion); appeals/ABAWD clock tests drive the gated clock.

  • MR6: one full journey (from #849-#854) executes end-to-end from step-primitives across two seeds; seed-sweep mode reports a planted failure’s seed.

  • Each MR through the standard gate (validate + D1-D8 + force-merge squash=false); plan-lint + check-docs.

Edit this page · default