Solutions architecture · Commercial title · Demo live · Launches Nov 10, 2026
FIRST LIGHT
A commercial, story-driven brick-breaker for Steam: 132 hand-built boards, each machine-checked winnable, five narrators over a 63-track score (33 of them on the soundtrack album), shipped through a gated release pipeline that has put 130 builds live.
Status
Demo live on Steam since 18 Aug 2026 · Steam Next Fest, 19–26 Oct · full game and the 33-track soundtrack album on 10 Nov 2026
Version
v0.174.3, build 235, live since 21 Sep 2026 (pre-1.0)
Licence
Proprietary · Ephphatha Games. Third-party notices ship with the game (SIL OFL fonts; MIT-licensed Electron, PixiJS and a modified steamworks.js)
Runs on
Windows 10 64-bit · macOS 12 on Apple Silicon · Steam Deck targeted, with its own input layout (not yet rated by Valve)
Play all 132 boards to the end after every change.
→
Now
A reachability prover inside the test suite: 132 of 132 pass, 39 of them with indestructible gold in play.
At a glance
boards proven winnable by the reachability prover
132/132
tests in 24 files, all passing; strict typecheck clean
327
builds set live on Steam (84 → 235)
130
lines of tooling vs 32,897 of game runtime (25,046 vs 16,801 code‑only)
41,358
How these were measured
132/132 — the prover in tests/solve.ts run over every board in an isolated scratch copy of the repository, 12 Oct 2026: 132 pass, 0 bricks sealed; 39 boards contain indestructible gold.
327 — vitest run in the same scratch copy: 24 test files, 327 tests passed. tsc --noEmit exited clean for both the game and the workshop configs.
130 — counted from the liveness record the release scripts write: 130 builds, numbered 84 to 235. Every build from 114 on was recorded by script; the 23 before it were entered from the earlier hand-kept release list.
41,358 — git ls-files over the scripts and the workshop, counted with wc -l; game runtime is src plus the Electron shell. Code-only drops blank and comment-only lines.
Fig. 1Memory 44, “Fall Dance”: a maple-leaf board, whole at the start of the round, with the memory and song titles in the footer. Press-kit capture, cropped to the play field on small screens.
The short version
An unfinishable board is a failing test. A reachability prover peels every board from its open edges, and any breakable brick left standing fails the suite.
The game upload reads the defect tracker first, scoped to what it ships. An unknown state counts as blocked.
The desktop shell moved from Tauri to Electron in one day, on a written spike that listed what it did not prove; a notarized build shipped that night.
Case 1 of 4
A level you can’t finish is a bug you ship to everyone
A prover in the test suite checks that every breakable brick on every board can be reached. All 132 boards pass.
In a brick-breaker, indestructible gold can wall a breakable brick off where no ball will ever reach it. The player gets no crash and no error, just a level that never ends. FIRST LIGHT’s boards are painted in a spreadsheet and edited all the time, so playing all 132 to the end after every change was never going to happen.
So I built a prover. It pads the 13-column board with an open border, flood-fills every empty cell a ball could reach, removes each breakable brick touching that space, and repeats until nothing changes: a peel to a fixpoint. Any breakable brick still standing is sealed, and the test fails. Because the model is ball-only, it also shows that no board needs a power-up to finish, and a second test asserts exactly that.
It is a reachability proof on the grid, not a physics simulation, aimed at the one failure that makes a board unfinishable, and the board file’s own header names it as the contract every board must meet. The build runs it with the rest of the suite every time, though a red suite doesn’t yet stop the build (Case 2).
Fig. 2Two invented 5-column boards (real ones are 13 wide). Gold squares never break, and each number is the pass on which the peel, starting from the dashed open border, reaches that brick. Left, one gap lets the peel in. Right, a closed ring seals two bricks, and the test fails.How we know
The prover is isWinnable() in tests/solve.ts: a four-neighbour flood fill and peel, capped at 500 passes. Run over all 132 boards in an isolated scratch copy on 12 Oct 2026, it found 132 winnable and 0 sealed bricks; 39 boards contain gold. The two tests that call it, one for winnability and one for needing no power-up, are in tests/levels.test.ts, and both passed in the 327-test run that day. The figure’s two boards are illustrations, checked with the same peel rule.
Case 2 of 4
Release gates that stop, and know what they’re gating
Each gate stops the run or demands a person’s explicit override (the test step, for now, only reports), and the upload gate checks only the shipment it is guarding.
Debug keys exist only under the dev server: they are derived from the build command, so every distributable is clean by construction. Two checks back that up anyway. A fail-closed one confirms the config still derives the keys that way; the other searches the built bundles’ bytes for a sentinel string before anything is notarized or uploaded.
The Steam upload calls a ship gate that reads the defect tracker before anything is staged. It is scoped per shipment: the game-and-demo upload asks only about game, demo, build and Steam blockers, so a soundtrack bug can’t hold up the game. Open, in-progress and fixed-but-unverified blockers all stop the upload, and an unknown state (an unreadable tracker, an unknown scope) counts as blocked. Its categories come from the tracker’s own legend, so a new one can’t quietly fail open. Shipping over a blocker takes a person typing an explicit override at a terminal; an unattended run aborts.
Static checks can’t see a runtime death. One build passed typecheck, tests, receipts and notarization and still could not start, so the build now launches the packaged macOS app and fails if the front end never reports its version.
Liveness is a handshake too: a build is recorded as live only if its log shows it was sent to Steam, and a doc checker fails when any document disagrees with that record.
Stops the run when it fails
Reports only
Runs on demand
Fig. 3The release path, top to bottom. Licence, notice and content checks also run in the build script and are left out here. Only the first launch of the Windows build is still checked by hand.How we know
Read on 12 Oct 2026: scripts/build_electron.sh (both typecheck gates, both debug-key guards, the receipt step and the boot step, each of which aborts the script, with notarization after the boot step) and electron-builder.config.cjs (the signing identity, applied while the Mac apps are packed); scripts/ship_gate.mjs and scripts/lighttosteam.sh (the scoped gate; its only caller, which always asks about game and demo; the typed override; the abort when no terminal is attached; the separate run that records a build as live), and scripts/steam_live.mjs with scripts/check_docs.mjs (the liveness record and the doc check). The test step pipes vitest through tail under sh with set -e and no pipefail, so tail’s exit code wins; reproduced with a /bin/sh pipeline the same day. The Playwright probe and the doc checker are not called by any script.
Case 3 of 4
Swapping the desktop shell mid-project, on evidence
Tauri to Electron in one day: a written spike, a notarized build that night, and a rollback kept until the new shell had earned it.
The game first shipped on Tauri, Rust plus the system webview, and the webview kept costing time: a cursor workaround because pointer lock didn’t work, a JavaScript-to-Rust handshake just to size the macOS viewport, a WebView2 dependency on Windows, two system renderers that could change under a shipped build, and no route to the Steam Deck, because the Windows webview doesn’t run under Proton.
I ran a branch-only spike. It showed the game running unmodified in Electron, and it listed in writing what it did not prove: achievements, saves, notarization and the Steam overlay. When the old shell later left the ship path, one fail-closed check stayed on purpose: the stager refuses to upload any bundle it can’t positively identify.
Build 84, the last Tauri build, goes live on Steam.
Branch-only Electron spike, with a written list of what it did not prove.
Notarized Developer ID build.
Build 91, the first Electron build, goes live.
The Tauri rollback is retired.
The old shell leaves the ship path: 193 lines out, 92 in, across three scripts.
How we know
Commit timestamps from git log, read 12 Oct 2026: four commits on 12 Aug (build 84 live, the spike, the notarized build, build 91 live), the rollback’s retirement on 13 Aug, and git show --stat for the 1 Sep cleanup (3 files, 92 insertions, 193 deletions). The process split is in electron/main.cjs; the fork, its restore step and its check are in scripts/vendor_steamworks.mjs and scripts/verify_electron_build.mjs. On 12 Oct, a byte search found the overlay callback’s symbol in both vendored binaries and in neither of two upstream builds, and GitHub’s public API listed the upstream pull request as open and unmerged, with nine lines added. The framework share is du of the build-235 Mac app the same day: Electron’s frameworks take 281,196 of its 636,312 KiB (44%). The storage requirement and platforms are from Steam’s public store data, read the same day.
Case 4 of 4
The tooling is bigger than the game
By line count the tools outweigh the game, with or without comments, and most workshop modules import the game’s live source, so they can’t fall out of step.
The game core imports no renderer and its random numbers are seeded, so bots can play it. The balance sweep plays each of the 132 rounds 100 times independently, with four selectable player models: teleport, follow, a “human” with reaction time and flick error, and aim. It logs 36 fields per run, so a new question becomes a query instead of a fresh simulation. A campaign bot plays rounds 1 through 132 as one player, with lives, score and power-ups carried forward, across ten seeds by default.
A workshop of 37 active tool pages (45 linked, 8 retired) runs on a second dev server. 32 of its 43 modules import the game’s live source rather than snapshots, so those tools can’t fall out of step with the game, and the workshop is typechecked in the build gate. Level art round-trips through Excel, with a lossless round-trip check added after a palette collision silently changed a board.
The voice pipeline is tooled the same way. Its model clips the first or last word of about 40% of renders, so every line is rendered with padding at both ends, the pads are found by paired silences, and the masters are kept: a bad cut means re-trimming, never re-rendering an approved take.
Tracked first-party lines in FIRST LIGHT, 12 Oct 2026.
Tooling, all lines
41,358
Game runtime, all lines
32,897
Tooling, code only
25,046
Game runtime, code only
16,801
How we know
Line counts from git ls-files and wc -l on 12 Oct 2026. Tooling is scripts/ (22,208 lines in 152 files) plus the workshop (19,150 in 94); runtime is src/ plus the Electron shell (40 files), including a 3,529-line board data file and a 2,016-line narration script. Code-only drops blank and comment-only lines. The sweep’s defaults and its 36-field run record are in scripts/balance_sweep.ts, the player models in scripts/keeper_bot.ts, and the campaign bot’s ten seeds in scripts/campaign_bot.ts. The workshop index links 45 pages, 8 of them marked retired; 32 of its 43 TypeScript modules import from src/.
From the game
Press-kit captures of the current, pre-release build
Fig. 4The title screen: an arcade cabinet in a garage at sunrise.
Fig. 5Memory 44, “Fall Dance”: a maple-leaf board at round start.Fig. 6Memory 67, “The Starlight”: a board shaped like an arcade alien.Fig. 7Memory 33, the first warden: bricks break away to reveal a framed painting. The four warden paintings were made with an AI image model, as the press kit discloses.