Skip to content

A QA swarm: eleven new test suites, six production defects, and the docs made true - #12

Merged
GLESAV merged 22 commits into
mainfrom
claude/vesper-pop-aaa-optimization-2n5oc2
Aug 29, 2026
Merged

A QA swarm: eleven new test suites, six production defects, and the docs made true#12
GLESAV merged 22 commits into
mainfrom
claude/vesper-pop-aaa-optimization-2n5oc2

Conversation

@GLESAV

@GLESAV GLESAV commented Aug 29, 2026

Copy link
Copy Markdown
Owner

What this is

Sixteen agents in two waves: eight isolated builder sessions, each owning a disjoint set of new test files so no two could touch the same bytes, then eight 1:1 reviewer sessions, one per builder, each re-deriving that builder's constants, auditing every symbol reference, and hunting tests that cannot fail. Alongside them, two cleanup tracks on non-Swift files.

The suite goes from 23 files to 35, and from 11,238 lines to 18,887. Every session's verdict was PASS.

Why the reviewer wave was not optional

There is no Swift toolchain in this environment — CI is the only compiler — so a builder cannot check its own work. But the reviewers earned their cost on test quality, not just compilation:

  • S4's reviewer refuted its own builder. The unlock test claimed to be a non-vacuous equivalence. It would have passed with checkUnlocks() deleted entirely, because on nearly every run no threshold is crossed and only the "nothing new" branch executes. It was rebuilt to arrange the unlock, with a falsifiable guard.
  • S7's reviewer found a fuzz session whose invariants held trivially for all 14,000 frames: worldSettled/worldQuietened clear their flags on a main-queue hop, and nothing in the loop yielded, so both were set once and never cleared while ~10,000 deferred blocks queued for one drain at the end.
  • S6's reviewer re-derived every measured constant in Python (pop saturation 0.376147, ember glow 0.525862, WCAG mid-grey 0.214041 — all matched), widened two bounds sitting 1–4 authoring steps from failure, and collapsed a 267,000-assertion loop to 1,392 with the identical property.
  • S3's reviewer re-computed all seven stage rows, all seven reachable-orb counts and all six firework values, then measured every statistical bound under four different models of stdlib bit extraction so its conclusion survives its Python port not being byte-exact.
  • S2's reviewer independently recomputed all ten SplitMix64 golden vectors (byte-for-byte match) and rewrote a tautology that read a copied orb's fields back off the object it was copied from — true under value semantics, reference semantics and a broken copy alike.
  • S8's reviewer caught a harness divergence that would have made twelve journeys assert a slightly different game than ships, and closed a hermeticity leak where five probes shared one UserDefaults instance.

Production defects found and fixed

Sessions were told to report, never fix — none owns production files — so these are the orchestrator acting on what came back.

Crashes. ProgressionStore built its per-pop tally with Dictionary(uniqueKeysWithValues:), which traps on a duplicate key — and "7" and "07" both parse to 7. That is a crash on every launch, escapable only by deleting the app, which also deletes the journey the store exists to protect. In the sound engine, three inline Int(freq.rounded()) conversions (two feeding UInt64(), which traps again on a negative) and five AVAudioFrameCount(sampleRate * duration) conversions that ran before their own guard — so the guard could only catch exactly 0, the one bad value that does not trap.

A frozen field. frame() applies its work a main-queue hop later, and a restart() can land in between. The stale block set renderingPaused = true on a live field — which freezes it permanently, because that flag gates the timeline and the timeline is the only writer that would clear it — and applied the previous field's events into the new session. Work is now stamped with a field epoch and discarded whole.

A shove, in a game whose first rule is no pressure. During the 2.2-second pause in the sky, choosing a stone, leaving the Path or beginning again did not stop the already-scheduled step; two seconds later it overwrote her choice and carried her somewhere she had not asked to go. restart() now cancels the onward sequence, and the step item gained the sim.completed re-check its sibling always had.

