<?xml version="1.0" encoding="UTF-8"?>
<!-- lustre:map --><feed xml:lang="en" xmlns="http://www.w3.org/2005/Atom"><title type="html">glemy devlog</title><subtitle type="html">Every real architectural decision behind glemy, in order.</subtitle><id>https://recregt.github.io/glemy-website/</id><link href="https://recregt.github.io/glemy-website/feed.xml" rel="self" type="application/atom+xml"/><link href="https://recregt.github.io/glemy-website/devlog" rel="alternate"/><updated>2026-08-09T00:00:00Z</updated><author><name>glemy</name></author><entry><id>https://recregt.github.io/glemy-website/devlog/0067.html</id><title type="html">#0067 — Publish glemy 0.1.0 to Hex</title><link href="https://recregt.github.io/glemy-website/devlog/0067.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Ran `gleam publish -y`, supplying the required semver-below-1.0.0 confirmation phrase, with the user-provided Hex.pm API key exported for that single invocation only. Publish succeeded: glemy 0.1.0 is live at https://hex.pm/packages/glemy, with generated HexDocs. Pushed the `v0.1.0` git tag gleam publish itself requested, so HexDocs&#39; source links resolve correctly. The API key was written to a local, 0600-permission file (never echoed in any tool output or commit), used once, then securely deleted (shred -u) immediately after the publish succeeded.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0066.html</id><title type="html">#0066 — Declare javascript as gleam.toml&#39;s default target, discovered as a real gleam publish blocker</title><link href="https://recregt.github.io/glemy-website/devlog/0066.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Added `target = &quot;javascript&quot;` to gleam.toml&#39;s top level. This only changes which target ambiguous CLI invocations (a bare `gleam test`/`gleam run`, and `gleam publish`&#39;s own validation build) default to when no `--target` flag is given -- it does not restrict or change what actually compiles: `gleam test --target erlang` and `gleam test --target javascript` both still pass in full (170 / 190) after this change, confirmed directly, not assumed. Chosen as the honest default given the package&#39;s own actual shape: `io.gleam` (and much of Shell) is javascript-only by design, and the browser demos are this package&#39;s primary real-world consumption path -- erlang support is real, tested, and valuable, but was never a deliberate default to begin with, only Gleam&#39;s own unexamined tool-wide fallback.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0065.html</id><title type="html">#0065 — Set Hex-publish metadata (0.1.0, BSD-3-Clause) and audit the public API surface, informed by glemy-games as a real external consumer</title><link href="https://recregt.github.io/glemy-website/devlog/0065.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">gleam.toml: version 0.1.0 (not 1.0.0 -- the user&#39;s own call, an honest &#39;first real external consumer, API may still move&#39; signal rather than the stability promise 1.0.0 implies), licences = [&quot;BSD-3-Clause&quot;] (the user&#39;s own choice), description, repository, and links (live demo, glemy-games) filled in. Added LICENSE (BSD-3-Clause) to both glemy and glemy-games (the latter had none, being a new public repo). Audited every pub item across physics.gleam/physics/*/render.gleam/io.gleam by checking glemy-games&#39; actual import/usage grep output, not assumption: found every module genuinely externally consumed (no internal_modules candidates), 100% doc-comment coverage on every pub item (a few apparent gaps in an automated line-before check turned out to be doc comments separated by @target(javascript)/@external annotation lines, not real gaps), and no naming inconsistency that survived reading the actual code -- several apparent asymmetries (x_fraction vs y_fraction_from_top; overlap vs overlaps; restitution/genuine_impact_velocity duplicated verbatim across bounds.gleam and collision.gleam) turned out to be already-deliberate, already-documented, already-cross-referenced design choices, not gaps.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0064.html</id><title type="html">#0064 — Extract the three reference games into a new glemy-games repository</title><link href="https://recregt.github.io/glemy-website/devlog/0064.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Created recregt/glemy-games (public). Moved games/tiers.gleam(+/rules.gleam), games/breakout.gleam(+brick/paddle.gleam), games/platformer.gleam, all three game_&lt;name&gt;.gleam/_ffi.mjs runners, the shared game_ffi.mjs, all three HTML entry points, every mirroring test file, and the browser-check tooling (browser_check*.ts + browser_check_shared.ts) into it, under src/glemy_games/ (module path glemy_games/... for game-specific code; imports of glemy/physics, glemy/render, glemy/io, etc. stay unchanged, resolving through the new git dependency). glemy-games&#39; gleam.toml depends on glemy via `{ git = &quot;https://github.com/recregt/glemy&quot;, ref = &quot;&lt;pinned commit sha&gt;&quot; }`. glemy-games got its own CI (mirroring glemy&#39;s lavapipe/WebGPU setup), ARCHITECTURE.md, CLAUDE.md, and a fresh docs/decisions.jsonl (glemy&#39;s own log stays canonical for everything before the split). glemy-website&#39;s deploy.yml was reworked: it now checks out and builds glemy-games (edge and stable) instead of glemy directly -- building glemy-games alone transitively builds glemy via its git dependency, so the separate glemy checkout/build steps it used to need are gone; glemy is still checked out once, only for decisions.jsonl/development-plan.jsonl content. STABLE_GLEMY_REF was renamed to STABLE_GLEMY_GAMES_REF and promote-stable.yml now polls glemy-games&#39; own CI instead of glemy&#39;s. The moved files were then deleted from glemy itself, and ARCHITECTURE.md/CLAUDE.md/docs/technical-architecture.md were updated to describe glemy as engine-only, pointing to glemy-games&#39; own ARCHITECTURE.md for game-specific conventions.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0063.html</id><title type="html">#0063 — Dedupe a Shell-side pattern into game_ffi.mjs&#39;s setElementVisibilityMessage, mirroring decision 0062&#39;s Core-side reasoning one layer up</title><link href="https://recregt.github.io/glemy-website/devlog/0063.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Added game_ffi.mjs&#39;s setElementVisibilityMessage(elementId, visible, message) -- the shared implementation. Each game&#39;s own set_game_over (game_tiers.gleam) / set_status_message (game_breakout.gleam, game_platformer.gleam) is now a thin Gleam-side wrapper: a new private set_element_visibility_message extern pointed at the shared JS function, called with that game&#39;s own hardcoded element id string. Every existing call site (set_game_over(visible, message), set_status_message(visible, message)) is completely unchanged in arity, name, and meaning. Deleted the three now-redundant per-game JS implementations from game_tiers_ffi.mjs/game_breakout_ffi.mjs/game_platformer_ffi.mjs.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0062.html</id><title type="html">#0062 — Decompose wall-handling into caller-parameterized Core primitives (resolve_axis/resolve_high_only), parameterize physics.update&#39;s wall step, instead of leaving it unused by 2 of 3 games</title><link href="https://recregt.github.io/glemy-website/devlog/0062.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">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) -&gt; 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) -&gt; Entity parameter instead of hardcoding bounds.bounce; games/tiers.gleam&#39;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&#39;s own List(Entity) orchestration.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0061.html</id><title type="html">#0061 — Close out RM-034: circle-vs-Rect detection generalizes to Core, wall/landing restitution policy does not (three games, three distinct answers)</title><link href="https://recregt.github.io/glemy-website/devlog/0061.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">The verdict, with three real reference games&#39; worth of evidence behind it: (1) Circle-vs-rectangle collision detection (Rect/nearest_point/overlaps/penetration_axis) genuinely generalizes and is now Core (physics/rect.gleam, decision 0058) -- Breakout and Platformer need byte-for-byte identical detection math, differing only in resolution (reflect vs. stop), the same detection/resolution split physics/collision.gleam already established. (2) No single wall/landing restitution policy belongs in Core, confirmed a third distinct way, not just reconfirmed once more: Tiers damps to ~10% (physics/bounds.bounce, decision 0029), Breakout reflects near-elastically (bounce_off_walls, decision 0052), Platformer stops dead at zero restitution (stop_at_walls/resolve_platform_hit, decision 0059) -- three genuinely different answers to &#39;what happens when an entity meets a boundary,&#39; not three tunings of one shared idea. physics.update&#39;s own composition remains correctly not a drop-in generic physics step; every game with non-default wall behavior should keep composing entity.integrate/collision_sweep.resolve_all_collisions directly with its own bespoke resolution, as all three now do. A genuinely new finding this phase also produced, beyond RM-034&#39;s original two questions: a grounded-state-transition pattern (deriving a one-shot event from a state field&#39;s own before/after tick values, not from a single frame&#39;s transient physics derivative) is a real, reusable technique for detecting discrete events inside continuous simulation, caught while fixing a real bug in Platformer&#39;s own Landed event (decision 0059) before it shipped.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0060.html</id><title type="html">#0060 — Wire the platformer&#39;s rendering/input (glemy/game_platformer), verified via a new browser-check using captured-GPU-uniform position checks</title><link href="https://recregt.github.io/glemy-website/devlog/0060.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Built glemy/game_platformer.gleam (tick_and_render/start/loop/main, no score-equivalent needed since this game has none) and glemy/game_platformer_ffi.mjs (setStatusMessage/createLevelElements -- platforms and goal created once, no hide/destroy counterpart needed -- playJumpSound/playLandSound, its own AudioContext instance) plus platformer.html. Extended tools/browser_check_shared.ts&#39;s bootstrap to a third caller (already extracted for exactly this at Breakout&#39;s own Phase 2, decision 0053 -- now confirmed correctly scoped by a real third use, not over-engineered for two). Added tools/browser_check_platformer.ts, reusing the exact captured-GPU-uniform-buffer technique tools/browser_check.ts already established for Tiers&#39; click-to-spawn confirmation (adapted here to confirm real ArrowRight movement and a real space-triggered jump moved the player in world space), plus a deterministic real fall-death check (hold one direction key from spawn, no jump, reaching a real &quot;You fell!&quot; status within ~2s).</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0059.html</id><title type="html">#0059 — Build the platformer&#39;s pure game logic (glemy/games/platformer), confirming a third distinct wall/landing policy and a real Landed-event design bug found by direct testing</title><link href="https://recregt.github.io/glemy-website/devlog/0059.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Added glemy/games/platformer.gleam: GameStatus (Playing/Won/Lost, same sum-type reasoning as Breakout&#39;s), Model (player: Entity, bounds, platforms: List(Rect), goal: Rect, grounded: Bool, status), GameEvent (Jumped/Landed/Fell), Input (move_left/move_right/jump), tick, new, colored_player. tick composes entity.integrate directly (bypassing physics.update, same reasoning as Breakout&#39;s own tick) with three bespoke, zero-restitution resolution functions: stop_at_walls (side walls and a soft ceiling stop the player dead, the bottom edge is left untouched entirely -- same &#39;falling past bounds.min.y is the lose trigger, not a wall&#39; convention as Breakout), resolve_platforms/resolve_platform_hit (resolves at most one overlapping platform per frame, same simplification as brick.resolve_first_hit -- landing snaps position and zeroes vertical velocity and sets grounded, hitting the underside stops upward velocity only, hitting a side pushes back to the nearer edge and zeroes horizontal velocity). Jump is gated on the *incoming* model.grounded (read before this tick&#39;s own motion can overwrite it), not a same-tick freshly-computed value -- a deliberate MVP simplification (no same-tick land-then-jump chaining) noted as a future tuning candidate if it feels wrong in play. A small, fixed six-platform staircase level plus a goal rectangle, MVP-scoped identically to Breakout&#39;s own single-life, no-lives-counter precedent: no score, no hazards beyond falling off the bottom, no coyote-time/variable-jump-height/one-way-platforms.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0058.html</id><title type="html">#0058 — Promote circle-vs-Rect detection and the x-fraction/rect-to-CSS helpers from games/breakout into Core, at RM-034&#39;s second real caller</title><link href="https://recregt.github.io/glemy-website/devlog/0058.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Moved games/breakout/rect.gleam to physics/rect.gleam verbatim (Rect, nearest_point, overlaps, Axis/penetration_axis), doc comments updated to describe it as Core rather than game-specific, and its own test file relocated to test/glemy/physics/rect_test.gleam. games/breakout/brick.gleam, paddle.gleam, and breakout.gleam updated to import glemy/physics/rect instead. Added bounds.x_fraction(world_x, bounds) -&gt; Float to physics/bounds.gleam (moved verbatim from game_breakout.gleam, now documented as y_fraction_from_top&#39;s x-axis sibling) and rect.to_css(rect, bounds) -&gt; #(Float, Float, Float, Float) to physics/rect.gleam (moved verbatim from game_breakout.gleam&#39;s rect_to_css, renamed to fit its new home). game_breakout.gleam deleted both local functions and calls the promoted Core versions instead; its own x_fraction/rect_to_css tests relocated into test/glemy/physics/bounds_test.gleam and rect_test.gleam respectively. No public API changed shape anywhere in Breakout -- Brick.rect, paddle_rect, etc. keep their existing field types, now typed against physics/rect.Rect instead of games/breakout/rect.Rect.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0057.html</id><title type="html">#0057 — Fix the Breakout stable-channel deploy crash and replace decision 0046&#39;s manual-only stable promotion with CI-gated automatic promotion</title><link href="https://recregt.github.io/glemy-website/devlog/0057.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Two fixes, landed together as one real incident response: (1) .github/workflows/deploy.yml&#39;s per-game demo copy is now a guarded copy_demo() shell function -- checks the source HTML file actually exists in that checkout before creating the destination directory at all, so a game missing from a lagging stable checkout skips that one demo (falling back to build.gleam&#39;s own already-documented prepare_play_demo_dir behavior) instead of crashing the entire deploy. (2) Added glemy/.github/workflows/ci.yml (gleam build/gleam test on both targets, against a real headless WebGPU backend via lavapipe -- Mesa&#39;s software Vulkan implementation, the standard way to give Deno&#39;s native WebGPU, decision 0015, a real adapter to find with no GPU hardware present) as the first real CI glemy has ever had, giving &#39;this commit&#39;s build/test genuinely passed&#39; a checkable GitHub status rather than only a local, manually-run gate (CLAUDE.md&#39;s own standing rule). Added glemy-website/.github/workflows/promote-stable.yml, superseding decision 0046&#39;s manual-only promotion: runs hourly (plus on-demand via workflow_dispatch), reads glemy&#39;s public Actions API for the most recent main commit with a passing CI run, and -- only if that differs from the current STABLE_GLEMY_REF -- commits the new SHA and explicitly dispatches deploy.yml (not a plain push, per the third rejected option above) to actually publish it. decisions.md/technical-architecture.md&#39;s §3.4 requirement (&quot;promotion is a deliberate, explicit action, not an automatic consequence of a glemy push&quot;) is updated to reflect this: promotion is now automatic, but CI-gated, preserving the actual property decision 0046 valued (a real, auditable git commit records every promotion; an untested or failing build is never promoted) while removing the human-remembers-to-do-it failure mode that caused this whole incident.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0056.html</id><title type="html">#0056 — Extend tools/decisions with a roadmap subcommand to validate and manage docs/development-plan.jsonl (RM-028)</title><link href="https://recregt.github.io/glemy-website/devlog/0056.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Added decisions/roadmap_id.gleam (an RM-NNN opaque id type, 3-digit, mirroring decisions/id.gleam&#39;s own shape and guarantees), decisions/roadmap_schema.gleam (Status/RoadmapItem/Draft/Patch types plus decoders/encoders matching docs/development-plan.md&#39;s schema exactly, including a status vocabulary decoder for planned/in_progress/completed/deferred/superseded), and decisions/roadmap_log.gleam (read/next_id/append mirroring decisions/log.gleam&#39;s own contracts, plus a new update(path, id, patch) -&gt; Result(Nil, LogError) capability decisions.jsonl&#39;s own append-only log structurally never needs, and validate_decision_refs for the decisions.jsonl cross-check). Renamed the CLI&#39;s bare-positional decision-append invocation to an explicit `decision` subcommand, and added `roadmap new`/`roadmap update` subcommands with matching draft/patch-file conventions. Every decision_refs entry supplied to either roadmap subcommand is validated against a real read of decisions.jsonl before anything is written. Dogfooded immediately: used `roadmap update` (not another hand-edit) to update RM-028&#39;s own status.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0055.html</id><title type="html">#0055 — Add Breakout to glemy-website&#39;s game catalog, generalizing game_card into a parameterized component at its second real caller</title><link href="https://recregt.github.io/glemy-website/devlog/0055.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Generalized glemy_website/game_card.gleam&#39;s tiers_card into a parameterized game_card(base_url, demo, icon, name, tagline, description) function; tiers_card and a new breakout_card are now both thin callers of it. Added breakout_icon() built from Breakout&#39;s own real on-screen colors (paddle #5af, ball white, bricks #fa5 -- matching breakout.html&#39;s CSS and games/breakout.ball_color, the same &#39;real colors, not a generic icon&#39; principle tiers_icon() already established). game_card.catalog now takes one DemoPaths per game (tiers_demo, breakout_demo) and returns both cards; glemy_website/pages/play.gleam&#39;s view threads both through. build.gleam gained two more prepare_play_demo_dir calls (play-demo-breakout-stable/-edge) alongside the existing Tiers pair, producing a second DemoPaths passed into play.view. .github/workflows/deploy.yml gained a second checkout-copy block (breakout.html renamed to index.html on copy, plus its own full build/dev/javascript copy, mirroring the existing Tiers block exactly) for both stable and edge channels. The homepage (home.gleam) was deliberately left calling tiers_card directly, unchanged -- it&#39;s a single-game teaser by design (its own prose still reads &#39;glemy&#39;s first proof is Tiers, below&#39;), not the full catalog; /play is where the generalized catalog now shows both games.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0054.html</id><title type="html">#0054 — Phase 3 (playtesting/tuning): empirical multi-exchange verification confirms Breakout&#39;s constants are sane; subjective feel-tuning deferred to the user&#39;s own hands-on play</title><link href="https://recregt.github.io/glemy-website/devlog/0054.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Ran a one-off Playwright probe (not committed -- ad-hoc, scratchpad-only, same convention as decision 0025&#39;s probe, distinct from the repo&#39;s permanent tools/browser_check_breakout.ts) against the real dev server: held a simple reactive paddle sweep (alternating ArrowLeft/ArrowRight every 2s) for up to 20 real seconds, sampling #glemy-score/.glemy-brick[hidden]/#glemy-status every 500ms. Observed two real, distinct brick hits (score 0 -&gt; 10 -&gt; 20, at t~=653ms and t~=2722ms) followed by a clean transition to &quot;Game over!&quot; at t~=4789ms once the paddle sweep failed to intercept the ball -- confirming the wall-bounce/paddle-bounce/brick-hit loop genuinely sustains multiple real exchanges, not just the single hit already covered by browser_check_breakout.ts, and that the Lost transition (missing the paddle) fires correctly and doesn&#39;t leave the game in a stuck/frozen state. Cross-checked the current constants analytically too: initial_ball_speed&#39;s vy component (60) over the ~41-46 world-unit gap from spawn (bounds.min.y + paddle_margin_from_bottom + paddle_height + ball_radius + 1 = 16) to the nearest brick row (bottom row at world-y [57, 62]) predicts a ~0.68-0.77s first hit -- the real probe&#39;s observed 653ms falls almost exactly inside that window, a genuine confirmation the physics composition (decision 0052) behaves as designed under real browser timing, not just in gleam test&#39;s synthetic dt. paddle.speed (80 world-units/s) against the paddle&#39;s own full clamped travel range (80 world-units, half_width 10 in a 100-wide field) predicts a ~1.0s full-width sweep -- comfortably faster than a human&#39;s likely reaction time, but the probe&#39;s own naive 2s-per-direction AI oscillator was slower than that sweep time, plausibly explaining why it eventually missed; not itself evidence any constant needs changing, since a real human player reacts to the ball&#39;s actual position rather than sweeping blindly. No constant was changed as a result -- everything checked out internally consistent and functioning correctly across a real multi-exchange session.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0053.html</id><title type="html">#0053 — Wire Breakout&#39;s rendering/input (glemy/game_breakout), redesign Brick with a stable id for DOM reconciliation, and extract shared browser-check bootstrap at its second real caller</title><link href="https://recregt.github.io/glemy-website/devlog/0053.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Added `id: Int` to games/breakout/brick.Brick, assigned once at grid-creation time (row * columns + column) and never reused; changed brick.resolve_first_hit&#39;s return type from an untagged tuple to a named `Hit(ball, remaining, score, destroyed_id)` record; changed `GameEvent.BrickDestroyed` to carry that `id`. Built glemy/game_breakout.gleam (format_score/x_fraction/rect_to_css as pure, target-agnostic helpers; tick_and_render/start/loop/main as the @target(javascript) runner, mirroring glemy/game_tiers.gleam&#39;s own split) and glemy/game_breakout_ffi.mjs (setScoreText/setStatusMessage/setPaddleBox/createBrickElements/hideBrickElement/playBounceSound/playBrickSound, plus its own AudioContext instance -- deliberately not shared with game_tiers_ffi.mjs, since the two games&#39; compiled output never loads on the same page). Added breakout.html as Breakout&#39;s own HTML entry point (index.html stays Tiers&#39;). Extracted tools/browser_check_shared.ts (findChromium, fail, waitForServer, spawnDevServer, stopDevServer, launchGpuBrowser) out of tools/browser_check.ts, and added tools/browser_check_breakout.ts on top of it, verifying: the RAF loop runs; a real held ArrowLeft keypress moves the real #glemy-paddle overlay (glemy/io.is_key_down&#39;s first real caller); a real ball-brick collision hides the corresponding brick div and updates the score; at least one sound plays; zero page errors.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0052.html</id><title type="html">#0052 — Build Breakout&#39;s pure game logic (glemy/games/breakout, rect/brick/paddle) with rectangles kept entirely outside physics, and confirm physics.update does not generalize to a second genre</title><link href="https://recregt.github.io/glemy-website/devlog/0052.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Built glemy/games/breakout/rect.gleam (a plain Rect type plus the standard clamp-to-nearest-point circle-vs-AABB overlap test and penetration-axis detection), glemy/games/breakout/brick.gleam (brick grid layout, per-brick score, reflect, and resolve_first_hit -- resolving at most one brick collision per frame, matching physics/collision_sweep.resolve_target_against_rest&#39;s own &#39;resolve one interaction and move on&#39; philosophy), glemy/games/breakout/paddle.gleam (held-key movement reusing physics/bounds.clamp_x directly via the half-width-as-radius equivalence, and a trig-free hit-position-dependent bounce reflection that preserves total ball speed), and glemy/games/breakout.gleam (the hub: Model/GameStatus/GameEvent/Input/tick/new). tick composes physics/entity.integrate and physics/collision_sweep.resolve_all_collisions directly, with a bespoke bounce_off_walls function (near-full-elastic on left/right/top, nothing at the bottom -- what makes falling past bounds.min.y a lose trigger rather than a wall) instead of calling physics.update. GameStatus is a three-way sum type (Playing/Won/Lost), not two independent Bools, ruling out the nonsensical &#39;both true&#39; state Gleam&#39;s type system can reject for free.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0051.html</id><title type="html">#0051 — Split glemy/game.gleam into glemy/game_tiers.gleam plus a shared game_ffi.mjs, in preparation for a second game runner</title><link href="https://recregt.github.io/glemy-website/devlog/0051.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Renamed glemy/game.gleam to glemy/game_tiers.gleam (mechanical, git mv, mirrors the pe-&gt;physics rename precedent) and split glemy/game_ffi.mjs: requestFrame, its error-reporting wrapper, and canvasBoundingRectLeftAndWidth stay in game_ffi.mjs as genuinely generic runner infrastructure every game&#39;s runner needs; setScoreText/setGameOver/setDangerLinePosition/setNextTierColor/playDropSound/playMergeSound/triggerMergeFlash (all Tiers-specific DOM/audio writes) moved to a new glemy/game_tiers_ffi.mjs. Test files/helpers renamed to match (game_test.gleam -&gt; game_tiers_test.gleam, game_test_ffi.mjs -&gt; game_tiers_test_ffi.mjs). index.html&#39;s module import updated to the renamed compiled output. Established the standing convention (documented in ARCHITECTURE.md): each game gets its own game_&lt;name&gt;.gleam + game_&lt;name&gt;_ffi.mjs, mirroring games/&lt;name&gt;/&#39;s existing one-tree-per-game convention one layer up at the Shell boundary, with game_ffi.mjs remaining the one shared file for genuinely generic runner concerns. While touching these files, also fixed several pre-existing stale doc-comment references found along the way: a duplicated &#39;(now glemy/physics)&#39; artifact left over from the pe-&gt;physics rename&#39;s own bulk sed (in io.gleam), a stale glemy/pe/bounds reference in tools/browser_check.ts, and a stale clamp_preview_x citation in ARCHITECTURE.md (that function was replaced by physics/bounds.clamp_x in decision 0050).</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0050.html</id><title type="html">#0050 — Add physics/bounds.clamp_x and physics.settle_all, reject a bounds.y_from_top helper, and fix a real float.clamp/bounce_axis divergence on degenerate bounds</title><link href="https://recregt.github.io/glemy-website/devlog/0050.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Added physics/bounds.clamp_x(x, radius, bounds) -&gt; Float (keeps a circle&#39;s center inside bounds horizontally, position only, no velocity -- for callers like a drop preview that only have a position to keep in bounds, not a simulated entity that should go through bounce instead) plus a private clamp_axis helper matching bounce_axis&#39;s exact edge-resolution order. Added physics.settle_all(model, dt) -&gt; Model, a bulk-apply convenience for the exact map-then-reconstruct shape physics.update already uses internally, mirroring that pattern one level up; deliberately kept separate from update itself (update stays the pure, undamped integrate/bounce/collision composition, per its own existing doc comment -- not reopened here). Rewired games/tiers.gleam: deleted the private clamp_preview_x entirely in favor of calling bounds.clamp_x directly at its one call site (matching how tick already calls cooldown.tick/stopwatch.tick/physics.entity_count with no local wrapper, decision 0049&#39;s own style), and replaced the 3-line manual settle map-then-reconstruct with one call to physics.settle_all. No Model field or public signature changed shape.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0049.html</id><title type="html">#0049 — Extend the Core API with three small utility modules (cooldown, stopwatch, random) and physics.entity_count, and reject a generic PreviewQueue abstraction</title><link href="https://recregt.github.io/glemy-website/devlog/0049.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">Added three new top-level Core modules, siblings to glemy/physics/glemy/render/glemy/io rather than nested under physics/ (every engine surveyed -- Bevy&#39;s bevy_time vs. its physics crates, Godot&#39;s Timer node vs. PhysicsServer2D, LOVE2D&#39;s love.timer/love.math vs. love.physics -- keeps timer/random utilities separate from physics, direct precedent for not re-diluting the physics name decision 0048 just fixed): glemy/cooldown.gleam (tick(remaining, dt) -&gt; Float, floored at 0.0; is_ready(remaining) -&gt; Bool), glemy/stopwatch.gleam (tick(elapsed, dt, holding) -&gt; Float, accumulates while holding else resets to 0.0, no threshold concept), and glemy/random.gleam (pick(random, options) -&gt; Result(a, Nil), a verified byte-for-byte generalization of random_droppable&#39;s existing index-selection algorithm, taking the random float as a plain argument rather than generating it internally -- validated against Godot&#39;s own confirmed, documented seeded-RNG-non-reproducibility problem across engine versions, godot/godot#27856, as a concrete reason never to let a Core utility own random state). Added physics.entity_count(model) -&gt; Int. Rewired games/tiers/rules.random_droppable into a thin wrapper (random.pick |&gt; result.unwrap(0)) and games/tiers.tick&#39;s three corresponding expressions (cooldown_remaining, at_capacity, danger_timer) to call the new primitives -- Model&#39;s field types unchanged (still plain Float/Int), so no test file outside the new modules&#39; own required any edit.</summary></entry><entry><id>https://recregt.github.io/glemy-website/devlog/0048.html</id><title type="html">#0048 — 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</title><link href="https://recregt.github.io/glemy-website/devlog/0048.html"/><updated>2026-08-09T00:00:00Z</updated><published>2026-08-09T00:00:00Z</published><summary type="html">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&#39;s separate entities/colors parameters with a single List(ColoredEntity) (ColoredEntity = #(Entity, #(Float, Float, Float))), updated render_ffi.mjs&#39;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) -&gt; Canvas (@target(javascript), wrapping a getCanvasElement FFI function moved from game_ffi.mjs into render_ffi.mjs, where Canvas&#39;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&#39;s stale header doc comment to describe input as read by a runner (glemy/game&#39;s loop) and folded into a game&#39;s own Input, not read by glemy/physics directly.</summary></entry></feed>