Skip to content

v4.37 — Flow Integrity & Interstitial Arbitration

A client can no longer claim a flow finished when the server never heard about it, and exactly one interstitial-class surface can win a session — enforced by one arbiter instead of a hand-maintained chain of v-if terms.

Summary

Desi's objectuve-throughline trace-only sweep of the 10 registered UAT journeys (OBJ-2589) found two contract-vs-enforcement gaps: in both cases the repo had already written the correct rule down, and nothing enforced it.

First, DoneSlide.completeOnboarding() wrote the onboarding-completion flag to localStorage before the terminal mutation resolved, and neither failure branch rolled it back. syncClerkUser's fast path then re-read that flag on every subsequent page load with no backend read — so a client that hit a mutation failure was permanently "onboarded" on the client, with no server record, no goal, and no route back into the wizard.

Second, the v4.15 interstitial cap policy — "at most one interstitial-class surface per session, chosen by a fixed priority order" — was enforced by a hand-maintained pairwise v-if chain covering 6 of roughly 14 surfaces. It had already drifted twice in five weeks, and none of the nine overlays mounted globally in App.vue was aware of routes at all: a legendary celebration or a What's New takeover could land on a mid-onboarding slide.

Both gaps are now closed. The onboarding flag writes only after a mutation resolves, and a client that's already diverged reconciles on its next in-app navigation. A single registration-based coordinator now owns interstitial arbitration for every surface, dashboard and global alike, with a route predicate that exempts flow routes by construction and a census test that fails CI the next time a surface is added without registering. Three smaller durability fixes closed re-entry-guard and dismissal-retry gaps, two new test journeys were registered, and the milestone's own promotion to master was verified by content rather than by a script that would have false-passed on 40% of the diff.

Goal

Two independent failure shapes surfaced in the same sweep, and both are contract-vs-enforcement gaps: the repo has already written down the correct rule, and nothing enforces it.