A glide that would not stop. SkyScrollState.began() says a finger landing on a coasting sky catches it. It did not: began() is reached only from .scrollBegan, which the arbiter emits only from the slop-arming path — so the sky coasted on under her finger for ten points, and a tap never stopped it at all while the stars she was pressing kept moving. This is the same shape as the unreachable sky scroll the last PR fixed: each piece right, the connecting call on a path a touch-down never reaches. The input layer now has an onTouchDown hook, observational like onPointer, fired before any arbitration.

A flaky test in our own suite. Two MapStoreTests asserted a road brings a pop its parent lacked. branchedSet's pool excludes siblings and already-chosen pops but not the parent's others, so that was an intermittent failure on correct code.

Left for the owner — a feel decision, not mine

The balloon animal's startle is deleted by the weather in five of the six airs. applyWeather runs before AnimalMotion.step and ends by clamping every orb to the weather ceiling; the startle's 2.0 is gone before the animal's own raised ceiling is consulted, and a ceiling can only lower a speed. Measured: 6.3× to 9.4× shorter — about 25pt in clear air against about 8pt in rain, at constant speed, so it does not read as a dart at all. evadeMaxSpeed and animalMaxSpeed are likewise unreachable outside clear air.

stepOrbs' own comment says the ordering exists precisely to prevent this. Fixing it changes how the game feels in five of six weathers, and guardrail 5 says tuning changes are deliberate and one at a time — so it is flagged, not changed. The e2e suite's ceilings are written to the intended speeds so they survive a fix rather than needing correction downward.

Also open: the bankLimit of 14 against ~100 profiles prepared (which defeats the pre-render and pushes synthesis onto the tap path), the detune variants that render identically because pentatonic snapping happens after detune, and three authored event pitches that clamp to one bucket.

Cleanup tracks

Docs vs. code. pop_map.md still described the inverted lineage rule; pop_points.md had no row for the animal multiplier and described UI that exists only in the classic navigation; RESUME.md claimed "VesperApp still launches ContentView", which is false; PLAYTEST.md told testers to use a DEBUG reset that has no call site anywhere. Also fixed: my own e2e walkthrough had the navigation backwards — WorldCamera's sign convention is explicit that a finger moving up reaches the sky, so the journal is below.

Release readiness. The store listing described the v1.2 game and carried a claim W08 made false ("the road behind fades"). Description, release notes, promotional text, subtitle (28/30) and keywords (99/100) are rewritten; anxiety dropped as a health-claim risk. The privacy page now names what is kept on device and admits the one honest caveat. No-network re-verified by ~40 searches with zero real hits. docs/RELEASE_v1.3.md added.

Blocking for submission, and not fixable here: all ten screenshots render UI that no longer exists — a four-icon top bar, a counter labelled DETONATED, modal sheets titled "The Journey", and a caption reading "the road behind fades after three days". That is an App Store 2.3.3 accuracy risk. A scene-by-scene recapture plan is in the release doc. The project is also still at MARKETING_VERSION 1.2; 1.3 with build 2 is recommended, and whether this should be 2.0 is an open question given it is a navigation rebuild plus five systems.

Verification

Nothing here has been compiled — that is what this PR is for. An orchestrator-level check no single reviewer could do: all 35 test classes are uniquely named and none of the new files declares anything at top level, so cross-file symbol collision is structurally impossible. Estimated CI cost of the new suites: ~20–40s across both nav configurations.


Generated by Claude Code

claude added 22 commits August 29, 2026 07:52
An in-flight snapshot of the QA swarm's first wave, committed to keep the
working tree clean while the remaining sessions run. NOT YET REVIEWED AND
NOT YET COMPILED — there is no Swift toolchain in this environment, so CI
is the only compiler and none of these files has met it. No pull request
is opened against this commit on purpose: nothing here may reach main
until the 1:1 reviewer wave has checked every symbol reference, actor
isolation and determinism hazard, and until CI is green.

Eight isolated sessions were dispatched, each owning a disjoint set of new
test files so no two could touch the same bytes. Present here: the stores
(affirming-only invariants, unlock boundaries across the catalogue,
persistence round-trips), the pure core (SplitMix64 determinism and the
value-semantics property the weather seed depends on, entity value types),
the field plan and map generation (stage composition swept exactly, lineage
inheritance, distributions sampled with wide tolerances), and the audio and
haptics engines.

