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::given — given::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/given — SnapCaseBuilder (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 submitted→pending (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 && fullStack→fullStack (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 && fullStack→fullStack (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). |