Decompose wall-handling into caller-parameterized Core primitives (resolve_axis/resolve_high_only), parameterize physics.update's wall step, instead of leaving it unused by 2 of 3 games
Context
physics.update was used by only 1 of glemy's 3 reference games: its wall-handling step was hardcoded to bounds.bounce, whose restitution tuning is Tiers-specific (decision 0029), so Breakout and Platformer each hand-composed entity.integrate + collision_sweep.resolve_all_collisions plus their own bespoke wall function instead of calling update at all (decisions 0052/0059). Asked directly whether the fix should be to retire update now that most games bypass it, versus restructure it into composable sub-functions Core still owns so capability keeps growing while each game's own code gets simpler -- the latter was chosen, mirroring the exact caller-supplied-callback generalization collision_sweep's interact already established (decision 0047) for the pairwise-collision half of the same composition, now applied to the wall-handling half too. Direct comparison of the three games' actual wall-handling code (not guessed) found the real, already-independently-duplicated shape to extract: bounds.gleam's private bounce_axis, games/breakout.gleam's bounce_axis_full, and games/platformer.gleam's stop_axis were byte-for-byte identical in structure, differing only in the velocity transform applied on each edge; games/breakout.gleam's bounce_top_only and games/platformer.gleam's stop_at_ceiling matched a second, smaller shared shape (max-edge-only).
Options considered
Decision
Added physics/bounds.resolve_axis(position, velocity, min, max, on_low, on_high) and resolve_high_only(position, velocity, max, on_high), both taking the velocity transform as a caller-supplied fn(Float) -> Float. bounds.bounce, games/breakout.bounce_off_walls, and games/platformer.stop_at_walls all refactored to build on these instead of their own private near-duplicate clamp-and-branch functions (bounce_axis/bounce_axis_full/stop_axis/bounce_top_only/stop_at_ceiling all deleted), each now supplying only its own specific velocity transform as a closure -- zero behavior change, confirmed by every existing wall-behavior test passing completely unchanged. physics.update gained a wall_behavior: fn(Entity, Bounds) -> Entity parameter instead of hardcoding bounds.bounce; games/tiers.gleam's one call site now passes bounds.bounce explicitly (same behavior, now visible rather than implicit). Breakout/Platformer deliberately do not switch to calling update itself, for the Entity-vs-List(Entity) Model-shape reason above -- they already benefit from the same resolve_axis/resolve_high_only primitives update itself is now also built from, without adopting update's own List(Entity) orchestration.
Verification
gleam test and gleam test --target javascript both pass -- 288 Erlang / 315 JavaScript, exactly the prior baseline (281/308 as of decision 0061) plus 7 new resolve_axis/resolve_high_only tests (target-agnostic, so both counts grew identically), with every pre-existing wall-behavior assertion in bounds_test.gleam/breakout_test.gleam/platformer_test.gleam/physics_test.gleam/tiers_test.gleam passing completely unchanged -- the actual regression proof that the extraction preserved every game's exact original transform. deno task check-warnings passes with the baseline completely unchanged (104/104, zero new warnings). All three real browser-checks (browser-check/browser-check-breakout/browser-check-platformer) re-run and pass for real, confirming every game's actual wall/landing behavior in a live browser is unaffected.
Consequences
physics.update is now usable, in principle, by any future List(Entity)-shaped game with its own wall policy -- the actual reason it couldn't be reused past Tiers is fixed, not routed around. resolve_axis/resolve_high_only are genuinely reusable Core primitives with three and two real, already-migrated callers respectively, not speculative additions. A future game needing yet another wall/landing policy (a one-way platform, a portal, a sticky wall) supplies its own on_low/on_high closures to the same two functions rather than reimplementing the clamp-and-branch shape a fourth time. The Entity-vs-List(Entity) Model-shape distinction between Tiers and Breakout/Platformer is now explicit, documented architecture (in update's own doc comment) rather than an implicit fact a future reader would have to rediscover by reading three games' source.