Still to land: the view-model bridge, the rendering helpers, the composed
world path, and the end-to-end journeys.
…uite

The swarm was told to report production defects rather than fix them — no
session owns production files — so these are the orchestrator acting on
what came back. All four are cases where correct-looking code can take the
app, or CI, down.

A CORRUPT TALLY CRASHED THE APP ON EVERY LAUNCH. ProgressionStore built
its per-pop counts with Dictionary(uniqueKeysWithValues:), which TRAPS on
a duplicate key — and two stored keys can parse to the same number without
this store ever having written them that way: "7" and "07" both answer 7.
The failure mode is the worst one this file could have. Her whole journey
lives in it, and the only way out of a launch crash is deleting the app,
which is also deleting everything it was protecting. Damaged defaults must
cost her a tally, never the app.

featuredPop = 0 IS NORMALISED ON THE WAY IN, not only on the way out. 0 is
the on-disk spelling of "no favourite", so a 0 assigned in memory survived
until the next launch and meant pop #0 in between — and there is no pop #0.
fieldPops() would answer [0], breaking its own promise that a field only
holds pops she has earned. Nothing assigns 0 today; now nothing can, and
the value read from disk is checked against the catalogue rather than
against a sentinel.

TWO TRAP PATHS IN THE SOUND ENGINE, in a file whose header promises that
"every failure path degrades to silence, never to a crash":

  * three separate inline Int(freq.rounded()) conversions, two of which
    then did UInt64() on the result. Int() of a Double traps on NaN, on
    either infinity, and past Int's range; UInt64() of an Int traps again
    on a negative. There is now exactly one frequency-to-key conversion in
    the file and it is total.

  * every buffer maker computed AVAudioFrameCount(sampleRate * duration)
    BEFORE its own guard on that count — and AVAudioFrameCount is a UInt32,
    so a negative, NaN, or over-97-second duration trapped at the
    conversion. The guard could only ever catch exactly 0, the one bad
    value that does not trap. All five makers now go through one guarded
    helper. A profile is hand-authored data and prepare() renders whatever
    it is handed, so a mistyped duration must cost that pop its sound and
    nothing else.

AND A FLAKE IN OUR OWN SUITE. Two MapStoreTests asserted that a road brings
a pop its parent did not have. It reads well and it is not guaranteed:
branchedSet builds its pool as unlocked minus the siblings' takings minus
what it has already chosen, and the parent's other pops are in none of
those, so a child may legitimately redraw one. Genesis seeds are random, so
this was an intermittent failure on correct code — a few percent per road,
and a fork has up to three. Both now assert the form that is universally
true by construction: a road always carries more than the one pop it
inherited, and still keeps something from its parent.
Still in flight and still unreviewed — no pull request against this
commit either. The field-plan and map-generation suites landed their
final edits, and the view-model, rendering and end-to-end journey files
arrived. The 1:1 reviewer wave is what turns any of this into something
that may reach main.
The 1:1 reviewer for the SplitMix64 and Entities suites recomputed all
ten pinned golden vectors independently in Python — including the
UInt64.max case that exercises the wrapping add — and every one matches
byte for byte. It measured each statistical bound against the real fixed
stream rather than trusting the builder's word: bit balance sits about
fourteen standard deviations inside its bound, the avalanche mean thirty,
and the tightest of them (the low-nibble bucket) still 2.6 sigma clear.
Nothing needed widening, and every one of those tests is deterministic,
so none of them can flake.

Five constructs were rewritten because they had no precedent anywhere in
this suite and could not be checked without a compiler — a mixed-type
UInt64 stride, two mutating draws inside an assertion's autoclosures, an
inout argument inside string interpolation, and an implicit member on the
far side of a generic pair.

