ADR-075 — Narrow exception #2 to "no client storage": one functional cookie for Curator Tour resume
Status · Accepted Date · 2026-06-03 Owner · Marko Supersedes (in part) · PRD-016 M8 ("Heard-state tracking is in-memory only, runtime-scoped (lost on reload)") — narrows to the specific case of tour-position resume; heard-state remains in-memory. Related ADRs · ADR-057 (
orrery_localecookie — sibling pattern that establishes "one cookie, one purpose, one ADR each"), ADR-024 (URL-as-state for missions), ADR-029 (PWA + visit-counter in runtime memory), ADR-044 (URL-as-state for locale), ADR-055 (Science Lens — runtime-only) Closes into · PRD-016 §should-have S9, RFC-019 §11.3
Context
The Curator Full Tour shipped in v0.7 as a 21-episode, ~66-minute auto-advancing playlist (PRD-016 / RFC-019, epic #146). It is the longest editorial commitment Orrery asks of a visitor — significantly longer than the listen-through median for any single screen episode. Realistically, most visitors who start the tour will not finish it in one sitting.
Per PRD-016 M8 + WILL-NOT-HAVE, tour and heard-state are explicitly runtime-only: "in-memory only, runtime-scoped (lost on reload). ADR-057 forbids localStorage." The visitor closes the tab and starts over from pale-blue-dot on the next visit.
ADR-057 already established the pattern for a narrow exception when the absolute rule is actively hurting a clearly bounded UX: one cookie, one purpose, one ADR each. The locale-override case (ADR-057) and the tour-resume case have identical shape — a single user-set state, lost on reload, where the friction is real and the storage payload is tiny.
This ADR opens that exception for tour resume only.
Decision
Allow one additional cookie, scoped narrowly to Curator Tour resume state.
The exception
| Field | Value |
|---|---|
| Name | orrery_tour |
| Value | URL-encoded JSON: `{"ep":"{episode-id}","pos":{positionSec},"idx":{tourIndex},"cmp":0 |
SameSite | Lax |
Secure | true (production builds; omitted on http://localhost for dev) |
HttpOnly | false (must be readable from document.cookie — there is no server) |
Max-Age | 2592000 (30 days — shorter than ADR-057's 365 because tour state ages out faster than locale preference) |
Path | / |
| Set when | Tour is active. Throttled write every ~5 s during playback; explicit write on pause, overlay close, episode boundary advance |
| Cleared when | User clicks "Stop tour" button; tour reaches capability-ladder-close end; cookie payload fails schema validation on read |
| Never set when | Tour is not active; a single episode plays via inventory click (per-episode resume is explicitly out of scope) |
Payload schema
interface TourResumeCookie {
ep: string; // episode id, must exist in audio registry on read
pos: number; // positionSec, finite, 0 ≤ pos ≤ episode duration
idx: number; // tourIndex, integer, 0 ≤ idx < tourSequence.length
cmp: 0 | 1; // compact-mode flag at last write
}Total wire size: ~55–70 bytes URL-encoded. Well under cookie size limits.
Resolution on visit
- On
audiostate init, readorrery_tour. - If parse fails OR
epnot in registry ORidx >= CURATOR_FULL_TOUR.length→ ignore + clear the cookie. Don't surface an error to the user. - If valid, present an explicit "Resume tour where you left off?" affordance in the AudioOverlay header — a one-click button, not autoplay. (Browser autoplay rules require a user gesture for audio anyway; we make the gesture explicit.)
- User clicks → restore
tourActive=true,tourIndex, load the episode, seek topos, restorecompactmode if set, begin playback. - User dismisses (Esc or × on the resume prompt) → clear the cookie, start fresh.
Constraints on what this exception unlocks
This ADR is explicitly scoped to Curator Tour resume only. It does not open the door to:
- Persistent heard-episode state (PRD-016 §will-not-have line still binding).
- Per-episode resume for inventory clicks outside the tour.
- Speed preference, CC default, A/B provider preference persistence (all stay runtime-only).
- Autoplay-prompt visit counter (ADR-029 still binding).
- Any analytics / tracking cookie.
- Any cookie that stores anything other than the four fields specified above.
Future expansion to cover any of the above requires its own ADR with explicit reasoning. The default answer for any new "should we cookie X?" question stays no. ADR-057's discipline carries forward here.
Compliance
orrery_tour is a strictly necessary / functional preference cookie under ePrivacy Directive (EU 2002/58/EC, as amended) and GDPR Recital 30 — same class as orrery_locale (ADR-057). It stores only the position the user paused at, contains no personal data, no identifier, no tracking. No consent banner is required. Documented in /credits alongside the orrery_locale line.
Implementation surface
src/lib/audio-tour-cookie.ts— new module:readTourCookie(),writeTourCookie(state),clearTourCookie(), schema validation, debounced write helper (~5 s).src/lib/audio-state.svelte.ts— call writer on state transitions (tourIndexchange,positionSecthrottled,compactflag toggle); call clearer onstopTour()+ natural end.src/lib/components/AudioOverlay.svelte— "Resume tour?" affordance when cookie present at mount; wire restore handler.src/lib/audio-tour-cookie.test.ts— unit tests for cookie round-trip, schema validation, payload-too-large guard, debounce timer.tests/e2e/audio-tour-resume.spec.ts— new e2e: start tour → advance two episodes → seek mid-episode → close tab → reopen → resume prompt appears → click → playback resumes at exact(episode, position).- Update PRD-016 §M8 to reference this ADR as the narrow exception (heard-state otherwise unchanged).
- Update PRD-016 §will-not-have to refine "Persistent heard-state" wording — heard-state still NOT persisted; tour resume IS, narrowly.
- Update
docs/adr/TA.md§components Audio narration with the cookie line. - Update
docs/adr/index.mdto add this ADR. - Update
/creditsprivacy blurb to mentionorrery_touralongsideorrery_locale.
Estimate: ~120 LOC across helpers + state wiring + overlay affordance; 4 unit tests; 1 e2e test; 5 doc updates.
Rationale
Tour resume is the single case where the runtime-only constraint creates a meaningful editorial loss. The Curator Full Tour is the museum experience — 66 minutes, documentary order, the Atmospheric Moves landing in sequence. Asking the visitor to restart from pale-blue-dot every time they close the tab discourages the long listen-through that the §success-criteria explicitly measures ("≥ 10 minutes stay" as the museum-grade atmosphere proxy).
ADR-057 already proved the pattern works: narrowly scoped, ADR-gated, no consent banner, no slippery slope. The codebase has stayed disciplined since. A second narrow exception under the same discipline is the principled extension, not a slope.
Keeping the resume payload to a fixed 4-field schema with a read-time validator + cookie-clear on schema mismatch means a single mistake in the cookie format can never cascade into a broken overlay state — the cookie either parses cleanly or is treated as absent.
Alternatives considered
- Status quo (in-memory only). Honest about ADR-057 spirit. Costs the editorial loss every visitor hits after closing the tab mid-tour. The friction is real and central to the museum-grade goal.
- URL-as-state —
?audio=ID&t=120&tour=1. Fits ADR-024 / ADR-044 precedent perfectly. Cost: user has to bookmark or copy the URL themselves. "Close tab, come back later" doesn't auto-resume. Considered + rejected as the primary path; can still be added orthogonally as a share-link feature without superseding this ADR. localStorage. Same reasons as ADR-057: blocks main thread, industry moved away, no advantage over a cookie for a tiny preference payload, fights the rule history.- IndexedDB. Over-engineered for ~70 bytes. Reject.
- Service Worker cache as state store. Misuses the SW. Reject.
- Hybrid (cookie just caches the last tour URL). Cleaner story but requires the URL-as-state work as a prerequisite; introduces two coupled changes instead of one. Considered + rejected for v0.8; revisit if URL-as-state for audio lands separately.
- Open the rule wholesale. Loses ADR-057's discipline. Reject.
Consequences
Positive:
- Closes the editorial loss the absolute no-storage rule was creating for the Curator Tour.
- Keeps URL still canonical for share semantics (URL > cookie > defaults — same precedence as ADR-057 implies for any future scoped storage).
- Preserves zero-tracking / no-banner stance.
- Reinforces the established precedent: one cookie, one purpose, one ADR each.
- Enables the v0.8 "minimized tour" UX (PRD-016 §S8) to remember its compact-state across sessions for free.
Negative:
- Second cookie. The maintainer must enforce the "default is no" discipline against future "just one more cookie?" requests under their own ADRs.
- Adds ~120 LOC + test surface to maintain.
- Adds a small read-time validation surface — malformed cookies (manual user tampering, browser-extension interference) must clear cleanly without breaking the overlay.
- Marginally complicates the "I cleared my cookies" story for tour resume — clearing cookies restarts the tour from
pale-blue-dot. Acceptable; clearly documented in/credits.
Decision status
Accepted on 2026-06-03 by Marko as part of the v0.7 tour-v2 work-block kickoff (GH #300). Implementation lands in src/lib/audio-tour-cookie.ts + state wiring in src/lib/audio-state.svelte.ts + Resume-tour affordance in src/lib/components/AudioOverlay.svelte, with unit tests + an e2e cookie-resume spec.
ADR-075 · Orrery · Curator Tour resume cookie · narrow second exception to no-client-storage