← All decisions

Rename pe to physics, pair render colors with entities instead of a parallel list, give render.Canvas a public constructor, and fix doc-comment drift left by decision 0047

architectureapi-designphysicsrenderrefactor

Context

Decision 0047 formalized the Core API boundary (pe/render/io vs. glemy/games/<name>) but was reviewed critically afterward, on request, against the actual concrete API surface rather than just the boundary drawing. Four real problems were found: (1) pe's own module doc comment still described itself as 'Physics Engine & Logic', a name stale as soon as decision 0047 moved the 'Logic' half out -- inaccurate, and confusable with the umbrella term 'Core API' docs/technical-architecture.md already uses for the whole pe+render+io surface. (2) render_entities_to_bytes/render_entities_to_canvas took entities and colors as two separate same-length lists kept in sync only by caller convention -- a real, reproducible bug, not a hypothetical: render_ffi.mjs's drawEntities indexed colors[i] alongside entities.forEach, so a caller-side length mismatch type-checked fine in Gleam and only surfaced as an uncaught JS exception at runtime. (3) render.gleam declared pub type Canvas with zero public constructor -- the only function that could build one from a real <canvas> element, get_canvas_element, was private inside glemy/game.gleam, making render unusable as a standalone Core API module by any consumer other than glemy/game. (4) io.gleam's header doc comment still said input was read 'for glemy/pe to read once per frame', inaccurate since pe never read input directly even before this session -- glemy/game's loop always did.

Options considered

Rename pe to core — rejected
docs/technical-architecture.md already established 'Core API' as the umbrella term for the whole genre-agnostic surface (pe + render + io + collision_sweep), not any single module. Naming the physics module core would collide with that already-in-use term and make every future reference to 'Core' ambiguous. physics is plain, accurate, and collides with nothing.
Keep entities/colors as two parameters but add a runtime length-check with a clear error message instead of restructuring the API — rejected
Would turn an uncaught-exception crash into a caught-Result crash, which is strictly better but still leaves the mismatch representable in the first place -- a caller can still construct the wrong pair of lists and only find out by running the code. Zipping entities and colors into one List(ColoredEntity) makes the mismatch impossible to construct at all, a stronger guarantee for a comparable amount of code change, matching this project's existing preference for encoding invariants in types over runtime checks wherever the type system can actually express them.
Give render.Canvas a public constructor but leave it in game_ffi.mjs, just marking the Gleam wrapper pub — rejected
Canvas is render.gleam's own opaque type; its one real constructor belongs in the same module for the same reason every other type in this codebase owns its own constructors -- a future runner for a second game would otherwise still need to reach into glemy/game's FFI file (or duplicate a getElementById call) just to obtain a value of a type it doesn't otherwise depend on glemy/game for at all.

Decision

Renamed glemy/pe (module, directory, and every test file) to glemy/physics throughout src/ and test/, and updated every import path and qualified reference accordingly (mechanical, ~24 files) plus every doc comment that named the module by its old name -- decisions.jsonl and development-plan.jsonl entries predating this rename were left untouched as accurate historical record. Replaced render_entities_to_bytes/render_entities_to_canvas's separate entities/colors parameters with a single List(ColoredEntity) (ColoredEntity = #(Entity, #(Float, Float, Float))), updated render_ffi.mjs's drawEntities to destructure [entity, [r, g, b]] pairs directly (Gleam tuples compile to plain JS arrays, confirmed by inspecting the compiled runtime, so this needed no wrapper), and renamed games/tiers.entity_colors to games/tiers.colored_entities to build the zipped list at the source. Added render.get_canvas(id: String) -> Canvas (@target(javascript), wrapping a getCanvasElement FFI function moved from game_ffi.mjs into render_ffi.mjs, where Canvas's owning module also owns its construction), and updated glemy/game.gleam to call it instead of its own former private copy. Fixed io.gleam's stale header doc comment to describe input as read by a runner (glemy/game's loop) and folded into a game's own Input, not read by glemy/physics directly.

Verification

gleam test and gleam test --target javascript both pass with identical counts to before this change (164 Erlang / 187 JavaScript) -- the rename and API reshaping changed no runtime behavior, only structure and signatures, confirmed by every existing assertion still passing unchanged in substance (render_test.gleam's assertions were rewritten to the new call shape but assert the identical pixel outputs). deno task check-warnings passes after updating the baseline (51/51, down from 52 -- one warning genuinely disappeared: render.gleam's Entity import is no longer flagged unused on the Erlang target, since ColoredEntity's type alias uses it ungated, a real incidental improvement, not just a renamed path) -- every other diff was confirmed as the same already-documented @target(javascript) target-gating artifact, just under renamed paths. deno task browser-check passes for real (182 real requestAnimationFrame frames, a real click-and-hold grew the entity count 4 -> 5, a real entity spawned near the expected world-x, zero page errors) -- this also directly exercises render.get_canvas for the first time as a public entry point, since glemy/game's main() now calls it exclusively to obtain its canvas.

Consequences

glemy/physics is now an accurate, unambiguous name that won't collide with 'Core API' as a term. render_entities_to_bytes/render_entities_to_canvas's caller-side mismatch bug class is now unrepresentable rather than merely undocumented -- every existing and future caller (currently only glemy/game.gleam) must construct the pairing correctly by construction. render.gleam is now actually usable as a standalone module by a future runner: it owns both Canvas and the one way to construct one, with no dependency on glemy/game's private FFI. This is the same category of work as decision 0047 (in-repo boundary hardening of already-real code, not new capability) and does not itself change docs/technical-architecture.md §2.3's extraction trigger.

References