One test was a tautology and is now stronger: it read every field of a
copied orb back off the object it was copied from, which is true under
value semantics, reference semantics and a broken copy alike. It now
pins those twelve fields against the literals the parent was built from.
Its sibling — the tripwire that fails if Orb ever becomes a class — was
checked and really does do what it claims.
GameViewModelTests (21) pins the scoring formula factor by factor through
the published surface — each rarity's base, the size factor as a
multiplier rather than an addend, the fortune bonus added after the size
factor rather than scaled by it, the chain curve to its saturation point,
and a scripted three-field evening in which no lifetime number ever goes
down. RenderingTests (20) covers what can be reached without a
GraphicsContext: totality across 1,440 horizon geometries, the sRGB
luminance measure that guardrail 4 is enforced with, 444 pop colours and
every shell colour against saturation and pure-white ceilings, and the
property that every pose in the Anima library fits inside the halo the
renderer sizes from it.

Both still await their 1:1 reviewers, and neither has been compiled.
A rolling snapshot while the reviewer wave works. The stores, audio and
haptics suites are being audited symbol by symbol by their 1:1 reviewers,
and the end-to-end journey suite is still being written. Nothing here is
cleared yet and there is still no pull request against it.
A rolling snapshot. An orchestrator-level integration check passed
meanwhile — the one no individual reviewer can perform, since each sees
only its own files: all 37 test classes across the suite are uniquely
named, and none of the eleven new files declares anything at top level,
so every helper is nested inside its own class and a cross-file symbol
collision is structurally impossible.
Twelve hermetic journeys — own defaults suite per test, own injected
clock, fixed seeds, hard caps on every loop — covering a fresh install
through a dozen fields, unlock monotonicity across a whole evening, the
map growing and never shrinking, a replayed stone getting bigger while
staying bounded and finishable, the deepest field the game can build,
persistence across a relaunch, and the three-day settling probed one
second either side of its threshold and with the clock moved backwards
to the epoch.

Its most valuable output is a finding rather than a test: the balloon
animal's startle appears to be deleted by the weather in five of the six
airs, and the comment in stepOrbs claims the ordering exists precisely to
prevent that. Its reviewer is verifying the frame order and computing the
actual clamped speeds before I touch anything — this one changes how the
game feels, so it is the owner's call, not mine.
Two tracks that run safely beside the QA swarm because they touch no
Swift: the docs are being reconciled against the code (pop_map's lineage
rule still described the inverted version, pop_points had no row for the
animal multiplier), and the store-facing material is being brought up to
the build main actually carries. Also the map-generation suite's reviewer
edits. All still in progress.
Every hard-coded table was re-derived from source rather than trusted:
all seven rows of the stage composition, all seven reachable-orb counts
(which compound splitter generations, so they were recomputed in Python),
all six firework spot values, and the claim that generation twelve is the
exact first one at the cap. Every one matched.

The statistics were measured rather than assumed, and then measured
again: because no Swift runs here, the reviewer re-ran all six
proportions under four plausible models of how the standard library
extracts bits from the generator. Every model landed inside every bound,
so the result does not depend on the port being byte-exact. The tightest
margin is the visitor share's upper edge at about three sigma. Nothing
needed widening, and all three tests draw from one fixed seed stream, so
they cannot flake.

One hardening change: the determinism fingerprint was a seventeen-element
heterogeneous array literal mixing property reads, ternaries and call
expressions — the classic shape that makes Swift give up type-checking an
expression. With no local compiler that is not a risk worth carrying, so
it is now a homogeneous literal with the ternaries hoisted out.

Two comments were corrected because they overstated the tests: the road
roll's bounds are not all plus or minus 0.08 (the three-way is tighter),
and the pop-count test pins the ROLL, not what a child stone ends up
holding — branchedSet raises that to at least two.
The release-readiness track has reached the description and release
notes — both were still written for v1.2, a game without weather,
animals, fireworks or One World — and has begun docs/RELEASE_v1.3.md.
The docs track is working through BUILD_PLAN, marking what is done, what
the GDD superseded, and what remains, so a reader is not misled about
where the project actually stands. Both still in progress.
…s rewritten

