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-ifterms.
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 integrity —
DoneSlide.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-existingresyncClerkUser()helper. - Interstitial arbitration coordinator — a new
useInterstitialCoordinator.tscomposable that every interstitial-class surface (6 dashboard surfaces + 9 globalApp.vueoverlays) resolves through, replacing the pairwisev-ifchain entirely. A newmeta.flowroute predicate exempts flow routes (/welcome/*,/profile/complete/*) by construction.App.census.spec.tsderives the mounted-overlay list fromApp.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 houseuseGoalForm.tsidiom used everywhere else in the app;useFeatureTour/useWhatsNewdismissals now retry through the existingackRetryQueue(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 docs —
docs/development/notification-surfaces.mdrewritten in one pass covering all three code phases' mechanisms, verified against shipped source. - Verified
masterpromotion — the integration branch promoted tomasterwith containment confirmed by content (ancestor checks + empty diff), not by the file-presence script alone.
Phases
| Phase | Name | Status | PR | Highlights |
|---|---|---|---|---|
| 1 | Onboarding completion integrity (g45, blocking) | Shipped | #2372 | Ships alone, first — the only blocking finding. Write-after-confirm plus deferred reconcile. |
| 2 | Interstitial arbitration coordinator (sys-A) | Shipped | #2376 | Largest phase — Desi UI-SPEC, then two sequenced Codi tasks. Live-confirmed by Josh personally that no unprompted surface can land on a flow route. |
| 3 | Flow-flag durability + re-entry guard residuals (sys-B, sys-C) | Shipped | #2375 | Re-entry guard live-confirmed via a real raw-dispatch Playwright probe. Sweep surfaced 26 other unguarded call sites, correctly left out of scope. |
| 4 | Journey registry repair + new-flow coverage | Shipped | #2374 | Shipped by Tess. Two new journeys registered; live end-to-end run not obtained, carried forward honestly. |
| 5 | Consolidated notification-surfaces.md docs | Shipped | #2379 | Added mid-milestone to avoid three concurrent PRs on one hot doc. Caught and fixed a factual error in its own first draft. |
| 6 | Promote v4.37 to master (verified by content) | Shipped | #2380 | Added 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
masterahead of the milestone's own unified promotion. masterpromotion 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 reachmaster," 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).
| Category | Count | Status |
|---|---|---|
| Onboarding completion integrity (F1) | 2 | Satisfied |
| Interstitial arbitration (F2–F4) | 2 | Satisfied |
| Durability residuals (F5–F7) | 2 | Satisfied |
| Journey coverage (F8–F9) | 1 | Partial — journeys registered, live end-to-end run not obtained, carried to OBJ-2595 |
| Docs | 1 | Satisfied |
master containment | 1 | Satisfied, 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'spersistEpoch()has the same swallowed-try/catchshape 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 ofMapinsertion 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+auditfull 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.
Related Artifacts
- Roadmap: v4.37-flow-integrity-interstitial-arbitration-ROADMAP.md
- Milestone Audit: v4.37-flow-integrity-interstitial-arbitration-MILESTONE-AUDIT.md
- Notification surfaces mechanism doc:
docs/development/notification-surfaces.md - Triggering issue: OBJ-2589
- Coordination anchor: OBJ-2590
- Follow-up (unmet criterion, live journey runs): OBJ-2595
- Prior instances of the master-promotion gotcha: v4.15 Dashboard Hierarchy, plus six other recorded occurrences
- Merge PR: #2380
Related Commits
5f3b9508f— Promotion tomaster(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