Scope — What Shipped

  • Onboarding completion integrityDoneSlide.completeOnboarding() now writes the completion flag only after each terminal mutation resolves successfully (write-after-confirm on both the goal and no-goal branches). syncClerkUser's fast path still trusts the cached value for first paint (no blocking cold-boot network read) but corrects itself against the backend on the client's next in-app navigation, via the pre-existing resyncClerkUser() helper.
  • Interstitial arbitration coordinator — a new useInterstitialCoordinator.ts composable that every interstitial-class surface (6 dashboard surfaces + 9 global App.vue overlays) resolves through, replacing the pairwise v-if chain entirely. A new meta.flow route predicate exempts flow routes (/welcome/*, /profile/complete/*) by construction. App.census.spec.ts derives the mounted-overlay list from App.vue's own source and fails CI on a future undeclared mount.
  • Flow-flag durability + re-entry guard residuals — a synchronous re-entry guard on GoalDiscoveryQuiz.adopt(), matching the house useGoalForm.ts idiom used everywhere else in the app; useFeatureTour/useWhatsNew dismissals now retry through the existing ackRetryQueue (namespaced, after a round-1 review caught cross-contamination with achievement acks) instead of silently swallowing a failed server write; AchievementWatcher's seen-set now records a badge as seen when its toast actually surfaces, not when it's scheduled.
  • Journey registry repair — journey 02's stale redirect pattern fixed; two new journeys registered for the goal-discovery quiz and the username gate.
  • Consolidated docsdocs/development/notification-surfaces.md rewritten in one pass covering all three code phases' mechanisms, verified against shipped source.
  • Verified master promotion — the integration branch promoted to master with containment confirmed by content (ancestor checks + empty diff), not by the file-presence script alone.

Phases

PhaseNameStatusPRHighlights
1Onboarding completion integrity (g45, blocking)Shipped#2372Ships alone, first — the only blocking finding. Write-after-confirm plus deferred reconcile.
2Interstitial arbitration coordinator (sys-A)Shipped#2376Largest phase — Desi UI-SPEC, then two sequenced Codi tasks. Live-confirmed by Josh personally that no unprompted surface can land on a flow route.
3Flow-flag durability + re-entry guard residuals (sys-B, sys-C)Shipped#2375Re-entry guard live-confirmed via a real raw-dispatch Playwright probe. Sweep surfaced 26 other unguarded call sites, correctly left out of scope.
4Journey registry repair + new-flow coverageShipped#2374Shipped by Tess. Two new journeys registered; live end-to-end run not obtained, carried forward honestly.
5Consolidated notification-surfaces.md docsShipped#2379Added mid-milestone to avoid three concurrent PRs on one hot doc. Caught and fixed a factual error in its own first draft.
6Promote v4.37 to master (verified by content)Shipped#2380Added after Phase 5 shipped, so promotion could not be assumed. Containment verified by content, not script alone.

Promoted to master via PR #2380 (5f3b9508f, 2026-08-16).

Key Decisions

  • The onboarding fast path stays cache-first, not network-first, on cold boot. A blocking backend read on every cold boot was rejected as too costly; instead the cached value is trusted for first paint and corrected in the background on the next navigation — satisfying "reconciles on the second paint, not never" without adding a new reconciliation helper.
  • The no-goal onboarding path keeps completing on mutation failure, by deliberate, recorded decision. That path has no error UI or retry surface today, unlike the goal path; blocking it would have needed a new recovery surface, out of this phase's UI scope. The flag write moved from before the mutation to inside the same finally, after the mutation settles — still write-after-attempt on this one branch, not write-after-confirm, and the docs (Phase 5) say so explicitly rather than smoothing it over.
  • Docs for all three code phases were consolidated into one pass, run last, instead of three separate PRs. Phases 2 and 3 shipped in parallel against the same target doc; this repo has a named gotcha for concurrent PRs chaining a hot doc's footer. Waiting until Phases 2 and 3 both merged cost a little latency and removed the conflict entirely — nothing was lost, since no phase reached master ahead of the milestone's own unified promotion.
  • master promotion was treated as its own phase, not a closing footnote. Five clean phase merges is exactly the state in which this repo's own named gotcha — "a milestone can finish every phase and never reach master," seven prior occurrences, v4.15 among them — has bitten before. Phase 6 was cut explicitly so promotion could not be assumed, and containment was verified by content (ancestor checks + empty diff) because the file-presence script alone would have false-passed on the two phases that only edited existing files.
  • A live-confirmation criterion that couldn't be closed in the agent chain was escalated, not weakened. Neither Codi nor Roy had the credentials or environment access to drive a legendary-unlock trigger on a deployed preview. Both said so plainly instead of substituting a unit test, and Maggie escalated to a human rather than routing a pointless fix round back to Codi — the gate worked as designed.

Requirements Coverage

8 / 9 success criteria fully satisfied, 1 partial (this hand-authored milestone has no standalone REQUIREMENTS.md; criteria are drawn from the ROADMAP's own "Success criteria for the milestone" section — see the Milestone Audit for the full per-criterion table).

CategoryCountStatus
Onboarding completion integrity (F1)2Satisfied
Interstitial arbitration (F2–F4)2Satisfied
Durability residuals (F5–F7)2Satisfied
Journey coverage (F8–F9)1Partial — journeys registered, live end-to-end run not obtained, carried to OBJ-2595
Docs1Satisfied
master containment1Satisfied, verified by content

Full success-criteria detail: v4.37-flow-integrity-interstitial-arbitration-ROADMAP.md on GitHub.

Outcomes

A user whose onboarding-completion mutation fails no longer gets stranded believing they finished — the client doesn't claim it until the server confirms it, and a client that's already diverged reconciles on its next navigation instead of staying permanently stuck. Exactly one interstitial-class surface can now present per session across all 15 registered surfaces, enforced by a single coordinator instead of a chain of hand-maintained conditionals that had already drifted twice — and a census test now fails the build the next time a new overlay ships without registering, closing the drift vector rather than just the two instances of it this sweep found. The goal-discovery quiz can no longer double-submit on a same-tick double click, tour and What's New dismissals are retried instead of silently lost, and the deferred human-supervised walk+audit sweep has two more registered journeys to start from.

Tech Debt

  • (Phase 3, found during the F5 sweep, untracked) 26 pre-existing mutation call sites elsewhere in the app lack the same synchronous re-entry guard added to the quiz surface. Correctly reported as a number, correctly left out of this milestone's scope.
  • (Phase 3, MINOR, out of scope) useWhatsNew.ts's persistEpoch() has the same swallowed-try/catch shape Phase 3 fixed elsewhere in the same file.
  • (Phase 2, MINOR, Desi to rule) useInterstitialCoordinator.ts's two exemption classes resolve to the same priority; a same-tick collision between them falls out of Map insertion order rather than an explicit ruling. The one-surface-per-session frame rule still holds.
  • (Phase 2, NIT, log-don't-block) An already-presenting overlay doesn't unmount on a later navigation into a flow route — narrow reachability, clears on hard reload.
  • (Phase 4, carried forward, OBJ-2595) Journey 02's fix and the two new journeys (11, 12) have not been live-run end-to-end against a running app. Tour-surface journeys (FeatureTour, WhatsNewSpotlight) remain deferred pending a UI-SPEC. Several routes remain uncovered by any journey, recorded as a named deferral.
  • (Out of scope, unchanged from kickoff) The overdue human-supervised walk+audit full evidence pass (last run 2026-07-08) remains filed separately; four blocking gates are structurally unscorable by any trace sweep. This milestone's Phase 4 is its prerequisite, not its execution.
  • 5f3b9508f — Promotion to master (PR #2380)
  • 9ad0627db — Phase 5: consolidated notification-surfaces docs (PR #2379)
  • 596e26df4 — Phase 4: journey registry repair + new-flow coverage (PR #2374)
  • 063dfa6aa — Phase 3: flow-flag durability + re-entry guard residuals (PR #2375)
  • 3ec7e8212 — Phase 2: interstitial arbitration coordinator (PR #2376)
  • 68cdd2100 — Phase 1: onboarding completion integrity (PR #2372)

Last updated: 2026-08-16

Loading…