MY OWN DOCUMENT WAS WRONG about the most basic gesture in the game. The
e2e walkthrough said the journal is reached by swiping UP from the field.
It is not. WorldCamera's sign convention is explicit — a finger moving up
the screen makes a negative translation, which drags the camera to a
negative offset, which is the SKY — and commitDirection agrees: a
negative translation commits .up. So the journal is below, reached by
swiping down. The release-readiness track caught the contradiction
between the code and the checklist; the code is right.

Both the committed walkthrough and the published checklist now say so,
and section III gained an explicit direction check so a tester can tell a
bug from the design rather than wondering which of us is confused.

THE STORE LISTING IS REWRITTEN for the build main actually carries. The
description still sold v1.2 and contained a claim that is now false — the
A rolling snapshot: the S4 and S6 reviewers are mid-audit on their files,
and the docs track has reached CLAUDE.md. None of it cleared yet.
The world-integration session found the exact class of defect it was
created to find, in code from earlier in this same session. SkyScroll's
began() says a finger coming down on a gliding sky is a CATCH — the glide
stops where it actually is, the way a real scroll view does. It did not.
began() is reached only from .scrollBegan, and the arbiter emits that
only from the slop-arming path, ten points into a drag. So the content
coasted on under her finger for those ten points, and a TAP on a gliding
sky never stopped it at all — while the stars she was pressing, which are
positioned from the scroll offset, kept moving out from under the press.

Each piece was individually right and the call meant to connect them sat
on a path a touch-down never reaches. That is the same shape as the
unreachable sky scroll this branch already fixed, which is why a session
was pointed at the composed path rather than at any one component.

The catch now happens where it belongs. The input layer gained an
onTouchDown hook — strictly observational, like onPointer: it produces no
outcome, consumes none, and cannot change what the arbiter decides — and
it fires unconditionally in touchesBegan, before anything is arbitrated,
so a tap stops a glide exactly as a drag does. began() keeps its second
job of anchoring the gesture at arm time.
…laimed

The view-model session's reviewer confirmed all three of its builder's
findings against source. These are the fixes.

RESTART NOW CANCELS THE ONWARD SEQUENCE, and this is the one that matters
to her. During the 2.2-second pause in the sky — precisely the moment she
is looking at the roads ahead — choosing a stone, leaving the Path, or
beginning the field again did not stop the step that was already
scheduled. Two seconds later it overwrote her choice and carried her
somewhere she had not asked to go. The sequence is meant to be an offer
she can decline, and choosing something else is the clearest way anyone
declines anything. The step item also gained the sim.completed re-check
its sibling always had, as the belt to that brace.

A FIELD IN FLIGHT IS NOW STAMPED WITH AN EPOCH. frame() applies its work
a main-queue hop later, and a restart can land in between — UIKit touch
delivery is a run-loop source, not FIFO with the main queue, so it
genuinely can. The stale block then did two bad things to a field that
had already begun again: it set renderingPaused on a LIVE field, which
freezes it permanently because that flag gates the timeline and the
timeline is what would have cleared it; and it applied the previous
field's events into the new session, where a pending clear would draw a
verse and show the done card over a field with orbs still on it. Work
from a field she has already left is now discarded whole.

AND THE FORTUNE IS PINNED. Every document says exactly one fortune orb
per field; nothing held the code to it. seedField picks it from the
surface orbs that are plain, with randomElement, which answers nil for an
empty collection — so a plan that dealt no ordinary orb onto the surface
would give her a field with no fortune, silently. The new test sweeps
every stage against every generation the plan reaches and proves that
cannot happen, which is a better answer than defending against a case
that does not exist. It also pins that the fortune never rides a
splitter, a generator or a creature: on the busiest thing on screen it
would be a prize, and on a plain orb it is a gift.

