Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Items with a **→ details** link have a full write-up in [`backlog/`](backlog/)

## Engine & State

- [ ] **`DragMode` consumers test `=== 'pan'`, a new variant falls silently into orbit** `ready` — five sites hand-pick one string literal instead of an exhaustive switch, and `orbitControls.ts` keeps its own local `DragMode` type distinct from the shared one. → [details](backlog/2026-09-29-dragmode-binary-pan-tests.md)
- [ ] **Takeover's camera-cluster merge wipes any field it doesn't set** `ready` — `runTakeoverSaga` merges a partial `camera` cluster, and `mergeSettingsSnapshot` replaces whole clusters rather than fields, so any `CameraSettings` field added later is dropped for the duration of every takeover. → [details](backlog/2026-09-29-takeover-replaces-camera-cluster.md)
- [ ] **`DEFAULT_VOLUME_PALETTE_ID`'s comment claims persistence that doesn't exist** `ready` — the comment says the palette choice is persisted to localStorage by the App shell via `setVolumePalette`; neither the storage call nor that symbol exists anywhere in `src`. → [details](backlog/2026-09-29-stale-palette-persisted-comment.md)
- [ ] **Derive `SettingsSnapshot` from the Layer registry** `needs-design` — the takeover's captured-cluster list is a hand-written `Pick<>` a new Layer must remember to join; two Layers didn't and were restored-over until #793 patched them by hand. → [details](backlog/2026-09-21-derive-settings-snapshot.md)
- [ ] **`SCENE_ANCHORS`/`AnchorBody` is the place table, filed under a body name** `needs-design` — the black-hole prep's first non-body row (`GALACTIC_CENTRE_ANCHOR`, a `PlaceId`) exposed that this table was never body-only, just never used for anything else. → [details](backlog/2026-09-22-scene-anchors-are-places.md)
- [ ] **The solar system hangs off the Sun; it could hang off a place** `deferred` — a `'solar-system'` `PlaceId` at the heliocentre would re-key the eleven `focusId: 'sun'` element rows and both Sun-anchored regions the way R15 did for the Galactic Centre; no reader needs it until a barycentric ephemeris, a moving render origin, or the seeded stars leaving the body tables. → [details](backlog/2026-09-23-solar-system-place.md)
Expand Down
41 changes: 41 additions & 0 deletions docs/backlog/2026-09-29-dragmode-binary-pan-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# `DragMode` consumers test `=== 'pan'`, a new variant falls silently into orbit

`ready`.

## What is true today

- `src/@types/camera/DragMode.d.ts` — the shared type is `'orbit' | 'pan'`.
- `src/services/camera/orbitControls.ts:21` declares a second, local
`DragMode = 'orbit' | 'pan' | 'pinch'` — the recognizer's own type, not an
import of the shared one. Line 60 sets `dragMode` to `'pan'` or `'orbit'`
from button state; line 71 sets it to `'pinch'` for a second contact; line
90 gates click-to-pick on `endedMode === 'orbit'` specifically, so pan and
pinch are both (correctly, today) excluded — but only because someone
enumerated `'orbit'` by hand, not because the switch is exhaustive.
- `src/services/camera/applyInputToCamera.ts:49` — `if (step.mode === 'pan') { ... }`
with no accompanying `else if (step.mode === 'orbit')`; anything that is not
`'pan'` falls through to the orbit math after the block.
- `src/utils/camera/latchSurfaceGesture.ts:39` — same shape:
`if (step.mode === 'pan') { ... }`, tilt-handle logic follows unconditionally
for every other mode.
- `src/services/engine/camera/replayInput.ts:140` —
`step.kind === 'drag' && step.mode === 'pan' && bodyMovesThisFrame(focus)`
gates the followed-body strafe path; any other mode skips it silently.
- `src/utils/camera/steppedSitePose.ts` — `InputStep`'s `drag` variant carries
a `mode: DragMode` field (`src/@types/camera/InputStep.d.ts`), but this file
never reads `.mode` at all: every `kind === 'drag'` step drives the
turntable orbit math, so a `'pan'` (or future) mode on the site rung has no
distinct effect here — it is not tested and not routed, just absorbed.

None of these five sites use a switch or `satisfies never`; each hand-picks
one string literal (`'pan'` or `'orbit'`) and treats the rest of the type as
the other case by omission. Adding a third member to the shared `DragMode`
compiles cleanly and silently reuses the orbit (or no-op) branch everywhere.

## What would fix it

Make the shared `DragMode` the only `DragMode` — drop the local redeclaration
in `orbitControls.ts` — and replace each `if (step.mode === 'pan')` with an
exhaustive `switch (step.mode)` (or an equivalent `satisfies never` default)
so a new variant is a compile error at every consumer, including
`steppedSitePose.ts`, until each site has decided what it does.
34 changes: 34 additions & 0 deletions docs/backlog/2026-09-29-stale-palette-persisted-comment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# `DEFAULT_VOLUME_PALETTE_ID`'s comment claims persistence that doesn't exist

`ready`.

## What is true today

- `src/data/defaults.ts:114-119` —
```
/**
* Default renderer-wide palette LUT for the scalar-volume overlay.
* 'viridis' is matplotlib's perceptually-uniform default — neutral
* blue-green-yellow ramp that reads as "scientific" without leaning
* warm or cool. Mutated at runtime via `setVolumePalette`; persisted
* to localStorage by the App shell so reloads keep the user's choice.
*/
export const DEFAULT_VOLUME_PALETTE_ID = 'viridis' as const;
```
- `grep -rn "localStorage" src` restricted to palette-related files finds
nothing; there is no `localStorage` call anywhere near `volumePalette`,
`setVolumePalette`, or `DEFAULT_VOLUME_PALETTE_ID`.
- `grep -rln "volumePalette" src` returns only `src/data/defaults.ts` itself
(the comment's own line) — no consumer under `src/components/` or an App
shell file reads or writes a persisted palette choice.

The "Mutated at runtime via `setVolumePalette`" half also does not resolve:
there is no `setVolumePalette` symbol anywhere in `src`. The palette choice
resets to `DEFAULT_VOLUME_PALETTE_ID` on every reload; nothing persists it.

## What would fix it

Delete the "persisted to localStorage by the App shell" sentence (and the
`setVolumePalette` reference, since that symbol doesn't exist) from the
comment, or — if persistence is actually wanted — add it and let the comment
describe real behavior.
40 changes: 40 additions & 0 deletions docs/backlog/2026-09-29-takeover-replaces-camera-cluster.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Takeover's camera-cluster merge wipes any field it doesn't set

`ready`.

## What is true today

- `src/state/takeover/runTakeoverSaga.ts:29` —
`yield* put(mergeSnapshot({ camera: { fovDeg: DEFAULT_FOV_DEG } }));`
is the only field the takeover bracket sets on `camera` before running the
body.
- `src/state/settings/mergeSettingsSnapshot.ts:38-42` —
`mergeSettingsSnapshot` does
`return { ...state, ...(structuredClone(patch) as Partial<EngineSettingsState>) };`.
The spread is top-level only: a cluster present on the patch (`camera`)
replaces the entire cluster object wholesale, it does not merge field by
field into the existing cluster. The file's own header calls this out as
intentional ("replace each cluster the snapshot carries, leave the rest
untouched") for the tour-restore use case, where the snapshot always carries
every field of a cluster it touches.
- `runTakeoverSaga`'s patch is `{ camera: { fovDeg: DEFAULT_FOV_DEG } }` —
a partial `camera` cluster with only `fovDeg` set. Given the merge
semantics above, this call replaces `settings.camera` with an object that
has only `fovDeg`; every other current field of `CameraSettings` is
dropped, not preserved.

So any field added to `CameraSettings` after this call was written is wiped
to `undefined` for the duration of every takeover (tour or exhibit), unless
that field happens to also be reasserted somewhere else in the same body.
The comment in `runTakeoverSaga.ts` describes this as merging the FOV back
"after this" for a body with different needs, but doesn't address that the
call already destroyed the rest of `camera` before the body runs.

## What would fix it

Either read the current `camera` cluster and spread it before overriding
`fovDeg` (`mergeSnapshot({ camera: { ...currentCamera, fovDeg: DEFAULT_FOV_DEG } })`),
or change `mergeSettingsSnapshot` to merge per-field within a cluster instead
of replacing the cluster object — the latter is the more general fix given
the header already documents cluster-replace as a deliberate, narrow choice
for the snapshot-restore path specifically.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SDD ledger — plan: docs/superpowers/plans/2026-09-29-openspace-camera-prep.md

Spec: docs/superpowers/specs/2026-09-29-openspace-camera-mode-design.md. Draft PR #830. Branch worktree-openspace-camera-mode, BASE f30ef5d57.
Asks (AqJE): approved; dispatches = camera chain D1 (T1+T2) → D2 (T3) → D3 (T4+T5), D4 (T6) in parallel with the chain; no perf gate.
Pre-flight: T1 & T3 both edit cameraDrivers.ts (disjoint lines, serial chain). T6 files are disjoint from T1–T5. No conflicts.

D1 (T1+T2) opus, BASE f30ef5d57 — dispatched
D4 (T6) opus, BASE f30ef5d57 — dispatched in parallel (file-fenced)
D4 (T6) done 8b8dd0fbe, opus 35 tool uses; deviations: methods-not-arrows in PersistedValue (variance), splashStorage.test deleted (moved). Review dispatched.
Task 6: complete (8b8dd0fbe, review clean). IDE togglePalette error was stale.
Task 6: minor (deferred): persistValues.test.ts:73-76 'skips null' test near-restates code — final review to triage
Task 6: minor (deferred): PersistedValue methods drop readonly (variance, commented)
D1 done: T1 d32c5302e, T2 e3624415f (opus, 21 tool uses); deviation: commitOnEdge.test.ts compile fallout. Typecheck clean at e3624415f.
Task 2: complete (e3624415f, not review-tagged)
Task 1: complete (d32c5302e, review clean; sonnet reviewer)
D2 (T3) done 0cc45ee66 opus. Deviations: ':180 tween' is actually followApproach ease; tweenToClip/evaluateClip DROP offset (focus tween snaps?); shell renderers switched to orbitForwardOf (f32 round-off at zero offset); approachTiltedPose carries (implementer ruling).
D3 (T4+T5) opus dispatched at 0cc45ee66, in parallel with the T3 review (fenced)
Task 3: review Spec ❌ — tween row snaps lookOffset (tweenToClip drops it); misnamed test. Fix round 1 → resumed T3 implementer.
Task 3: minor (deferred): shell renderers → orbitForwardOf is ulp-level, sound (old subtraction was the wrong one)
PR 2 note: nothing clamps orbit pitch + offset pitch past zenith (imagePlaneBasis flips); approachTiltedPose tilt reads τ+offset pitch then engage drops offset (one-time snap).
Task 3: fix round 1 → 6abea10ef (tween row eases offset by 1−easeOutCubic; real tween test). Task 3: complete (0cc45ee66..6abea10ef; lean protocol: one round, no re-review)
D3 done: T4 341a02f4e, T5 9be5211a4 (opus). Deviations a–g in agent reply: PITCH_LIMIT→data/camera/pitchLimit.ts; SurfaceStepCtx extracted, no NudgeCtx; world/site nudge synthesize pixel steps; body roll uses 'strafe' settle (roll beats look's tilt write); orbit [0,0] is non-identity.
Ruling: T4+T5 mid-branch review folded into the final whole-branch review (last tasks; one reviewer reads the same diff with both task contracts) — saves one review seat — cost if wrong: a T4/T5 defect gets one fix round instead of two.
PR 2 note: navigator must omit zero axes from ArmDelta (orbit [0,0] breaks by-reference identity).
CI run 36735151350 RED: 1/1425 files — noInlineTypes ratchet still allow-lists surfaceStep.ts (fixed by extraction).
Final review (opus): 1C 2I 7M → final-review.md. Fix wave dispatched (opus): C1 ratchet line, I1 world nudge zoom via absoluteRung zoom path (frameAlignedRoll), I2 body roll applied after settle (reject deviation d), M1, restating tests, spec drift.
Ruling: delete PersistedValue.skip + its test (M1) — no reducer writes null, dead guard carried from persistSplashVersion — cost if wrong: a null seenVersion would be written as 'null' and parse back to null (harmless).
Ruling: reject T5 deviation (d) roll-forces-strafe; roll applied after the orbit/look settle — per final review I2 — cost if wrong: none identified (roll moves neither eye nor forward).
Fix wave: 1130007e9 (code) + f957d03d1 (spec). Lean protocol: no re-review; CI is the gate. Pushed.
CI GREEN on f957d03d1 (run 36736620587). Diff vs f30ef5d57: src +673/−252 (50 files), tests +536/−137 (18), docs +11/−11. Awaiting user smoke → /feature-done → merge on word.
Dev server :5173 (shell bgsf23zx4) running from this wt for the smoke check. Pending dash ask onhC (smoke). Next on 'all unchanged': /feature-done (no deletion audit on prep PR) → squash-merge #830 on explicit word → PR 2 plan (writing-plans) from spec §4–§10 + the PR 2 notes above.
Task 4: complete (341a02f4e; reviewed in final review)
Task 5: complete (9be5211a4 + fix 1130007e9; reviewed in final review)
Smoke: user attested 2026-10-04 (all unchanged). /feature-done READY: 1422 files / 14891 tests pass, typecheck clean. Plan moved to completed/; spec stays in specs/ for PR 2.
Loading
Loading