Release 1.16.0 — Waves, and one world to keep - #238
Merged
Merged
Conversation
…drawer The one red every lane in roadmap 29 carried as "pre-existing" is a TEST fault, not a product one. No core code changes here - `git diff -- src/` is empty. - WHEN IT STARTED: the check was written at 4799994 (roadmap 14 PM, 2026-07-24), when ConnectInfoDrawer rendered `{#if $drawerSlot}<CloudSlot ...>` inline in its single body, so opening the drawer with the chevron showed the plugin's section straight away. The NEXT DAY 4b7b8cf ("one tabbed drawer (Info/Rooms/Toasts)") moved the cloud mount behind the Rooms tab, and nobody updated the suite. It has been red for the whole life of the tabbed drawer - about two months. - MEASURED with a throwaway probe rather than inferred: plugin loaded, chevron clicked -> {drawerOpen:true, drawerTab:"info", tabs:["Info","Rooms","Toasts"], roomsBtn:true, section:false}; the Rooms shortcut or the Rooms tab clicked -> section:true, and `.cloud-slot` count 1 -> 2. Inert build: no tabs, no Rooms button, no cloud slot. - THE PRODUCT IS RIGHT and stays untouched. The chevron opening on Info is deliberate (toggleInfo keeps the last tab; the drawer's own job is connection and server info), and the plugin's content has a first-class way in: Connect grows `#connect-rooms-button` exactly when `$drawerSlot` is set, and `openRooms` opens the drawer on that tab. - THE CHECK now drives that shortcut - the way the app offers the mount - and asserts the section renders on the Rooms tab. - NEW CHECK `M1d: no Rooms affordance in the inert build`: the Rooms shortcut exists only because a plugin mounted drawer content, which is what stops the corrected check passing vacuously. 17 checks -> 18. Nothing was deleted. - The e2e skill drops open-core-m1 from both dirty-baseline lists, says where the drawer mount now lives, and records the lesson: identical-on-base only rules out YOUR diff, so when a red is pre-existing, spend two minutes on when it started and what changed then. Counterfactuals (product broken, then restored byte-identically): - removed `<CloudSlot mount={$drawerSlot} />` from the Rooms tab -> `PM: drawer section renders on the drawer Rooms tab` FAILS. - dropped `$drawerSlot &&` from Connect's Rooms-button gate -> `M1d: no Rooms affordance in the inert build` FAILS. - both breaks together: exactly 2 FAILURES, no others; restored -> 18/18. Gates: open-core-m1 16/1 -> 18/18; battery under the lock ALL PASS (ai-presets, approval-timeout, connect-states, dial-metadata, join-result, net-handshake, open-core-m1 - 7 suites, 203 checks, 399s); svelte-check 336/47 unchanged (identical by construction - no src change); vitest 178; build green server-down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- src/lib/physics.js: `bodyVelocityOf` reports `held` for a `user` hold only (the grab it was documented for) and adds the raw `hold` kind. `held: !!entry.hold` folded the EXTERNAL hold — the engine yielding to another writer (a module walking a body every frame, a peer's move stream) — into "somebody is carrying it". - src/lib/knock.js: `evaluateProbe` still skips a carried body; on the initiator `fireKnock` treats a body that refused the impulse because it is DRIVEN (`hold === 'external'`) as a real hit — logged and sent like every other, the impulse alone refused (the next write would erase it) — which is the rule the receive side already keeps for a held crate (noteRemoteHit logs, applyHit refuses, independently). A body nobody drives that still refuses (gone, not dynamic) sends and spends nothing, as before. - Found by the waves template (29-F): its enemies are dynamic bodies the waves module walks each frame and its damage source is `hit`. On the one peer stepping the world every sweep read 0 hits with the body at `hold: 'external'` (a solo probe, eight sweeps), while a non-initiator (no body, `held: false`) hit them fine — the host could not damage a walking enemy. Counterfactual (game-waves against the staged scene): `held: !!entry.hold` restored -> 58/25, red from 4.1/4.2 (the initiator's slow knock reads 0 hits) and 4.3/4.4 (nothing for B to log) through the whole round (6.1-6.12); fix restored, physics.js identical. knock-physics 1.15 (a CARRIED body is never knocked) held in the same battery. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… suite - scripts/author-templates.cjs: `waves` joins MODULE_DEFS (modules/waves/waves.def.json, emitted by `npm run build:waves`; both modules in installModules, so the file's derived requirement list names health + waves). Built against the lane at 5215; byte-stable across two builds (compare-authored SAME). Staged for the scenes repo in cloud-lane-29-staging/games/waves/ (scene 11701 B + thumb 1690 B + index-row + README with the release order and the #230 ref note). - tests/e2e/game-waves.test.cjs (90 checks; two peers + a late joiner; skip-never-fail): the card is picked in the REAL Games tab (the feed's index.json + scene served through a route, since the row is not released) and loadRemoteScene replaces a non-empty world; the file's 10 objects, both modules, the sunset env, the play block (grab/grounded/simOnPlay) and the knock block; the modules derive 4 enemies on a 3-wave curve, a goal, three spawn pads, five health rows; B over the handshake; Start -> playing on both -> the HUD screen; the run is ON, the first enemy WALKS (+z) and B places it where A does; HUD Text reads the Waves Value nodes and the bar the player's health; kills are KNOCKS through core's feedProbe (a slow sweep = one pulse, a hard one = three), the hit in B's hit log and B's ledger agreeing, B's knock (a non-initiator) completing wave 1, the kills rows per peer, wave 2 healing the survivors one death's worth; the late joiner reads wave 2 and the same ledger from the triggers handshake; waves 2 and 3 fall one enemy at a time -> DONE on all three -> over (won) -> the over screen -> ONE run logged on every peer; Again -> a new round with every enemy back at full health. - .claude/skills/e2e-verify/SKILL.md: the suite in the GAMES-TAB line. The suite reads core's own SCENES_BASE fallback out of sceneTemplates.js, so the #230 ref move follows by construction. Counterfactuals: the def's env preset `dusk` (not a core preset; PR #8's def) -> 1.6 red (studio), fixed to `sunset` on modules feat/29f-waves-template (def-only, module.js and the zip unchanged); `waves` out of MODULE_DEFS -> the build refuses the slug and the suite SKIPs (no scene to test); the knock fix reverted -> the initiator's knocks 4.1/4.2/4.5/4.6 red (its own commit). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r-looking name (#230) - SCENES_BASE default moves scenes@v2 -> scenes@format-2 and PACKS_BASE packs@v1 -> packs@format-1; contentBase() is untouched, so the deploy-time VITE_* override still wins (production keeps VITE_SCENES_BASE=...scenes@main until this release ships) - WHY: jsDelivr parses `v2` as a SEMVER VERSION (`x-jsd-version-type: version`, `cache-control: immutable` for a year), so `git tag -f v2` was a no-op forever and four purges changed nothing -- the Games tab shipped three games while the feed had six. Measured today: a plain TAG named `format-2` (scenes, a5ebe8f) and `format-1` (packs, 03b9568) are reported `x-jsd-version-type: branch` with `s-maxage=43200`, and scenes@format-2/index.json lists all six games (towers, stars-room, football, jam-room, dungeon-realms, untangle). packs@v1 was the identical trap, unexploded only because it had never been retagged, so it moves in the same change - the three games suites' feed fallback follows (game-untangle / game-football / game-dungeon-realms), plus the one packs@v1 asset URL in scripts/author-templates.cjs (sha256-pinned, same bytes at format-1) - tests/unit/contentBase.test.js (4): the override wins with a trailing slash trimmed; an absent override ships the fallback byte-identically; the two consumers' SOURCE literals are the format-N refs; and no shipped fallback names a ref jsDelivr would parse as a version. Counterfactual: scenes@v2 restored in sceneTemplates.js -> 2 of 4 red ("the scenes default is scenes@format-2" and "no shipped fallback names a ref jsDelivr would parse as a semver version"); restored byte-identically -> 4/4 - docs: the SCENES_BASE/PACKS_BASE JSDoc carries the ritual and the measurement; PACKS.md, CLAUDE.md (games-suite line + the #230 gotcha closed) and the e2e skill name the new refs. The scenes/packs READMEs and cloud's .env.deploy.example move in their own PRs (scenes#7, packs#1, cloud#28) - gates: svelte-check 336/47 with byte-identical message lists; vitest 182 (base 178); battery held (see the handover table); build green server-down Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…deterministic rule
THE BUG, measured on a real two-peer Football match (24-B's handover): two Play
presses inside the sim's start-up window both pass `playMode.maybeSimOnPlay`'s
`simulating || remoteSimulating` guard, because the other side's `simulate` has
not landed yet. Both peers then step a world and broadcast `move` at 30 Hz, each
stream reads as an EXTERNAL write on the other, and every dynamic body sits under
a `hold: 'external'` refreshed long before its 250 ms timeout can expire — 74
moves in ~2 s, the ball snapping back, `applyThrow` eaten, NO GOAL COULD SCORE.
A late joiner that is already simulating meets the same shape.
THE RULE: **the lower peer id keeps the world.** The guard cannot be fixed where
it stands — a peer cannot know it is racing until the message arrives — so the
rule is on the RECEIVE side, and it reads only two facts both sides already hold
(my id, the id in the message), so both reach the same verdict with no round trip
and no new message. Checked against what is already there before committing to
it: PeerJS ids are non-empty strings and `<` is a total order, so exactly one
winner is elected; `remoteSimulating` is already set from that same `peerId`; the
handshake push is symmetric (both sides send one), so a joiner race resolves the
same way; and the football module's `isAuthority()` ALREADY falls back to the
lowest live id when no sim runs, so core's winner and a module's fallback
authority are one peer by construction.
- `src/lib/simAuthority.js` (NEW, imports nothing): `simulateVerdict` ->
keep | yield | adopt | clear | ignore, with the reasoning. ADDITIVE: a message
with no `peerId` (an older build) takes the pre-29-F path verbatim, and a
session with no race in it never reaches a verdict but `adopt` and `clear`.
- `physics.applySimulate` acts on the verdict. YIELDING IS CLEAN, NOT MERELY
QUIET: `stopSimulation({yielded: true})` withholds the settling `move` per body
(each would pin one of the winner's copies one last time — the very shape the
yield exists to end) and the transformSet undo entry (Ctrl+Z over a layout
nobody ever saw).
- The winner has two mirror duties: `releaseExternalHoldsBy(peerId)` drops the
holds the loser's moves had already claimed (the same `releaseHold` the 250 ms
timeout would run, only sooner), and it ANSWERS the competing claim with its
own start — redundant in an ordinary race, where the two starts cross, and the
only thing that ever reaches a peer which never heard ours (one that travelled
into the room after the run began: the push rides `sendHandshake` and is not
repeated on arrival).
- A SPECTATOR agrees with the racers: told about two simulators it keeps the
lower id, and a stop from a peer it was not watching no longer blanks
`remoteSimulating` — that store is what arms the knock probes and play-mode
grab (24-A A2), so blanking it silently disarms a third peer mid-match.
- `tests/unit/simAuthority.test.js` (14): the truth table, including that the two
sides of a race reach OPPOSITE verdicts over a spread of real-shaped ids — the
property two browsers cannot show in reasonable time.
- `tests/e2e/game-football.test.cjs` section 7 (+32 checks, 102 -> 134): the race
as it happens (both presses, nothing between them), then FORCED both ways
because two presses do not reliably race, then the goal that scores. Section 2
keeps its ordered entry so the rest of the suite has a known authority.
Counterfactuals, each broken and restored byte-identically:
- the whole rule -> `adopt`: 111/8, section 7 red incl. "A GOAL SCORES" (and the
winner was the HIGHER id that run — the nondeterminism the rule removes).
- the `yielded` suppression removed: 132/3 — one settling move, undo 4 -> 5.
- the spectator start rule removed: 132/3 — the spectator adopts the higher id.
- the stop-side `ignore` removed: 129/6 — a loser's stop blanks the spectator.
- the keep re-announce removed: 131/4 — the forced higher-id yield never resolves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[fix] open-core-m1: the drawer-mount check was asserting the pre-tab drawer
[fix] content refs: scenes@format-2 and packs@format-1, never a semver-looking name (#230)
[feat] 29-F: the waves template, and a knock fix it found
…ally carry FOUND by running the modules football flight against the fix: two real Play presses do NOT reliably race. The first peer's `simulate` often lands before the second peer's guard is read, and then nothing raced at all and whoever pressed first keeps the world — higher id or not. Measured on the flight: A (ce526) kept it while B (33ec2) never started, which is correct behaviour and made a "the lower id wins" assertion red. So the presses assert what they can carry — EXACTLY ONE simulator, and the other peer knowing who it is — and the ID RULE is left to 7b, where the race is forced and has no timing in it. Section 7 then hands the world to the lower id explicitly (a no-op when the presses did race) so 7b starts from the state the rule elects. Without this, 7.1 was a coin: it passed three runs and would have gone red the first time the presses happened not to race. The first counterfactual run had already shown the shape — with the rule removed the HIGHER id kept the world — and that reading was mistaken for the counterfactual biting rather than for what it also was. game-football 134 -> 136 checks, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[fix] 29-F: a peer receiving `simulate` while simulating yields by a deterministic rule
… status entry - CHANGELOG: the 1.16.0 section (the Waves game + the format-2/format-1 content refs, the simulate-race rule + the external-hold knock fix, the open-core-m1 housekeeping line), in the wording the four 29f lanes wrote for the integrator. - CLAUDE.md: the round-3 status entry; the older duplicate of the "TWO PLAY PRESSES" gotcha (round 1, still calling the rule an open ticket) dropped in favour of the copy PR #236 wrote. - Gates on the union (7d0010d = #233 #234 #235 #236 over v1.15.1): build green, svelte-check 336/47 identical list, vitest 196; battery under the e2e lock: open-core-m1 18/0, dial-metadata 44/0 (first run 29/1 "context destroyed by navigation", green on re-run), ai-presets 46/0, scene-physics-state 37/0, session-scenes 36/0, knock-node 50/0, knock-physics 77/0, throw-peer 28/0, play-interact 46/0, game-towers 20/0, game-stars-room 36/0, templates-modal 45/0, template-modules 27/0, game-untangle 46/0, game-dungeon-realms 64/0, game-football 136/0, game-waves 90/0; modules flights football 93/0, door-keypad 13/0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
[docs] 1.16.0 "Waves, and one world to keep": changelog + CLAUDE.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap 29 round 3: #233 (open-core-m1 test fix), #234 (waves template def + suite, the external-hold knock fix), #235 (content refs scenes@format-2 / packs@format-1, #230), #236 (the simulate-race rule), #237 (changelog + CLAUDE.md).
Gates on the union: build green, svelte-check 336/47, vitest 196, 17-suite serial battery green + two modules flights. Tag v1.16.0 follows via
npm version minoron main.🤖 Generated with Claude Code