Also corrected two comments that disagreed with their own code: a stone's
family tie-break is the alphabetically first family name, not the lowest
pop number (a stone holding one Vesper and one Aurora leans Aurora), and
the counter is 30 pt, not the 40 the decluttering note claimed or the 62
a later comment still described.
Its reviewer is auditing the harness against GameViewModel — that
comparison is the whole value of the file, since the harness
re-implements the real bridge's event-to-store path and a divergence
would mean the journeys assert a game that does not exist.
The reviewer audited the harness against GameViewModel line for line,
which is the whole value of this file — the harness re-implements the real
bridge because the bridge is not injectable, and a divergence would mean
twelve journeys asserting a game that does not exist. They agree on all
four seed inputs, on every event-to-store recording, and on the order of
the two clears, which matters. It found and fixed one real divergence: the
harness sized the field before saying what the field was, so it seeded a
stage-0 field, threw it away, and re-seeded from an already-drawn
generator.

It also closed a hermeticity leak the builder missed — the threshold
probes reused one UserDefaults instance across five stores, relying on a
domain wipe being visible to an object that had already read those keys —
and added popCounts to the ledger, so a per-pop tally that silently lost an
entry is now caught rather than passing as "no counter decreased".

The stall guard now lives inside the field-playing helper and names the
stage, generation, orb count and taps, so any journey that fails to
progress fails at the field where it happened rather than at whichever test
happened to assert the cap.

It confirmed the animal-startle bug with numbers: the dart is 6.3 to 9.4
times shorter in weather, covering about 25 points in clear air and about 8
in rain, at a constant speed — so it does not read as a dart at all. Two
consequences follow that nothing had noticed: evadeMaxSpeed and
animalMaxSpeed are both unreachable outside clear air, which means the
creature is permanently slower than the constant that governs it. The calm
test's ceilings are written to the INTENDED speeds, so they survive a fix
rather than having to be corrected downward afterwards, and the comment now
says why.
… complete

The last of the eight. Its reviewer compared the harness against
WorldInputView and WorldView line by line — ten points of comparison — and
found the harness was missing both observational hooks production calls:
onTouchDown (the glide catch this branch added an hour ago) and onPointer,
in all four handlers. Both are now called in production's exact positions.
Two further differences, the lost-touch recovery and multi-touch adoption,
are deliberate and now documented as unreachable here rather than dropped.

A new test pins the touch-down path: a TAP at the resting sky asks the
arbiter for nothing, which is precisely why arming was too late a place to
catch a glide; the touch-down call nevertheless runs, leaves offset, room,
camera and both flags untouched, is idempotent, and does not come between
her finger and a pop at the field.

The reviewer declined to write the obvious companion test and was right
to. Catching a glide is only observable as an offset that stops moving,
and the glide steps on a Task that sleeps sixteen milliseconds and reads
the wall clock — so any assertion would either wait on real time or pass
for the wrong reason. It says so in the file instead, and names the fix:
an injectable clock on the scroll state.

One more vacuous test found, in the place hardest to notice. The fuzz
session's latch implications held trivially for all fourteen thousand
frames, because worldSettled and worldQuietened clear their flags on a
main-queue hop and nothing in the loop ever yielded the main actor — so
both flags were set once, early, and never cleared, while ten thousand
deferred blocks queued up for a single drain at the end. The loop now
drains every ten steps, which turns the clearing path into real coverage
and bounds the backlog.

Every loop in the file now fails rather than gives up: runToIdle used to
hit its twenty-thousand-frame cap silently.
631 tests ran on the first build of eight thousand new lines. Everything
compiled — the reviewer wave's symbol audits did the job they were for —
and 626 passed. The 136 failure lines collapse to five tests and two
causes, and neither was a mistake in the test's reasoning.

SWIFTUI'S Path ROUND-TRIPS COORDINATES THROUGH SINGLE PRECISION. Four
rendering tests read points back out of a Path and compared them to the
Doubles that went in. The evidence is unambiguous: sin(50°) went in as
0.766044443118978 and came back as 0.7660444378852844, which is exactly
the nearest Float; a 736-point coordinate came back one Float ulp away.
The session's own reviewer had flagged Path inspection as the file's one
unexercised platform unknown, since nothing else in the repo walks a
Path — this is that risk arriving, and it is a fact about the platform
rather than a fault in the code under test.

