Swarm quality pass: the sky scroll was unreachable, and eleven other fixes - #11
Merged
Merged
Conversation
…stion
A swarm quality review of the merged One World + Anima work found that the
headline feature of the sky rework never ran on device. The arbiter decided
"is this touch a grab of a world in flight" from !isFieldAtRest() — a
predicate that is false at the RESTING sky (there is no orb to pop there),
so every sky touch armed the camera at touch-down, pinned the room to .none,
and skipped the one path where scrollRoom() is queried. The scroll outcomes
were unreachable by construction, and the test suite proved the split's math
while never once driving the shipped wiring: every scroll fixture said
isFieldAtRest: { true }, a state in which production never offers room.
The fix is the smallest true statement: two questions, two closures.
isFieldAtRest keeps deciding the pop; a new isCameraAtRest — camera at rest
ANYWHERE — decides the transit grab. At the resting sky the touch now goes
through the slop path, asks for the sky's room, and scrolls. Consequences
repaired with it: a touch on empty sky no longer flips worldMoving at
touch-down (which disabled every star's hit test under a resting finger and
woke the frame clock for a tap), and a sub-slop thumb wobble at the sky now
moves the world exactly as far as it does on the field: not at all. The
regression test that used to MEASURE that asymmetry now pins its absence.
The glide got the same honesty pass. It handed the offset to withAnimation,
but the constellation is drawn in Canvas closures, which SwiftUI cannot
animate: only the invisible tap targets eased while the drawing snapped to
the destination — and for half a second every star's target glided through a
place its star had already left. A finger catching the glide measured from
the destination and the content leapt under it. The offset itself now steps
on the state's own small clock (the world's TimelineView may be paused up
here), so drawing, hit targets and a catching finger share one number, and
began() freezes the content where it actually is.
The scroll fixtures now run the production shape (field false, camera true)
and would catch the conflation if it ever came back; SkyLayout.metrics stops
offering phantom room on windows too short to draw a single star.
…nnot be lost
Findings from the same swarm review, each with the failure it repairs:
THE PAUSE NEVER ENGAGED ON A CLEARED DISPLAY FIELD. isQuiescent waits for
every shell to spend, but a .waiting shell could never spend once the field
completed — tap() refuses a completed field, so it could not even be lit.
The comment at the top of Firework.swift ("a shell she never touched simply
fades with the field") was implemented nowhere: the TimelineView ran at full
rate over a still field for as long as she sat with it. Waiting shells now
fade out on the curve they faded in on, then spend. Tested across seeds.
THE PAUSE FROZE WEATHER MID-FALL. isQuiescent ignored the weather, so on a
snowy field the held frame kept flakes frozen mid-air under the done card.
Completion now eases WeatherField.presence to zero (~2 s), the renderer
multiplies the whole layer by it, and quiescence waits for the fade — the
held frame is a quiet field. Tested for snow, rain, fog and storm.
A PRESS ON A .taps GENERATOR AT THE ORB CEILING WAS A DEAD TAP: it consumed
the press and yielded no orb, no sound, no haptic. The ceiling bounds
AMBIENT emission; a press is her own hand, and it now forces one orb through
— bounded by the generator's own 3–5 presses.
A CORRUPT MAP DESTROYED THE WHOLE PATH. load() swallowed a decode failure,
ensureGenesis laid a fresh stone, and the first save() overwrote the only
copy of her map — a future release adding a stored property to MapStone
would have done this to every upgrading user. The unreadable bytes are now
moved to a keepsake key before anything can write over them, first failure
wins, and a future release that understands them can bring the Path back.
AUDIO COULD GO SILENT FOREVER OR CRASH. No media-services-reset handling
meant one system hiccup muted the whole session (the dead engine fails every
start() silently); the engine is now rebuilt on reset, wearing the old
buffers. A route change stops the engine from a system thread — the
configuration-change observer restarts it as the notification lands, and
play() re-checks at the last instant to make the uncatchable-exception
window as narrow as Swift can make it.
FRAME-RATE EQUIVALENCE, 60 vs 120. The fuse rope was damped per CALL
(0.94² per frame-unit on ProMotion — a visibly stiffer cord), spark
probabilities were per frame (double fire at 120 Hz), and wander's variance
halved. pow(damping, f), probability × f, and √f on the random walk make a
ProMotion evening the same evening; at f = 1 every value is byte-identical
to the tuned reference.
Reduce Motion catches up with the fireworks: the shell climbs straight (the
wobble is the oscillation a vestibular system objects to), the break's
field-wide shove is damped like the animal's startle, and the counter stops
pulsing per pop. The drifter's wall-easing now measures room against the
live insets rather than the static fallback and the bare glass, so it
settles at a bottom band instead of jittering against it.
…ed up ANIMA'S AUDIO SIDE IS NOW TOTAL UNDER NaN, like its geometry has been since iteration 5. Swift's min/max pass NaN through, so an authored NaN duration reached Int(sampleRate * seconds) and trapped, and a NaN glide poisoned every sample and trapped the exporter's Int16 conversion instead. A voice authored from a division that went wrong now renders as silence — never a crash — and both sample clamps are NaN-safe. Tested with four bad voices. THE PREVIEWER ESCAPES WHAT IT INTERPOLATES. Names, flavors and rarities go through esc() before innerHTML: the first flavor line authored with an ampersand would have broken its card, and on the Pages origin a crafted library.json was script injection. THE PAGES WORKFLOW NOW RE-RUNS ON ITS REAL INPUTS. Every gallery object is generated from the pop catalog and written by the exporter test, but the path filter watched only Vesper/Anima — a PR editing PopCatalog shipped a stale gallery until an unrelated Anima change happened to land. Also: the voice table is keyed by name first-wins, so a duplicate name is now a test failure instead of one voice silently wearing another's audio; the loudness gate no longer exits on the first silent voice; ember's spark fan spans its arc over the sparks actually drawn (spacing by the unclamped count bunched authored counts above five); tools/__pycache__ is untracked and ignored; two GameConfig comments now tell the truth about their own values; the SkyView and JournalView headers stop describing compositions that were inverted or landed long ago; CLAUDE.md's repository layout learns about World/, Anima/, the weather, the animals, the fireworks and the classic navigation's true (flag-gated) status.
Everything to verify on device since v1.2, code-verified: the core loop, quiet, One World navigation, the Path, auto-onward, the new sky scroll, all six weathers, balloon animals, the stage mechanics, fireworks, the journal, 3-day settling, accessibility, and what should NOT be found. Items this branch fixed are marked so the pass exercises them deliberately; accepted rough edges are listed so nobody hunts them as bugs.
The DevReset key audit demands that every declared key is actually written, and the map's two keepsake keys are written only on the corruption path — so the test now exercises that path like any other write path: unreadable bytes seeded first, and the store's construction is the write. The keys stay declared, because the fresh-install wipe must clear a keepsake too. The firework quiet test's clearing loop could livelock: it only stepped when the surface was empty, but a tap that lands on a burning fuse hurries it — and a hurried fuse pins at 0.995, finishing only in step(). An orb sitting on a burning cord was therefore hurried forever and never popped (seeds 34 and 36 found it). The loop now steps every iteration, so a stolen tap's fuse burns down, launches and spends, and the orb beneath becomes tappable again.
GLESAV
marked this pull request as ready for review
August 29, 2026 07:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Six parallel reviewers audited the merged One World + Anima work, each owning one domain: simulation purity and guardrails, world input/navigation, rendering/performance/accessibility, audio/haptics/persistence, the Anima engine and CI, plus a code-verified inventory of every user-facing feature. This PR lands the high-confidence fixes;
docs/e2e_walkthrough.md(also in this PR) is the device pass built from the inventory.The headline finding
The sky scroll — the headline feature of PR #9 — never ran on device. The input arbiter decided "is this touch a grab of a world in flight" from
!isFieldAtRest(), a predicate that is false at the resting sky, so every sky touch armed the camera at touch-down and the scroll room was never consulted. The test suite proved the split's math while never driving the shipped wiring: every scroll fixture saidisFieldAtRest: { true }, a state in which production never offers scroll room, and the one transit-grab test asserted precisely the broken production shape.The fix splits the decision onto its own live closure (
isCameraAtRest); the scroll fixtures now run the production shape and pin the regression. Fixed with it: a resting thumb on empty sky no longer disables every star's hit test and dims the world, and a sub-slop wobble at the sky moves the world exactly as far as on the field — not at all. The glide also now steps the offset itself (Canvas content isn't animatable, sowithAnimationglided only the invisible tap targets while the drawing snapped; catching a glide leapt under the finger).The other fixes
.waitingshell could never spend once the field completed (taprefuses completed fields), soisQuiescentheld the frame clock awake at full rate forever. Untouched shells now fade with the field, asFirework.swift's own header always promised.WeatherField.presenceto zero and quiescence waits for it.stonesempty, genesis re-laid, and the first save overwrote the only copy. A future release adding a stored property toMapStonewould have done this to every upgrading user. The unreadable bytes now move to a keepsake key before anything can write over them..tapsgenerator at the 26-orb ceiling was a dead tap — consumed, no orb, no sound, no haptic. A direct press now forces its orb through (the ceiling bounds ambient emission; presses are bounded at 3–5).start()failures until relaunch; now rebuilt on reset) and no route-change recovery (now restarted on the configuration-change notification, with a last-instantisRunningre-check narrowing the known AVAudioPlayerNode exception window).pow(damping, f), probability × f, and √f on the random walk; at f = 1 every value is byte-identical to the tuned reference, so nothing tuned changed.cornerednever faded against a signage band and it jittered there. Now measured against the live insets, same as the bounce.Int()conversion (exporter today, the phone the day voices are adopted). Now renders as silence; both sample clamps NaN-safe.innerHTMLraw; the first ampersand in a flavor line broke the card, and on the Pages origin a craftedlibrary.jsonwas script injection.PopCatalogand written by a test, but the path filter watched onlyVesper/Anima/**; a catalog edit shipped a stale gallery.tools/__pycache__untracked and ignored, two wrong GameConfig comments corrected (values untouched), stale SkyView/JournalView header comments brought up to date, and CLAUDE.md's repository layout refreshed.Deliberately not fixed (noted for a tuning pass — tuning is sacred)
Audio clipping headroom in large chains (needs a limiter decision), the detune-after-pentatonic-snap dead variant system, split/emit/startle pitch clamping, sound truncation under pool wraparound, the starfield re-rasterization caching, the
splits/twinklesfirework behaviors (authored but unimplemented), sub-44 pt star patches in crowded sky rows (documented trade-off), and the Pages deploy job'scontinue-on-error(kept so a disabled Pages setting cannot fail CI).Verification
No gameplay constant changed;
GameConfig.swiftedits are comment-only. New/updated tests: the production-shaped scroll fixtures and touch-down regression,testASubSlopPressAwayFromTheFieldMovesNothingAtAll,testAClearedFieldGoesQuietEvenWithUnlitShells(6 seeds),testAClearedFieldGoesQuietWhateverTheAir(4 weathers),testAnUndecodableMapIsKeptAsideNeverOverwritten,testAVoiceAuthoredWithNaNRendersAsSilenceNotACrash,testNoTwoVoicesShareAName. The previewer's script passesnode --check; both workflows parse.The e2e walkthrough
docs/e2e_walkthrough.md— the definitive device checklist: core loop, quiet, navigation, the Path, auto-onward, the sky scroll, all six weathers, balloon animals, stage mechanics, fireworks, the journal, 3-day settling, accessibility (Reduce Motion / VoiceOver / Dynamic Type / iPad / ProMotion), what should not be found, and the accepted rough edges — with the items this branch fixed marked so the pass exercises them deliberately.Generated by Claude Code