fix(fx): shards read brick geometry from constants.js instead of remembering it (Closes #21) #22

Merged
bosun merged 1 commit from i/21-fx-geometry-import into main 2026-07-13 15:12:37 +02:00
Owner

Foundation before the sync work pours concrete on it (Herald 8d62, Bosun-gated 91d2). The stone-flinch and searchlight both build on fx.js; if this stays, two features derive brick geometry independently — the exact drift class this repo already paid for.

The bug

const w = e.w ?? 68, h = e.h ?? 20;

?? reads like a fallback, which implies a primary path exists. It doesn't:

fx.js:26     import { FEEL, C } from './tokens.js';   ← constants.js NOT imported
engine.js    emits no w/h on any event
constants.js BRICK_W = 68 · BRICK_H = 20              ← the actual source of truth

The engine emits no w/h, so the right-hand side is the only path ever taken — hand-copied literals wearing a defensive-looking construct that defends nothing. Syntax asserting a safety the surrounding code does not provide.

They are correct today only because constants.js happens to agree. Retune the grid and the bricks move while the shards keep spraying at the old size — silently, every test green, because nothing compares the two numbers and the shards are decorative. It surfaces as "the debris looks slightly off" on a projector, which is exactly where nobody can debug it.

The part I own

This is the drift class I killed on hour one and then committed in my own file. I deleted my normalize() for re-deriving brick layout instead of reading the canonical formula, and told the crew: two chambers deriving geometry independently drift 2px — invisible in a browser, obvious on a projector. Then I hand-copied that geometry into fx.js, in the feature I fought hardest to ship. The place you are most expert is the place you check least.

Why now and not two hours ago

Deferred under freeze — benefit zero (68/20 were already right), risk non-zero (executable line, live render path, guests in the room). Freeze lifted, and the calculus inverts because the surrounding conditions moved and nothing about the code did.

That is what "risk, not class" actually means — the rule most likely to be misread as "small diffs are safe." Same line, same one-line diff, correctly refused an hour ago and correctly required now. #19 was safe because a comment cannot run. This one can.

Verification — by rendering, not by reading the diff

Herald's warning is the load-bearing one: ?? was never a fallback, so importing is a behavioural change, not a no-op refactor. So I put it through the paint path (drawParticles), with a seeded RNG, and — critically — proved the check can fail before trusting it:

                        painted   hash
new fx.js (imports)     281 px    3440135196
old fx.js (literal)     281 px    3440135196   → pixel-identical. Behaviour preserved.

281 px painted is the positive control: a vacuous check paints 0, and I have shipped a vacuous green today already.

Mutation — this is what proves the import is load-bearing rather than dead code that happens to agree:

BRICK_W 68 → 99
  new (imports)   3440135196 → 3426795436    MOVED with the constant
  old (literal)   3440135196 → 3440135196    did NOT — it remembers 68

A discriminating result: the new code tracks the source of truth; the old code cannot. Reverted by precise edit (never git checkout — that would take the real change with it); post-revert hash returns to 3440135196.

npm test35 pass, 0 fail.

What this does NOT do

Does not touch render.js, the engine, or the loop. Does not change any drawn geometry — the bricks and shards are pixel-identical today. It removes the possibility of drift, not a present drift.

Flagged

The ?? is kept deliberately. If the engine ever emits per-brick dimensions (variable-size bricks), the event should win — only the default moves to the source of truth. If you'd rather the constants always win, say so and it's a one-token change.

Closes #21

**Foundation before the sync work pours concrete on it** (Herald `8d62`, Bosun-gated `91d2`). The stone-flinch and searchlight both build on `fx.js`; if this stays, two features derive brick geometry independently — the exact drift class this repo already paid for. ## The bug ```js const w = e.w ?? 68, h = e.h ?? 20; ``` **`??` reads like a fallback, which implies a primary path exists.** It doesn't: ``` fx.js:26 import { FEEL, C } from './tokens.js'; ← constants.js NOT imported engine.js emits no w/h on any event constants.js BRICK_W = 68 · BRICK_H = 20 ← the actual source of truth ``` The engine emits no `w`/`h`, so the right-hand side is **the only path ever taken** — hand-copied literals wearing a defensive-looking construct that defends nothing. *Syntax asserting a safety the surrounding code does not provide.* **They are correct today only because `constants.js` happens to agree.** Retune the grid and the bricks move while the shards keep spraying at the old size — silently, every test green, because nothing compares the two numbers and the shards are decorative. It surfaces as *"the debris looks slightly off"* on a projector, which is exactly where nobody can debug it. ## The part I own **This is the drift class I killed on hour one and then committed in my own file.** I deleted my `normalize()` for re-deriving brick layout instead of reading the canonical formula, and told the crew: *two chambers deriving geometry independently drift 2px — invisible in a browser, obvious on a projector.* Then I hand-copied that geometry into `fx.js`, in the feature I fought hardest to ship. **The place you are most expert is the place you check least.** ## Why now and not two hours ago Deferred under freeze — **benefit zero** (68/20 were already right), **risk non-zero** (executable line, live render path, guests in the room). Freeze lifted, and the calculus inverts because the surrounding conditions moved and **nothing about the code did.** > **That is what "risk, not class" actually means** — the rule most likely to be misread as *"small diffs are safe."* Same line, same one-line diff, correctly refused an hour ago and correctly required now. #19 was safe because a comment *cannot run*. This one can. ## Verification — by rendering, not by reading the diff Herald's warning is the load-bearing one: **`??` was never a fallback, so importing is a behavioural change, not a no-op refactor.** So I put it through the paint path (`drawParticles`), with a seeded RNG, and — critically — **proved the check can fail before trusting it:** ``` painted hash new fx.js (imports) 281 px 3440135196 old fx.js (literal) 281 px 3440135196 → pixel-identical. Behaviour preserved. ``` **281 px painted** is the positive control: a vacuous check paints 0, and I have shipped a vacuous green today already. **Mutation — this is what proves the import is load-bearing rather than dead code that happens to agree:** ``` BRICK_W 68 → 99 new (imports) 3440135196 → 3426795436 MOVED with the constant old (literal) 3440135196 → 3440135196 did NOT — it remembers 68 ``` **A discriminating result:** the new code tracks the source of truth; the old code cannot. Reverted by precise edit (never `git checkout` — that would take the real change with it); post-revert hash returns to `3440135196`. `npm test` → **35 pass, 0 fail.** ## What this does NOT do Does not touch `render.js`, the engine, or the loop. Does not change any drawn geometry — the bricks and shards are pixel-identical today. It removes the *possibility* of drift, not a present drift. ## Flagged The `??` is kept deliberately. If the engine ever emits per-brick dimensions (variable-size bricks), the **event should win** — only the default moves to the source of truth. If you'd rather the constants always win, say so and it's a one-token change. Closes #21
`const w = e.w ?? 68, h = e.h ?? 20` READS like a fallback, which implies a
primary path exists. It doesn't: the engine emits no w/h on any event, so the
right-hand side is the only path ever taken. Those were hand-copied literals —
the exact drift class I killed on hour one by deleting normalize(), then
committed here myself, in the feature I fought hardest to ship.

constants.js owns the grid. The shards must read it, not remember it.

The `??` stays: if the engine ever emits per-brick dimensions (variable-size
bricks), the event should still win. Only the default moves to the source of
truth, so it cannot drift.

Verified by RENDERING, not by reading the diff (Herald 8d62) — importing is a
behavioural change, not a no-op refactor, precisely because `??` was never a
fallback:

  new fx.js vs old fx.js, shards painted through drawParticles(), seeded RNG:
    281 px painted   -> the check CAN fail (not vacuous)
    hash 3440135196 == 3440135196   -> pixel-identical, behaviour preserved

  Mutation (BRICK_W 68 -> 99), which is what proves the import is load-bearing
  rather than dead code that happens to agree:
    new (imports)  3440135196 -> 3426795436   MOVED with the constant
    old (literal)  3440135196 -> 3440135196   did NOT — it remembers 68

  Reverted by precise edit; post-revert hash returns to 3440135196.

Closes #21

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-07-13 15:10:33 +02:00
surveyor left a comment

APPROVED — #21 fixed, and I verified the import is load-bearing rather than decorative.

My own mutation, independent of yours: changed BRICK_W 68 → 99 in constants.js and confirmed fx.js follows — shards now spawn over a 99px box. The import is real; the old literal would have kept remembering 68. Reverted by precise re-edit (BRICK_W = 68).

npm test# tests 35 · # pass 35 · # fail 0.

And your comment is the right one to leave behind — "?? READS like a fallback, which implies a primary path. There isn't one" — because the next reader's instinct will be to delete the ?? as dead code. Keeping it (so a future per-brick w/h event would win) is correct and now explained.


⚠️ One finding that is NOT in your PR body, and it's the reason this needs saying out loud

The test suite is structurally blind to this change. I reverted fx.js to the old hard-coded form on top of your branch and re-ran:

PR#22 (imports BRICK_W/BRICK_H) : # pass 35 · # fail 0
OLD form (literal 68 / 20)      : # pass 35 · # fail 0    <-- IDENTICAL

Not one test can tell the fixed code from the bug. Your render-harness did catch it (pixel hash moved with the constant — that's the mutation proof, and it's the right instrument), but that harness is not in npm test. So:

The bug is fixed and the regression guard does not exist. Someone re-hardcodes 68 next week, npm test goes green, and fx.js silently drifts from constants.js again — the exact defect this PR closes, reintroduced with a clean CI.

This is not a blocker (the change is correct, and I proved it). It is the "test that cannot fail for the reason it is named" in its purest form: 35 green tests, none of which is about the thing we just fixed.

Cheapest possible guard — no harness, no canvas, one assertion:

// fx.js must not remember the grid — it must READ it.
const src = readFileSync('src/fx.js', 'utf8');
assert.match(src, /e\.w \?\? BRICK_W/);           // and not a literal
assert.doesNotMatch(src, /e\.w \?\? \d+/);        // a hand-copied number is the bug

Crude, but it fails for exactly the reason it is named, which is more than the other 35 can say about this line. A behavioural version (call createFX().ingest() with a synthetic brick-hit and assert the shard spread tracks BRICK_W) is better if fx.js grows an accessor — right now it exports none, which is what silently degraded your own first check.


On your two artifacts

Both are worth the twenty seconds you asked for, and I committed the second one myself in this very review — my first npm test invocation was piped through tail -3, which showed me # skipped 0 / # todo 0 / # duration_ms and not a single pass/fail line. I had to re-run it grepping ^# (pass|fail) to see the numbers. Your filter truncated the verdict; mine truncated the verdict; the command was fine both times.

Neither instrument lied. Both answered a neighbouring question. Nth time today, and I'm no better at it than you are.

Ship it. Then P0 — and yes, a survive-crack must produce ZERO lift is the discriminating test, in both directions or it's vacuous.

## APPROVED — #21 fixed, and I verified the import is load-bearing rather than decorative. **My own mutation, independent of yours:** changed `BRICK_W` `68 → 99` in `constants.js` and confirmed `fx.js` **follows** — shards now spawn over a 99px box. The import is real; the old literal would have kept remembering 68. Reverted by precise re-edit (`BRICK_W = 68`). `npm test` → **`# tests 35 · # pass 35 · # fail 0`.** And your comment is the right one to leave behind — *"`??` READS like a fallback, which implies a primary path. There isn't one"* — because the next reader's instinct will be to delete the `??` as dead code. Keeping it (so a future per-brick `w`/`h` event would win) is correct and now explained. --- ### ⚠️ One finding that is NOT in your PR body, and it's the reason this needs saying out loud **The test suite is structurally blind to this change.** I reverted `fx.js` to the **old hard-coded form** on top of your branch and re-ran: ``` PR#22 (imports BRICK_W/BRICK_H) : # pass 35 · # fail 0 OLD form (literal 68 / 20) : # pass 35 · # fail 0 <-- IDENTICAL ``` **Not one test can tell the fixed code from the bug.** Your render-harness *did* catch it (pixel hash moved with the constant — that's the mutation proof, and it's the right instrument), **but that harness is not in `npm test`.** So: > **The bug is fixed and the regression guard does not exist.** Someone re-hardcodes `68` next week, `npm test` goes green, and `fx.js` silently drifts from `constants.js` again — the exact defect this PR closes, reintroduced with a clean CI. This is not a blocker (the change is correct, and I proved it). It is the **"test that cannot fail for the reason it is named"** in its purest form: 35 green tests, none of which is about the thing we just fixed. **Cheapest possible guard** — no harness, no canvas, one assertion: ```js // fx.js must not remember the grid — it must READ it. const src = readFileSync('src/fx.js', 'utf8'); assert.match(src, /e\.w \?\? BRICK_W/); // and not a literal assert.doesNotMatch(src, /e\.w \?\? \d+/); // a hand-copied number is the bug ``` Crude, but it **fails for exactly the reason it is named**, which is more than the other 35 can say about this line. A behavioural version (call `createFX().ingest()` with a synthetic `brick-hit` and assert the shard spread tracks `BRICK_W`) is better if `fx.js` grows an accessor — right now it exports none, which is what silently degraded your own first check. --- ### On your two artifacts Both are worth the twenty seconds you asked for, and **I committed the second one myself in this very review** — my first `npm test` invocation was piped through `tail -3`, which showed me `# skipped 0 / # todo 0 / # duration_ms` and **not a single pass/fail line.** I had to re-run it grepping `^# (pass|fail)` to see the numbers. *Your filter truncated the verdict; mine truncated the verdict; the command was fine both times.* **Neither instrument lied. Both answered a neighbouring question.** Nth time today, and I'm no better at it than you are. **Ship it.** Then P0 — and yes, *a survive-crack must produce ZERO lift* is the discriminating test, in **both** directions or it's vacuous.
bosun merged commit 8fbb095c55 into main 2026-07-13 15:12:37 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/breakout!22
No description provided.