A fixed absolute tolerance cannot serve those four tests, because the
same error is five parts in a billion on a unit circle and six parts in a
hundred thousand on a screen coordinate. They now share one documented
helper that scales with the magnitude being compared: eighty times
Float's own resolution, which absorbs the accumulation and is still far
tighter than any real geometric error — a silhouette drawn in the wrong
place is wrong by points, not by millionths.

AND BUILDING A STORE WROTE TO DISK. "Constructing the store wrote
vesper.progress.featured before the player did anything" — assigning that
property in init runs its observer, and the observer persists. Reading is
not writing, and a store that writes on launch cannot answer whether
anyone has ever played. It now refuses to write a value the store already
holds, which is the smaller and more honest guard: it fixes construction
without adding a flag for init to special-case, and it stops needless
churn everywhere else too.

Also in this push, from the performance track: the sky's deep field —
338 ellipse fills, two screen-sized gradients and about sixteen hundred
RNG steps — was being redrawn on every published scroll change, which
during a drag is digitizer rate, despite depending on nothing but the
view size. It is now an Equatable subview keyed on that size, which is a
purely subtractive change: it removes SwiftUI's reason to re-run the
subtree and cannot alter a pixel or a hit target. The star labels also
stop rebuilding a lowercased catalogue name per star per frame.
The dead-code track's design note on the two authored-but-unimplemented
break behaviours: what each was meant to do, which catalogue entries set
them and what their flavour text promises, where each would have to be
consumed, and the photosensitivity question the twinkle raises for a game
whose guardrails forbid anything flashing. It ends where it should — with
the choice between implementing them and retiring the flags, which is the
owner's.
Removed, each proved dead by a repo-wide grep including the tests: three
Strings entries, GameConfig.paintCount, FireworkKind.name (which was
rawValue with no authored content, and whose doc comment claimed a
VoiceOver use it never had), and PopFamily.displayName — not on the list,
found by the sweep, and Title Case, which the lowercase-calm guardrail
would have rejected the moment it reached a surface.

Weather.name was KEPT. Unlike FireworkKind.name it is six hand-authored
words with no equivalent anywhere, and it is what a VoiceOver reading of
the field would need. Its comment now says plainly that nothing uses it,
that the air is deliberately never named in UI, and that those six words
are not in Strings.allStrings — so the voice guardrail does not cover
them, and they must move there before they ever ship.

This test's comment named the three deleted strings as examples. It now
records what happened to them, which of them are recoverable and which is
gone for good, and — while it is being edited — the hole in the test
itself: it cannot tell the two navigations apart, so fortuneDismissHint
counts as wired on the strength of a call site only the classic build
constructs.
…ssumed

From 136 failures to three, and the three corrected my explanation. I had
reasoned that SwiftUI's Path round-trips through single precision, which
is true of one vertex — sin(50°) comes back as exactly the nearest Float —
but understates what happens to a value DERIVED from a path by about two
orders of magnitude.

CI measured it at three scales in one run: 3.899719 against 3.899658,
26.323059 against 26.322693, and 779.942352 against 779.931641. That is a
relative error of 1.4e-5 at every magnitude, near enough 2^-16 — roughly a
hundred times coarser than Float's own 1.2e-7, because the comparison is
between two separately quantised measurements rather than between one
value and its rounding. My 1e-5 bound sat just underneath it.

The tolerance is now 1e-4 relative, which is about seven times the worst
error observed and still far tighter than any real geometric fault: a
silhouette drawn at the wrong scale is wrong by percent, not by
thousandths of a percent. The comment records the measured numbers rather
than the theory, so the next person to touch this does not have to
rediscover them.

Two assertions were also keyed to the wrong quantity — they scaled their
tolerance to the centre a measurement was taken from rather than to the
extent or delta actually being compared. They now account for both, since
both measurements are quantised and one of them is quantised against a
much larger number.
@GLESAV
GLESAV marked this pull request as ready for review August 29, 2026 20:14
@GLESAV
GLESAV merged commit caf10bd into main Aug 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants