Repository navigation
Releases: wanoo/web-scumm
Release list
v4.1.15-rc.1
Release candidate rc.1 of 4.1.15: a pre-release for a cycle of observation; the final tag may differ.
"Remix": the programme's eighth and last release, the fourth with a release candidate, and the release
candidate of 4.2 (D28: the DSL and the IR are frozen). One game, several worlds: a variation manifest and a seed give
an immutable world (items among tagged anchors, actors' starting rooms and rounds, coupled hints and answers, puzzle
order, presentation on its own stream), deterministic (tested in Node; the cross-runtime check is written, not run),
every world of a catalogue proved
(verify:variants), Math.random banned from the logical path; the save envelope v4 carries the world and a v3 save
gets the story (breaking, with its migration); the title screen's Remix, seed codes with a check symbol, the daily
challenge and Mystery seeds committed by the Bridge and checked offline; the code wheel, a playful reconstruction with
its accessible alternative and its printable version; the Studio's Remix tab; the reference chapter in Story, Remix
and daily. Measured against 4.1.14 in docs/dev/baselines/4.1.15.md; what this release does not do is in the LOG and
the passes sheet (docs/dev/passes/4.1.15.md).
Fixed
-
A tag older than 4.1.14 releases again (4.1.15):
release.ymlruns frommainwhile it checks out the tag's commit,
and since 4.1.14 it verified and attached a speedrun file the 4.1.10 and 4.1.13 commits do not have: both release jobs
failed after every check had passed. The speedrun is now verified and attached only when the tag carries it. -
A Bridge on SQLite no longer exits on "database is locked" when several start at once (4.1.14): the opening's
PRAGMA journal_mode = WALran outside the store's busy wait, so of severalserveprocesses opening one fresh
file the one that met another's lock exited (1). Every statement now waits (the opening's pragmas, the statements
inside a transaction, the connection's owntimeouttoo), and past its wait answersStoreBusyError(a 503), never
a crash; a poll of the other instances' acceptances that stays busy is logged once (store.poll.failed) and tried
again.
Breaking
- The save envelope v4 (4.1.15, ADR 0018). A save is written as
SaveEnvelopeV4(schema: 4): the v3 envelope
and theWorldVariantthe game was played in.parseSavereads v3 and v4; a v3 save migrates to the story world
(upgradeEnvelope) and loads as before, and the golden saves of 3.0.0 to 4.1.9 still reach the ending. A save from
another world is refused withSaveWorldMismatch, which names the world to rebuild. A host that wrote or checked
schema: 3itself seesschema: 4and avariantfield (UPGRADING §27); a 4.1.15 save does not load on 4.1.14.
Changes
-
The release workflow's install steps get 20 minutes instead of 8 (4.1.15): the
v4.1.10-rc.1release job timed
out onapt-get install ffmpegon 7 October 2026, as CI's jobs had a dozen times that day (ci.yml got the same in 4.1.14). -
ffmpeg on the runners through
scripts/ci-ffmpeg.sh(4.1.15): apt first, bounded to four minutes an attempt,
then a static build from GitHub's CDN (BtbN/FFmpeg-Builds) when the apt mirror hangs, as it did for twenty minutes
without a byte on 7 October 2026. Every ci.yml job that needs ffmpeg uses it; release.yml carries the same logic inline, because it runs frommain
while checking out a tag's commit that may predate the script. -
The first visit's JavaScript goes from 125 to 132 KB gzipped in the sample game (137 in the reference chapter),
and the reference chapter's witness changes (4.1.15): the save envelope carries the story world (a SHA-256 written
out and the world's schema,core/remix/story.ts) and the minigames draw from the run's seed; the world's compiler,
the Remix menu and the daily challenge's Reality code load only when a game with a manifest boots or a daily link is
opened (the budget,initialJsKB140, holds; a game withoutrealitystill never precaches the Reality chunk). The
reference chapter's content moved (the password and the wheel are optional puzzles, the seller's round varies): the
baseline moved on purpose, every golden save still loads. -
The French locales of the two bundled games say « vendeur » where the Remix lines said a word the asset audit
blocks (4.1.15):npm run audit:assetsis part ofbuild:game, and CI refused the build. -
Remix: several worlds of one game (4.1.15, ADR 0018). A game may declare a variation manifest (
remix) and
tagged anchors in its rooms: an item among anchors, a character's starting room, his round among scripts, a code
coupled with its hint ({code:<id>},{hint:<id>}in every language), an order of puzzle groups, alternative lines,
images, palettes or minigame parameters. A seed (WS-XXXX-XXXX, with a check symbol) makes the same world and the
same hash (tested in Node; the cross-runtime checknpm run e2e:remixis written, not yet run); the world is plain data (reserved flagsremix.*the content reads with{ flag, eq }),
so the engine, the solver and the replay need nothing new. An impossible manifest is a build error; a malformed seed
an explicit error. The sample game hides the pantry key under the oranges or in the lantern; the reference chapter
moves the seller and his round, lets Lou hand the board before the lights, and draws a festival password with
Grandma's riddle, in English and French. -
Every world proved (4.1.15, D25).
npm run verify:variantsvalidates and solves each world of a catalogue mode
(--prove: no softlock), and a generator's published sample, with coverage per dimension and pair, values never
chosen and dominant ones; it runs inverify:game, and a release publishes the bundled games' reports.npm run remix -- --seed|--previewshows a world. Measured:demo3 worlds,reference24 worlds per mode, all proved. -
Remix in the player (4.1.15). The title screen's Remix offers the story, a new world, a typed seed or the
daily challenge; the pause menu shows the world's code to copy, or "hidden until the end" in a masked mode; links
?seed=,?daily=,?world=name a world. Sessions record their world and a replay rebuilds it; speedrun
categories Story, Fixed, Random, Mystery and Daily keep their leaderboards apart. -
The daily challenge and Mystery seeds (4.1.15, D26). A new Bridge module signs the day's seed and rules and
commits to a Mystery seed before revealing it; the player verifies both offline with the key the game's manifest
names. The Bridge never chooses a seed after seeing actions. -
The code wheel (4.1.15). "The Extremely Legitimate Pirate Check", a playful reconstruction of 1990s code wheels
drawn from the game's characters (never DRM): the minigamecode-wheel(parody by default), seeded by the world,
accessible by buttons, keys, gamepad and a text list, andnpm run code-wheelprints it (SVG, PDF with Pillow). The
reference chapter has one on Lou's map. -
The Studio's Remix tab (4.1.15). Preview a seed, lock and reroll, compare, coverage and bias, anchors from the
scene, play or export a frozen world. -
The DSL and the IR are frozen (4.1.15, D28). 4.1.15 is the release candidate of 4.2:
docs/dev/DSL-STABILITY.md
lists everything frozen, Remix's additions included.Math.randomis forbidden insrc/engine/coreand
src/engine/minigamesby the linter; the minigames draw from the run'sminigame:<id>stream (MinigameCtx.random). -
After the second reading (4.1.15). A link to another world never replaces a saved game silently: the page starts
in the saved game's world and the title asks before the link's world replaces it; the stores keep a save of another
world instead of writing over it. A stored or linked world is checked value by value against the game whatever its
hash says (integrity is not authenticity). A generator backtracks instead of dead-ending; a presentation value cannot
touch a minigame parameter that decides a win; Mystery commits are limited per client and a Mystery run must start
within a minute of its reveal; seed codes refuse non-ASCII lookalikes;newSeedthrows without WebCrypto.
Manual passes (D12: reported, not blocking)
0 of 16 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| Five Remix seeds of the reference chapter played by people to the end | not done | |||
| A daily challenge played on two devices the same day: the same world, the same code | not done | |||
| The code wheel printed and used at the table; the accessible alternative by a screen-reader user | not done | |||
| Three speedrunners play a category and submit a run | not done | |||
| A real LiveSplit and OBS session on the local tools | not done | |||
| Real phone, frame rate of the heaviest scene on both painters (≥ 30 FPS) | not done | |||
| Screen reader on both painters, the quest journal read aloud | not done | |||
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.14
"Time Attack": the programme's seventh release, the third with a release candidate. A speedrun category is
content (GameDef.speedrun, validated, no change of the engine); the run clock reads real time, logical steps and
logical time and never writes the state; a seeded generator with streams replaces Math.random in the engine
(breaking, with its migration); a run is a chained journal of 500-input chunks that resumes after a crash; a .wsrun
chains every input to a final proof that speedrun:verify replays with one verdict and a code (thirty alterations
each refused); OBS and LiveSplit are local tools; leaderboards on the Bridge verify in an isolated worker;
integrity is not authenticity, said as such. A complete Any% run of the reference chapter is attached to the release,
verified first. Measured against 4.1.13 in docs/dev/baselines/4.1.14.md; what this release does not do is in the
LOG and the passes sheet (docs/dev/passes/4.1.14.md).
Breaking
- A session holds 500 entries, not 5,000 (4.1.14, ADR 0016).
SESSION_MAX(Engine.SESSION_MAX) is now the size
of a run's journal chunk: the 501st input starts a new session from the current state, as the 5,001st did. A long
session file is several files (or a run's chunks);replay()follows a session across its rollover (it reported
"nothing happened" at the rollover before). engine.randomdraws from the run's seeded stream (4.1.14). By default the engine no longer callsMath.random:
it draws from thelogicstream of the run's seed (xoshiro128**,core/prng.ts). A host or a test that sets
engine.randomis unchanged; a session whose host chose the seed (Engine.sessions.nextSeed) writesSession.seed.
Changes
-
CI's install steps get 20 minutes instead of 8 (4.1.14):
npm ci,apt-get install ffmpegandplaywright installon
GitHub's runners timed out a dozen times on 7 October 2026 while the downloads were slow; every one passed on a rerun.
A step limit, not a job limit: a hung install still fails the job within its owntimeout-minutes. -
Speedrun categories as content (4.1.14,
docs/en/SPEEDRUN.md).GameDef.speedrundeclares categories (timed on
RTA, IGT or Active IGT; start and finish on semantic events; saves, pauses, hints, reloads, Reality policy, the
fingerprint components a run must match, the inputs, a fixed or random seed), splits and the rules' version;
npm run validatechecks them. A category needs no change of the engine. The reference chapter declares Any%,
Any% No Hints and Real Time. -
The run clock (4.1.14, ADR 0016, D24).
Engine.runClockreads RTA (monotonicNow, never an authority), logical
steps (one per input) and logical time (microticks: the declared durations ofcore/timing.ts,TIMING_VERSION 1;
a line costs the same whatever its language or the text speed); Active IGT leaves out the cutscenes. It observes the
engine and never writes the state; replayed, a session gives the same steps and times (200 generated games). -
A seeded generator with streams (4.1.14).
core/prng.ts: xoshiro128**,PRNG_VERSION 1, a stream per purpose
(logic,cosmetic,minigame:<id>,copy-protection), test vectors for every runtime
(tests/fixtures/prng-vectors.json).rnd[]stays the trace: a verifier draws again and demands the same numbers. -
Speedrun mode in the player (4.1.14). Pause menu › Speedrun › a category: a new game with its seed, a timer,
automatic splits (a missed split never spoils the attempt), the pause menu and the background recorded as intervals,
Export run (.wsrun) at the finish, Abandon run; local records offline (personal best, best segments, sum of
best, attempts, abandons), a ghost of the PB on semantic targets (off the first time a category is played). The run
is written to IndexedDB (web-scumm-runs) in chained chunks of 500 inputs and resumes after a closed tab or a crash
from its last chunk. New interface texts:ui.speedrun,ui.exportRun,ui.abandonRun. -
The proof of a run and its verifier (4.1.14, ADR 0016, ADR 0017). A
.wsrun(schema 1) chains every input by
SHA-256 from the category's rules to a final proof, with the final state's hash;npm run speedrun:verify, the
CLI'sweb-scumm speedrun verifyand the MCP toolspeedrun_verifyreplay it with its seed and give one verdict
with a code and a reason (valid,valid-unranked,invalid-category-rule,invalid-replay,modified-game,
missing-reality-proof,unsupported-version,inconclusive, never valid). Thirty alterations (time, action,
seed, rules, signal, hash, chunk, shape) are each refused with their code. Reality categories keep each signal's
signed JWS and check it with the Bridge's keys. A complete Any% run of the reference chapter is attached to the
release, verified first. -
Routes, ghosts and the Studio (4.1.14).
.wsrouteroutes (exported, imported, compared); the solver's witness as
a logical route, never a record. The Studio's Play tab has a speedrun panel: categories and rules, a splits editor
written back to the game, a preview of the splits on the frame's session, routes, the run's export. -
OBS and LiveSplit, locally (4.1.14, D23).
npm run speedrun:overlayserves an OBS Browser Source (full, compact,
transparent);npm run speedrun:livesplitexports a LiveSplit splits file and drives LiveSplit through its own
WebSocket server. The game posts its run's events to them with?speedrunTool=<port>, nothing else. -
Leaderboards on the Bridge (4.1.14).
bridge/src/runs.ts:POST /v1/runs, a queue, an isolated verification
worker (its own process group, bounded heap and time, no secret, fetch, WebSocket, TCP, UDP and DNS refused in-process
(not an isolation: the deployment's container is), the package approved by fingerprint, its answer
signed with a one-time key), leaderboards per category and seed kind (valid runs, pseudonyms, trust levels),
moderation, deletion on request, 90-day retention purged hourly, a run identified by its inputs (the first submitter
keeps it), ten submissions a minute per client, the envelope dropped once judged. A module for the Bridge's host to
mount (runsRoute). Pseudonyms are not authenticated. -
After the second reading (4.1.14).
reload: 'segment'is reserved and refused by the validator (it was timed like
allowed); a walk's logical length usesMath.sqrt, notMath.hypot, exact alike in every engine; an empty chunk
is refused; the local overlay and autosplitter accept events from the game's origin only (--origin); a time is
ranked only for avalidrun; SPEEDRUN, ADR 0016 and ADR 0017 say whatreplay-validadmits (tool-assisted runs,
crash resumes), that a random seed is the client's choice and that pseudonyms are not authenticated;
docs/{en,fr}/UPGRADING.md§26. -
The Bridge's
realitymutation set is gated again (4.1.10): the 20 survivors thev4.1.10-rc.1run left
unnamed (18 inbridge.ts, 2 inlock.ts): 19 killed by tests (tests/bridge-mutants-tenant.test.ts: the slow
pass's timer and itsclearInterval, a pairing's origin and session id, a code confirmed or claimed twice, a claim
racing a confirmation, thebacklogandpropose.msvalues recorded, the V1 payload, a row whose sequence is not its
own, a fetch whose last page is exactly full, an export past 1000 rows, a third section queued on a keyed lock), 1
named with its reason (if (this.timer)forced true:clearInterval(undefined)does nothing).
Manual passes (D12: reported, not blocking)
0 of 15 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| Three speedrunners who do not know the engine play a category and submit a run | not done | |||
A real LiveSplit driven by speedrun:livesplit; a real OBS Browser Source on speedrun:overlay |
not done | |||
| The same run replayed on Chromium, WebKit and Firefox: same final state, IGT and proof | not done | |||
| A proof of your own game stopped, the machine rebooted, resumed to the same verdict | not done | |||
| Real phone, frame rate of the heaviest scene on both painters (≥ 30 FPS) | not done | |||
| Screen reader on both painters, the quest journal read aloud | not done | |||
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.14-rc.1
Release candidate rc.1 of 4.1.14: a pre-release for a cycle of observation; the final tag may differ.
"Time Attack": the programme's seventh release, the third with a release candidate. A speedrun category is
content (GameDef.speedrun, validated, no change of the engine); the run clock reads real time, logical steps and
logical time and never writes the state; a seeded generator with streams replaces Math.random in the engine
(breaking, with its migration); a run is a chained journal of 500-input chunks that resumes after a crash; a .wsrun
chains every input to a final proof that speedrun:verify replays with one verdict and a code (thirty alterations
each refused); OBS and LiveSplit are local tools; leaderboards on the Bridge verify in an isolated worker;
integrity is not authenticity, said as such. A complete Any% run of the reference chapter is attached to the release,
verified first. Measured against 4.1.13 in docs/dev/baselines/4.1.14.md; what this release does not do is in the
LOG and the passes sheet (docs/dev/passes/4.1.14.md).
Breaking
- A session holds 500 entries, not 5,000 (4.1.14, ADR 0016).
SESSION_MAX(Engine.SESSION_MAX) is now the size
of a run's journal chunk: the 501st input starts a new session from the current state, as the 5,001st did. A long
session file is several files (or a run's chunks);replay()follows a session across its rollover (it reported
"nothing happened" at the rollover before). engine.randomdraws from the run's seeded stream (4.1.14). By default the engine no longer callsMath.random:
it draws from thelogicstream of the run's seed (xoshiro128**,core/prng.ts). A host or a test that sets
engine.randomis unchanged; a session whose host chose the seed (Engine.sessions.nextSeed) writesSession.seed.
Changes
-
CI's install steps get 20 minutes instead of 8 (4.1.14):
npm ci,apt-get install ffmpegandplaywright installon
GitHub's runners timed out a dozen times on 7 October 2026 while the downloads were slow; every one passed on a rerun.
A step limit, not a job limit: a hung install still fails the job within its owntimeout-minutes. -
Speedrun categories as content (4.1.14,
docs/en/SPEEDRUN.md).GameDef.speedrundeclares categories (timed on
RTA, IGT or Active IGT; start and finish on semantic events; saves, pauses, hints, reloads, Reality policy, the
fingerprint components a run must match, the inputs, a fixed or random seed), splits and the rules' version;
npm run validatechecks them. A category needs no change of the engine. The reference chapter declares Any%,
Any% No Hints and Real Time. -
The run clock (4.1.14, ADR 0016, D24).
Engine.runClockreads RTA (monotonicNow, never an authority), logical
steps (one per input) and logical time (microticks: the declared durations ofcore/timing.ts,TIMING_VERSION 1;
a line costs the same whatever its language or the text speed); Active IGT leaves out the cutscenes. It observes the
engine and never writes the state; replayed, a session gives the same steps and times (200 generated games). -
A seeded generator with streams (4.1.14).
core/prng.ts: xoshiro128**,PRNG_VERSION 1, a stream per purpose
(logic,cosmetic,minigame:<id>,copy-protection), test vectors for every runtime
(tests/fixtures/prng-vectors.json).rnd[]stays the trace: a verifier draws again and demands the same numbers. -
Speedrun mode in the player (4.1.14). Pause menu › Speedrun › a category: a new game with its seed, a timer,
automatic splits (a missed split never spoils the attempt), the pause menu and the background recorded as intervals,
Export run (.wsrun) at the finish, Abandon run; local records offline (personal best, best segments, sum of
best, attempts, abandons), a ghost of the PB on semantic targets (off the first time a category is played). The run
is written to IndexedDB (web-scumm-runs) in chained chunks of 500 inputs and resumes after a closed tab or a crash
from its last chunk. New interface texts:ui.speedrun,ui.exportRun,ui.abandonRun. -
The proof of a run and its verifier (4.1.14, ADR 0016, ADR 0017). A
.wsrun(schema 1) chains every input by
SHA-256 from the category's rules to a final proof, with the final state's hash;npm run speedrun:verify, the
CLI'sweb-scumm speedrun verifyand the MCP toolspeedrun_verifyreplay it with its seed and give one verdict
with a code and a reason (valid,valid-unranked,invalid-category-rule,invalid-replay,modified-game,
missing-reality-proof,unsupported-version,inconclusive, never valid). Thirty alterations (time, action,
seed, rules, signal, hash, chunk, shape) are each refused with their code. Reality categories keep each signal's
signed JWS and check it with the Bridge's keys. A complete Any% run of the reference chapter is attached to the
release, verified first. -
Routes, ghosts and the Studio (4.1.14).
.wsrouteroutes (exported, imported, compared); the solver's witness as
a logical route, never a record. The Studio's Play tab has a speedrun panel: categories and rules, a splits editor
written back to the game, a preview of the splits on the frame's session, routes, the run's export. -
OBS and LiveSplit, locally (4.1.14, D23).
npm run speedrun:overlayserves an OBS Browser Source (full, compact,
transparent);npm run speedrun:livesplitexports a LiveSplit splits file and drives LiveSplit through its own
WebSocket server. The game posts its run's events to them with?speedrunTool=<port>, nothing else. -
Leaderboards on the Bridge (4.1.14).
bridge/src/runs.ts:POST /v1/runs, a queue, an isolated verification
worker (its own process group, bounded heap and time, no secret, fetch, WebSocket, TCP, UDP and DNS refused in-process
(not an isolation: the deployment's container is), the package approved by fingerprint, its answer
signed with a one-time key), leaderboards per category and seed kind (valid runs, pseudonyms, trust levels),
moderation, deletion on request, 90-day retention purged hourly, a run identified by its inputs (the first submitter
keeps it), ten submissions a minute per client, the envelope dropped once judged. A module for the Bridge's host to
mount (runsRoute). Pseudonyms are not authenticated. -
After the second reading (4.1.14).
reload: 'segment'is reserved and refused by the validator (it was timed like
allowed); a walk's logical length usesMath.sqrt, notMath.hypot, exact alike in every engine; an empty chunk
is refused; the local overlay and autosplitter accept events from the game's origin only (--origin); a time is
ranked only for avalidrun; SPEEDRUN, ADR 0016 and ADR 0017 say whatreplay-validadmits (tool-assisted runs,
crash resumes), that a random seed is the client's choice and that pseudonyms are not authenticated;
docs/{en,fr}/UPGRADING.md§26. -
The Bridge's
realitymutation set is gated again (4.1.10): the 20 survivors thev4.1.10-rc.1run left
unnamed (18 inbridge.ts, 2 inlock.ts): 19 killed by tests (tests/bridge-mutants-tenant.test.ts: the slow
pass's timer and itsclearInterval, a pairing's origin and session id, a code confirmed or claimed twice, a claim
racing a confirmation, thebacklogandpropose.msvalues recorded, the V1 payload, a row whose sequence is not its
own, a fetch whose last page is exactly full, an export past 1000 rows, a third section queued on a keyed lock), 1
named with its reason (if (this.timer)forced true:clearInterval(undefined)does nothing).
Manual passes (D12: reported, not blocking)
0 of 15 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| Three speedrunners who do not know the engine play a category and submit a run | not done | |||
A real LiveSplit driven by speedrun:livesplit; a real OBS Browser Source on speedrun:overlay |
not done | |||
| The same run replayed on Chromium, WebKit and Firefox: same final state, IGT and proof | not done | |||
| A proof of your own game stopped, the machine rebooted, resumed to the same verdict | not done | |||
| Real phone, frame rate of the heaviest scene on both painters (≥ 30 FPS) | not done | |||
| Screen reader on both painters, the quest journal read aloud | not done | |||
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.13
"Solver Research": the programme's sixth release, named by the two thresholds its sheet fixed before any
code was written. The minimum is met: a proof needs nine times less heap over the proof matrix (up to twenty times
less on the open instances), a long proof stops and resumes to the same verdict and witness, the proof profile says
what multiplies the states, workers share what the search has seen, and every verdict is the one 4.1.8 gave, state
for state. The release objective is not met: eight of the twelve matrix instances finish within budget, as with 4.1.8,
and the gap report (docs/dev/PROOF-MATRIX.md §8) says why. Measured against 4.1.12 in
docs/dev/baselines/4.1.13.md; what this release does not do is in the LOG and the passes sheet
(docs/dev/passes/4.1.13.md).
Changes
-
Three SQLite tests of the Bridge get 30 s on the runner (4.1.13): a tenancy property, a restore and a pairing sweep
took more than vitest's 5 s on the Windows runner three times on 7 October 2026 and passed every time elsewhere. -
A proof needs nine times less heap over the proof matrix, up to twenty times less on the open instances (4.1.13,
ADR 0015). The search stores the states it has seen by index, with exact interned keys, parents and steps in flat
columns, and keeps a state's engine copy only while it waits to be expanded. Over the twelve instances of the new
proof matrix the peak heap falls ÷9 (÷4.5 in RSS), and ÷16 to ÷20 on the large open ones o21, o23 and o25 (1.6 KB a
state instead of 31.6 on o21); the time per state, the engine's runs, does not change. The verdicts, paths, softlocks and reachable sets are those of 4.1.8, state for state, on the sample game,
the reference game and 200 generated games (tests/solver-oracle.test.ts).--representation=objectskeeps the
4.1.8 storage. -
A long proof can be stopped and taken up again (4.1.13).
npm run solve -- --prove --checkpoint=<file>writes
the search down every five minutes (--checkpoint-every) and when a budget stops it;--resumetakes it up to the
same verdict and the same witness, even after the process was killed or a budget stopped it inside a state's
expansion.--mem=<MB>stops a search astruncated
past a heap size. A budget that cuts a search never givesproved. -
The proof profile says what multiplies the states (4.1.13).
npm run solve -- --prove --profileattributes the
states to positions, inventories, flags, dialogues and scripts (how many would merge without each), counts the
symmetries folded, the tries that changed nothing and the orders merged, and names every abstraction with what it did
or why it is off. A softlock cause now carries its session entries:npm run replayplays the way into it. -
The proof matrix (4.1.13,
docs/dev/PROOF-MATRIX.md,npm run prove:matrix, nightly). Twelve generated games of
20 to 40 rooms and three playable characters, six constrained and six open, with published budgets and expected
verdicts. 4.1.13 proves the same eight of twelve as 4.1.8 within ten minutes on the maintainer's Mac; four open ones
run out of time, and the gap report says why (the engine's runs on tries that change nothing, and who carries which
key). This release is therefore "Solver Research". -
Symmetric items, and workers that share what the search has seen (4.1.13).
--symmetryfolds two items the game
treats alike (off by default: none in the bundled games). With--workers, a worker sends back a state the search
already stored without its engine copy, and nodes are dealt by room with work stealing; the result stays the same for
any number of workers. -
One checkpoint case fewer in the counted declarations (4.1.13): the killed-process resume is skipped on Windows
(the test reads the snapshot while the child renames a new one over it); the baseline's count moves by one on purpose. -
The coverage floors raised to within three points of what the tests reach (4.1.10): lines 66, statements 65,
functions 61, branches 62 (64 / 63 / 59 / 61 before),reality/protocol.tsbranches 96. The strict ratchet on the
v4.1.10-rc.1tag refused the five floors Constellation's tests had left behind (measured 67.91 / 67.21 / 63.31 /
64.09 and 98.75); the pull request had only warned. -
e2e:realitywaits two minutes for the ending cutscene after the gate (4.1.10), 30 s before: on a runner
carrying five runs the cutscene outlasted it twice today (Chromium and WebKit) with every other check green. -
The coverage floors raised again for 4.1.11's tag (4.1.11): lines 67, statements 67, functions 63, branches 63;
reality/client.tsbranches 90,bridge/src/server.tslines 94 and branches 85,bridge/src/cli.tsbranches 66.
Viewport's tests brought the measure to 69.86 / 69.05 / 65.32 / 65.60 (PR #42's coverage job), and the strict
ratchet of a tag refuses a floor three points or more below it. -
e2e:realityacts on the gate again when the ending does not come (4.1.12): an input while the engine is busy is
dropped, as a player's tap is, and the shed's cutscene could still be running when the harness used the gate; the
check waited 120 s for an ending that never came (one WebKit run in three on 7 October 2026). Now: wait for the
engine to be free, act, and act again up to three times, 40 s each.
Manual passes (D12: reported, not blocking)
0 of 13 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| A proof of your own game stopped, the machine rebooted, resumed to the same verdict | not done | |||
| The proof profile read by an author to simplify a game | not done | |||
| Real phone, frame rate of the heaviest scene on both painters (≥ 30 FPS) | not done | |||
| Screen reader on both painters, the quest journal read aloud | not done | |||
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.12
"Language": the programme's fifth release. The game's logic as plain data (GameIR, deterministic, with
the file and line that writes each id), a fingerprint in four SHA-256 (logic, trusted extensions, presentation,
engine) that the pause menu shows and the build seals, one canonical text per value (canonicalJson, held to fifty
edge values in Node, a script written to compare them in three browsers, not yet in CI), objectives and the quest journal (the one primitive admitted,
with its ADR; every other candidate refused with its proof), forms generated from the schemas in the Studio, the DSL's
reference generated from them, and the DSL stabilised until Remix freezes it. Measured against 4.1.11 in
docs/dev/baselines/4.1.12.md; what this release does not do is in the LOG and the passes sheet
(docs/dev/passes/4.1.12.md).
Breaking
- The pause menu's fingerprint row in every game (4.1.12). A game whose release language is not English shows its
English default ("Build") until it addsui.fingerprint(andui.objectiveswhen it declares objectives) to its
uiand its translation tables; the release-language e2e (npm run e2e -- --lang xx) fails on a visible default.
The sample game and the reference chapter carry both, in English and French (UPGRADING §24).
Changes
-
Objectives and the quest journal (4.1.12, ADR 0014). A game may declare
objectives(title,done,
optional,parent). The pause menu lists them, each step under its parent, ✓ done or ○ open; the semantic journal
saysobjectiveCompletedonce, right after the event that completes it (even inside a cutscene, before what
follows, the autosave and an ending), never again in the session, and silently for what already holds when a game
starts or loads;npm run solve -- --goal=100%searches for a state where every objective that is not optional
holds at once. The validator refuses an objective whosedonecan never hold (naming a custom command without
declaredeffects), an unknown parent and a cycle of parents, and warns about adonethe content can take back.
The flag and item handlers now change the state before they journal it. The sample
game and the reference chapter declare five each, translated into French. The save format does not change. -
The game's intermediate representation (4.1.12, ADR 0013).
compileIRturns a compiled game into its logic as
plain data (rooms, entities, rules, scripts, objectives, Reality policies, the world, the trusted extensions by name,
a variant slot reserved for 4.1.15), deterministic, with thefile:linethat writes each id.npm run ir -- --game <id> [--json]prints it; the Studio's new Language tab shows it with the objectives; MCP's newget_irreturns
it. Every field of a game, a room and an entity is classified logic, presentation, both or tooling in one table the
compiler checks. The runtime, the solver and the replay keep reading the compiled game. -
The game's fingerprint (4.1.12, ADR 0013). Four SHA-256 computed with WebCrypto:
logic(the IR),
trustedExtensions(the game's code beside its content, hashed by the build),presentation(decors, sprites,
sounds, interface texts and the asset manifest) andengine(its version). A rule changed moveslogiconly, a
decorpresentationonly, a custom command's codetrustedExtensionsonly. The pause menu shows the short form
(ui.fingerprint, "Build" by default); a build writessite.jsonwith the trusted extensions' hash and the
engine's version, whichnpm run verify:distexpects. -
One canonical text per value (4.1.12).
canonicalJson(NFC, sorted keys, no-0, big integers as decimal
strings, anything lossy refused) is held to fifty edge values in Node;npm run e2e:canonicalcompares them in
Chromium, WebKit and Firefox (written in this lot, not yet in CI). The solver's proof cache is keyed by it: the
entries of earlier versions are not reused. -
The Studio writes objectives (4.1.12). Their form is generated from their schema (each field with its
description); a value is checked before it is sent, and the validator's errors come back named by file, id and
field; the diff is previewed before the write and Undo takes it back. MCP'sset_valuewrites them in the game file
withid: "@game";list_roomsshows them. -
The DSL's reference is generated (4.1.12, D22).
docs/en/DSL.mdanddocs/fr/DSL.mdlist every condition,
every command with its shape, the objectives' fields and how each field counts for the IR, from the schemas
(npx tsx tools/dsl-doc.ts, held by a test). The DSL is stabilised:docs/dev/DSL-STABILITY.mdsays what is
stable, what may still grow until 4.1.15, and the candidate primitives of the programme with the proof that admitted
or refused each (objectives admitted; no Reality nor stage primitive needed). -
API (4.1.12), additive.
web-scumm/content:compileIR,canonicalJson,provenanceOf,logicView,
completionGoal,ObjectiveDef,GameIRand the IR's types.web-scumm/testing:fingerprint,
fingerprintGame,presentationOf,hashSources,sha256Hex,shortFingerprint,GameFingerprint. MCP:get_ir.
Engine.objectives,App.fingerprint(),App.objectivesMenu(),BootOptions.buildand twouikeys
(fingerprint,objectives) are new members. -
The first visit's JavaScript goes from 122 to 124 KB gzipped (4.1.12): the fingerprint (WebCrypto), the
objectives and the quest journal are in the player's main chunk; the budget (initialJsKB140) is untouched and the
baseline moved on purpose. -
The validator's migration checks moved to
tools/validate/migrations.ts(4.1.12), beside the objectives'
checks:validate.tsgoes from 1 140 to 1 113 lines, and its cap with it. -
e2e:pwaon Firefox tolerates one file missing from the cache after a reinstall warm-up it reported complete
(4.1.12, CI only; the files are listed), under the same bound as the refused cached files (two): more is a failure.
Chromium and WebKit stay strict. -
A tag's release chain in half the time (4.1.10).
release.ymlruns the two mutation sets as jobs of their own
(core,reality, about half an hour each; the final tag after its candidate reuses the candidate's reports through
the cache) beside
release-check:ci(release-checkwithout the mutation step), and the release waits for all three: about 35
minutes from the tag's green run to the published release instead of 70.ship tag --now(andchain --now) tags
as soon as the pull request is merged instead of waiting for main's run of the same commit: the tag's own run, the
same suite on the same commit, is what the release checks. -
ship tagfetches before reading the commit (4.1.10):chaintagged nothing twice (4.1.9, 4.1.10) because the
merge commit it had just made was on origin only ("not a commit here").
Manual passes (D12: reported, not blocking)
0 of 13 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| A game of your own given five objectives in the Studio, played to 100 % | not done | |||
| The pause menu's fingerprint compared between two builds of the same game by a person | not done | |||
| Real phone, frame rate of the heaviest scene on both painters (≥ 30 FPS) | not done | |||
| Screen reader on both painters, the quest journal read aloud | not done | |||
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.11
"Viewport": the programme's fourth release, the second with a release candidate. The rendering is no
longer a source of state: the room view makes an immutable scene frame with a pure function and paints it, the DOM
and the Canvas painters send the engine nothing but intentions, and the semantic journal of what happened in the
game (rooms, items, flags, endings, loads) belongs to the core, replayed identically by a session. The Studio edits
a room's layers, masks, zones and portals; the validator refuses a degenerate mask. Measured against 4.1.10 in
docs/dev/baselines/4.1.11.md; what this release does not do (every browser measure, the WebGL spike) is in the
LOG and the passes sheet (docs/dev/passes/4.1.11.md).
Changes
- The semantic journal (4.1.11).
Engine.journalnumbers what happened in the game, in ids: a session started, a
room entered (and from where), an item acquired or lost (an item handed to another player is both, each with its
player), a flag changed (only when its value changes;nullwhen it is removed), the player switching character,
an ending reached, a load, and the autosave that follows something semantic. The command handlers, a room's entry and the
engine's lifecycle emit it, nothing in the DOM does, and a kind it does not know is refused. Replaying a session yields
the same journal (the sample game, the reference chapter, 200 generated games). A session file carries it;
npm run replayprints it and exits 1 when the replay's differs; a session longer than the journal's window
(10 000 events) is exported withjournalTruncated: trueand no journal, andnpm run replaythen says "no journal
comparison: the window was exceeded" instead of "matches". The dev panel lists the latest events. - The scene frame (4.1.11, ADR 0011). The room view makes an immutable
SceneFrame(camera, layers, characters,
targets with their hit polygons precomputed, effects, a hash) with a pure function, then paints it. Taps are tested
against the frame and the accessible buttons follow its targets, whatever paints the room. Every room of the sample
game and of the reference chapter, in three states, paints the same DOM and answers the same taps as before. - The player's input is an intention (4.1.11, D21).
Appcomposes the engine, aPresenter(dom/presenter.ts:
the scene's calls, lines, overlays, minigames, the ending) and the room view'sRenderer; a verb on a target, a
walk, a choice, a skip or a screen opened goes through the presenter'sintent(). The same clicks on the DOM and on
the Canvas painter record the same session and journal, at device pixel ratios 1, 2 and 3, on a phone and a desktop
screen, with and without reduced motion. The busy state (runs, the tutorial step, a skip) iscore/busy.ts; a skip
left pending no longer outlives a new game or a load. The page's test hook moved with it:
window.__game.presenter.inventory(…)where e2e scripts calledwindow.__game.inventory(…). - The Canvas painter survives a lost context (4.1.11). Nothing is painted while the browser has reclaimed the
canvas; once restored, the background, the masks and the occluders are rebuilt from their images and the room is
painted again. A tap or a hover reuses the room's frame until something changes (a version counter), instead of
building and hashing it again. The route between walk zones is the core's (core/motion.tszoneRoute), the walker walks it. - The Studio edits a room's layers, masks, zones and portals (4.1.11). Under the room sheet of the Rooms tab: each
layer's depth, parallax, opacity and blend; occlusion masks and walk zones drawn as polygons on the backdrop; links
between zones placed by two clicks; Save stage writes the geometry over the room's layout. - The validator refuses a mask polygon that closes no surface and, in a room of several walk zones, a zone no link
joins (4.1.11). A layout that validated in 4.1.10 with a degenerate mask polygon (collinear points, crossing edges)
now failsnpm run validate; the bundled games pass. - API (4.1.11), additive.
web-scumm/player:SceneFrame,Renderer,Intent(@extension).
web-scumm/testing:SemanticEvent,SemanticJournal(@public).Engine.journalandEngine.sessionSeqare new
members ofEngine. - The first visit's JavaScript goes from 120 to 122 KB gzipped (4.1.11): the scene frame, the presenter, the
intents and the journal are in the player's main chunk; the budget (initialJsKB140) is untouched and the
baseline moved on purpose.
Manual passes (D12: reported, not blocking)
0 of 12 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| Real phone, frame rate of the heaviest scene on the DOM and the Canvas painter (≥ 30 FPS) | not done | |||
Screen reader on both painters (docs/dev/SCREEN-READER.md) |
not done | |||
| A room's layers, masks, zones and portals edited by a person in the Studio, then played | not done | |||
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.11-rc.1
Release candidate rc.1 of 4.1.11: a pre-release for a cycle of observation; the final tag may differ.
"Viewport": the programme's fourth release, the second with a release candidate. The rendering is no
longer a source of state: the room view makes an immutable scene frame with a pure function and paints it, the DOM
and the Canvas painters send the engine nothing but intentions, and the semantic journal of what happened in the
game (rooms, items, flags, endings, loads) belongs to the core, replayed identically by a session. The Studio edits
a room's layers, masks, zones and portals; the validator refuses a degenerate mask. Measured against 4.1.10 in
docs/dev/baselines/4.1.11.md; what this release does not do (every browser measure, the WebGL spike) is in the
LOG and the passes sheet (docs/dev/passes/4.1.11.md).
Changes
- The semantic journal (4.1.11).
Engine.journalnumbers what happened in the game, in ids: a session started, a
room entered (and from where), an item acquired or lost (an item handed to another player is both, each with its
player), a flag changed (only when its value changes;nullwhen it is removed), the player switching character,
an ending reached, a load, and the autosave that follows something semantic. The command handlers, a room's entry and the
engine's lifecycle emit it, nothing in the DOM does, and a kind it does not know is refused. Replaying a session yields
the same journal (the sample game, the reference chapter, 200 generated games). A session file carries it;
npm run replayprints it and exits 1 when the replay's differs; a session longer than the journal's window
(10 000 events) is exported withjournalTruncated: trueand no journal, andnpm run replaythen says "no journal
comparison: the window was exceeded" instead of "matches". The dev panel lists the latest events. - The scene frame (4.1.11, ADR 0011). The room view makes an immutable
SceneFrame(camera, layers, characters,
targets with their hit polygons precomputed, effects, a hash) with a pure function, then paints it. Taps are tested
against the frame and the accessible buttons follow its targets, whatever paints the room. Every room of the sample
game and of the reference chapter, in three states, paints the same DOM and answers the same taps as before. - The player's input is an intention (4.1.11, D21).
Appcomposes the engine, aPresenter(dom/presenter.ts:
the scene's calls, lines, overlays, minigames, the ending) and the room view'sRenderer; a verb on a target, a
walk, a choice, a skip or a screen opened goes through the presenter'sintent(). The same clicks on the DOM and on
the Canvas painter record the same session and journal, at device pixel ratios 1, 2 and 3, on a phone and a desktop
screen, with and without reduced motion. The busy state (runs, the tutorial step, a skip) iscore/busy.ts; a skip
left pending no longer outlives a new game or a load. The page's test hook moved with it:
window.__game.presenter.inventory(…)where e2e scripts calledwindow.__game.inventory(…). - The Canvas painter survives a lost context (4.1.11). Nothing is painted while the browser has reclaimed the
canvas; once restored, the background, the masks and the occluders are rebuilt from their images and the room is
painted again. A tap or a hover reuses the room's frame until something changes (a version counter), instead of
building and hashing it again. The route between walk zones is the core's (core/motion.tszoneRoute), the walker walks it. - The Studio edits a room's layers, masks, zones and portals (4.1.11). Under the room sheet of the Rooms tab: each
layer's depth, parallax, opacity and blend; occlusion masks and walk zones drawn as polygons on the backdrop; links
between zones placed by two clicks; Save stage writes the geometry over the room's layout. - The validator refuses a mask polygon that closes no surface and, in a room of several walk zones, a zone no link
joins (4.1.11). A layout that validated in 4.1.10 with a degenerate mask polygon (collinear points, crossing edges)
now failsnpm run validate; the bundled games pass. - API (4.1.11), additive.
web-scumm/player:SceneFrame,Renderer,Intent(@extension).
web-scumm/testing:SemanticEvent,SemanticJournal(@public).Engine.journalandEngine.sessionSeqare new
members ofEngine. - The first visit's JavaScript goes from 120 to 122 KB gzipped (4.1.11): the scene frame, the presenter, the
intents and the journal are in the player's main chunk; the budget (initialJsKB140) is untouched and the
baseline moved on purpose.
Manual passes (D12: reported, not blocking)
0 of 12 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| Real phone, frame rate of the heaviest scene on the DOM and the Canvas painter (≥ 30 FPS) | not done | |||
Screen reader on both painters (docs/dev/SCREEN-READER.md) |
not done | |||
| A room's layers, masks, zones and portals edited by a person in the Studio, then played | not done | |||
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.10
"Constellation": the programme's third release, the first since 4.1.8 with a release candidate. The Bridge reads and
writes through one store interface (RealityStore): SQLite locally, Postgres for several instances, the 4.1.9 journal
still served and migrated; instances without state of their own, sharing one durable journal that wakes the streams;
tenants isolated by key and by row, each with its own quotas, rotation and revocations; a signal that names its
context (SignalV2, the threat model's answer, V1 still accepted by the player until 4.1.12); health routes, metrics,
backup and restore, quarantine of rows that no longer verify, a load bench of three instances. Measured against 4.1.9
in docs/dev/baselines/4.1.10.md; what this release does not do is in the LOG and the passes sheet
(docs/dev/passes/4.1.10.md).
Breaking
SignalV2, the signal that names its context (4.1.10, ADR 0010).WorldSignalV2addstenantId,
environment,audience(the origin the player paired from),sessionIdandkeyIdto the signed payload;
verifySignalaccepts the versions its expectation allows (both by default), refuses a V2 signed for another
tenant, environment, origin or link (audience-mismatch) and akeyIdthat is not the header'skid(key). A
multi-tenant Bridge signs V2 only; a single-tenant Bridge signs V1 by default until 4.1.12, when V2 becomes the only
version (announced,docs/en/UPGRADING.md§22). A key bound to a tenant signs V2 only: a V1 signal under it is
refused (schema); a V1 Bridge's keys bind no tenant. A V2 signal may name the Bridge's own audience when the
Bridge did not see the player's origin (its keys declare it). The Studio's simulator signs V2 by default;
RealityClientchecks the page's origin by default and the shipped player passes the link'ssessionId. The Rust
cross-check verifies V2 with the same codes (bridge/test-vectors/signal-v2/).- The Bridge's methods are asynchronous (4.1.10, ADR 0009):
startPairing,claimPairing,revoke,
forgetPlayer,exportPlayer,ack,unlink,subscribe,streamAliveandplayerOfreturn promises; the
routes/v1/*are unchanged.--trust-proxyalone trusts the loopback only; the client is the rightmost
X-Forwarded-Foraddress that is not a listed proxy. Behind a proxy elsewhere than on the loopback (a PaaS's
router), every client shares one rate bucket until--trust-proxy=<its network>names it.
Changes
- A durable, replicable, multi-tenant Bridge (4.1.10 "Constellation", D20,
docs/dev/threat-models/constellation.md).
The Bridge reads and writes throughRealityStore, every method taking the tenant first;appendSignaldecides the
deduplication, the sequence (MAX + 1), the quotas and the signature in one transaction. Stores: SQLite through
node:sqlite(thelocalprofile; Node 22.13+, no native dependency), Postgres throughpg(thedistributed
profile,experimentaluntil a real deployment), the 4.1.9 journal (still served;npm run bridge -- migrate --from=jsonl --to=sqlitemoves it). The schema is versioned (bridge/migrations/, up and down). One server serves
several tenants (serve --tenants=…, routed byHost), each with its own keys, root, quotas, rotation and
revocations, and connector tokens bound to their tenant; instances are stateless (a stream on one instance receives
what another accepted, woken byNOTIFYor a short poll). New:/livez,/readyz,/healthz; OpenTelemetry
metrics when@opentelemetry/apiis installed;tenant export|delete,backup,restore; quarantine of rows that
no longer verify (or name another player, sequence or tenant than their row), listed bydoctor;
streamsPerInstance; a store busy beyond 5 s answers 503 withRetry-After; the SQLite files are mode 0600.
Tested: the store contract on memory, SQLite and
Postgres with fast-check properties (concurrent proposals, two tenants crossed), three processes with one killed
during 1 000 proposals, backup and restore rehearsed.npm run bridge:loadmeasures three instances, 1 000 players
and 50 000 proposals (docs/dev/BENCH-BRIDGE.md; nightly on SQLite and Postgres); CI runs abridge-postgresjob.
Manual passes (D12: reported, not blocking)
0 of 13 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering, a signal received after a disconnection | not done | |||
A Bridge behind a PaaS router with --trust-proxy=<its network>, the rate limit per client observed |
not done | |||
| A 4.1.9 journal migrated to SQLite on a real server, the players' streams resumed | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Screen reader (docs/dev/SCREEN-READER.md) |
not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
| Real phone, frame rate of the heaviest scene (≥ 30 FPS) | not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.10-rc.1
Release candidate rc.1 of 4.1.10: a pre-release for a cycle of observation; the final tag may differ.
"Constellation": the programme's third release, the first since 4.1.8 with a release candidate. The Bridge reads and
writes through one store interface (RealityStore): SQLite locally, Postgres for several instances, the 4.1.9 journal
still served and migrated; instances without state of their own, sharing one durable journal that wakes the streams;
tenants isolated by key and by row, each with its own quotas, rotation and revocations; a signal that names its
context (SignalV2, the threat model's answer, V1 still accepted by the player until 4.1.12); health routes, metrics,
backup and restore, quarantine of rows that no longer verify, a load bench of three instances. Measured against 4.1.9
in docs/dev/baselines/4.1.10.md; what this release does not do is in the LOG and the passes sheet
(docs/dev/passes/4.1.10.md).
Breaking
SignalV2, the signal that names its context (4.1.10, ADR 0010).WorldSignalV2addstenantId,
environment,audience(the origin the player paired from),sessionIdandkeyIdto the signed payload;
verifySignalaccepts the versions its expectation allows (both by default), refuses a V2 signed for another
tenant, environment, origin or link (audience-mismatch) and akeyIdthat is not the header'skid(key). A
multi-tenant Bridge signs V2 only; a single-tenant Bridge signs V1 by default until 4.1.12, when V2 becomes the only
version (announced,docs/en/UPGRADING.md§22). A key bound to a tenant signs V2 only: a V1 signal under it is
refused (schema); a V1 Bridge's keys bind no tenant. A V2 signal may name the Bridge's own audience when the
Bridge did not see the player's origin (its keys declare it). The Studio's simulator signs V2 by default;
RealityClientchecks the page's origin by default and the shipped player passes the link'ssessionId. The Rust
cross-check verifies V2 with the same codes (bridge/test-vectors/signal-v2/).- The Bridge's methods are asynchronous (4.1.10, ADR 0009):
startPairing,claimPairing,revoke,
forgetPlayer,exportPlayer,ack,unlink,subscribe,streamAliveandplayerOfreturn promises; the
routes/v1/*are unchanged.--trust-proxyalone trusts the loopback only; the client is the rightmost
X-Forwarded-Foraddress that is not a listed proxy. Behind a proxy elsewhere than on the loopback (a PaaS's
router), every client shares one rate bucket until--trust-proxy=<its network>names it.
Changes
- A durable, replicable, multi-tenant Bridge (4.1.10 "Constellation", D20,
docs/dev/threat-models/constellation.md).
The Bridge reads and writes throughRealityStore, every method taking the tenant first;appendSignaldecides the
deduplication, the sequence (MAX + 1), the quotas and the signature in one transaction. Stores: SQLite through
node:sqlite(thelocalprofile; Node 22.13+, no native dependency), Postgres throughpg(thedistributed
profile,experimentaluntil a real deployment), the 4.1.9 journal (still served;npm run bridge -- migrate --from=jsonl --to=sqlitemoves it). The schema is versioned (bridge/migrations/, up and down). One server serves
several tenants (serve --tenants=…, routed byHost), each with its own keys, root, quotas, rotation and
revocations, and connector tokens bound to their tenant; instances are stateless (a stream on one instance receives
what another accepted, woken byNOTIFYor a short poll). New:/livez,/readyz,/healthz; OpenTelemetry
metrics when@opentelemetry/apiis installed;tenant export|delete,backup,restore; quarantine of rows that
no longer verify (or name another player, sequence or tenant than their row), listed bydoctor;
streamsPerInstance; a store busy beyond 5 s answers 503 withRetry-After; the SQLite files are mode 0600.
Tested: the store contract on memory, SQLite and
Postgres with fast-check properties (concurrent proposals, two tenants crossed), three processes with one killed
during 1 000 proposals, backup and restore rehearsed.npm run bridge:loadmeasures three instances, 1 000 players
and 50 000 proposals (docs/dev/BENCH-BRIDGE.md; nightly on SQLite and Postgres); CI runs abridge-postgresjob.
Manual passes (D12: reported, not blocking)
0 of 13 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| A real multi-instance deployment behind HTTPS (Postgres), a connector delivering, a signal received after a disconnection | not done | |||
A Bridge behind a PaaS router with --trust-proxy=<its network>, the rate limit per client observed |
not done | |||
| A 4.1.9 journal migrated to SQLite on a real server, the players' streams resumed | not done | |||
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
Screen reader (docs/dev/SCREEN-READER.md) |
not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
| Real phone, frame rate of the heaviest scene (≥ 30 FPS) | not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |
v4.1.9
"Gateways": the programme's second release, the same day as the first. Four connectors of the world
outside (email, Telnet, SSH, Open Badges) on one SDK, in a fourth package outside the player and the DSL, each with its
threat model and its abuse tests, all experimental until a real pass; the cadence itself (the CHANGELOG and the LOG
as fragments per branch, the CI in three tiers sized by the change, the mutation job off the pull request path).
Measured against 4.1.8 in docs/dev/baselines/4.1.9.md; what this release does not do is in the LOG and the passes
sheet (docs/dev/passes/4.1.9.md).
Fixed
npm run ship -- verifyon a release candidate (4.1.9). It looked forweb-scumm-4.1.8-rc.1.tgzwhere the
packages carrypackage.json's version (web-scumm-4.1.8.tgz): the rc's sums and eight attestations verified, then
the command failed on that name, inrelease.ymltoo. The tarball's name drops the tag's suffix.
Changes
-
The CHANGELOG and the LOG written as fragments per branch (4.1.9, lot 0 "cadence"). A branch that changes
the code writeschanges/<slug>.md(its bullets under### Breaking,### Fixedor### Changes) and, for a LOG
entry,changes/<slug>.log.md;npm run changes -- --assemblefolds them intoCHANGELOG.md'sUnreleasedand
numbers the LOG entries in the order the fragments reachedmain; CI'scheckjob fails a pull request that
touches the code without a fragment (npm run changes -- --check). During 4.1.8 every merge made the other open
branches conflict on those two files and re-run their CI: that is over (changes/README.md). -
CI in three tiers, sized by the change (4.1.9, lot 0). A pull request runs a fast tier on every change (
plan;
check: formatting, lint, knip, both type checks, the sample game's gates,build:gameinstead ofbuild, the
baseline, the proof;coverage: the unit suite once), then only the heavier jobs its diff can affect, as
tools/ci-plan.tsclassifies it (npm run ci:plan): a docs-only pull request opens no browser, no Windows runner and
no Node 24; a Bridge change runs Reality, a painter the browser rows and the reference chapter. The browser rows and
the Firefox PWA job play thedist/thatcheckbuilt instead of building it seven times;node-24runs the suite
withoutqualityagain;audit:depsruns when the lockfile moved. The seventeen checks the ruleset requires keep
their names and succeed with "not needed by the plan" when spared. The coverage ratchet warns on a pull request
(::warning::) and stays strict onmain, tags, the nightly and release-check. Onmain, on a tag and with the
full-cilabel, everything runs as before; a newpr-gatejob sums every result up, the candidate single required
check (CONTRIBUTING.md, "What CI runs";docs/en/SUPPORT.mdsays what a pull request no longer checks). Themutationjob no longer runs on a push tomaineither: a main run must stay short, since the release
chain waits for the run of the exact commit it tags and a later merge cancels one still going; the nightly and
release-checkmeasure the sets, afull-cilabel on a pull request too. The Firefox PWA job now plays the same build as the Chromium and WebKit PWA rows (the Studio demo included),
from the shared artefact. -
Four connectors of the world outside: email, Telnet, SSH and Open Badges (4.1.9, experimental, D19, ADR 0008).
Each is a process of its own (web-scumm-connector <id> --config <file>, ornpm run connector -- …in the
repository), never in the game nor its DSL, with one Biscuit attenuated to its own signals. Email: a provider's
signed webhook or an IMAP mailbox, the message read in a worker under limits, HTML made inert, attachments refused,
one signal perMessage-ID, a message the Bridge could not take left unseen for the next poll, IMAP without TLS
only to this machine. Telnet and SSH: a virtual terminal (the game's commands,help,exit) and, for SSH, a
virtual disk (ls,cd,cat); no host shell, noexec, nosftp, no forwarding; one shell per SSH
connection; line, rate and time limits, 20 seconds to pair, three connections per address and wrong codes counted
per address across reconnections. Open Badges 2.0 (hosted, signed) and 3.0 (VC-JWT, Data Integrityeddsa-jcs-2022), issuer,
recipient, dates and revocation checked, every document fetched under an SSRF-safe network policy; the verdict is
valid,invalid,expired,revokedorindeterminate. A player links a connector with the pairing code the
pause menu shows. Not done: replies to emails, DKIM and SPF, RDF-canonicalised proofs (indeterminate), a real
provider, badge or exposed terminal tried by a person (docs/en/SUPPORT.md). -
A game declares what its connectors may do, as data (4.1.9):
reality.connectorsholds the words of an email's
answers, a terminal's commands and replies, an SSH disk's files and the badge issuers a game trusts;npm run validatechecks that every signal named is declared, that commands are plain words and not the terminal's own,
and that paths stay inside. The Reality manifest carries the block (its hash changes only for a game that declares
it). A game runs without any connector. -
The connector SDK (4.1.9,
connectors/src/sdk.ts): a connector receives, validates, binds to a player, gives a
dedupeKey(sha256('<source>:<external id>'), the Bridge's existing deduplication key) and proposes; delivery is
at least once and applied once (a proposal whose answer was lost is sent again with the same key, the Bridge answers
duplicate). What a connector saw never leaves it: the Bridge receives the SHA-256 of its payload as
evidenceHash. Limits (size, a local quota, a timeout), metrics and/healthin JSON, a log that writes
[redacted]for anything that looks like content, SIGTERM drained in at most five seconds. -
web-scumm-connectors, a fourth package (4.1.9): one bundled module, its MIME worker beside it,ssh2(MIT)
its only dependency, whose optional native parts are refused (cpu-featuresandnanmap to a refusing stub): in the
repositorynpm cistill runs ssh2's install script, which attempts a native build and fails without thenan
headers, so no.nodefile results (tested); the package is installed with--ignore-scripts.npm run packmakes four tarballs;npm run fresh-installinstalls this one without native
code and runsweb-scumm-connector --help. -
A build that carries server code fails (4.1.9):
verify:dist(innpm run build) refuses a game's JavaScript
that holds any marker of the connectors or the Bridge (connectors/,ssh2,imapflow,web-scumm-bridge…). -
npm run solve:realityproves recorded replays too (4.1.9):games/<id>/replays/*.json(connector inputs and
the signals they made), proved finishable like the scenarios and replayed through the real connector code by a
test, with no network. The sample gamegames/signalsgains "the mailbox and the terminal": a letter opens the
shed, a command lights a lamp, a badge puts a ribbon on the bucket, all optional. -
Tools and CI (4.1.9):
npm run fuzz:connectors(seeded mutations of each connector's corpus, crashes and memory
counted); aconnectorsCI job (the contract on the four connectors against a real Bridge, abuse and replay
tests, a run under--disallow-code-generation-from-strings, half a minute of fuzzing, the tarball installed); a
nightly fuzz of a minute per connector, not gating yet; aconnectorsmutation set, outsideall, not gated yet;
e2e:realitysends one key three times from two connectors and proposes the sample chapter's replays. The Windows
job leaves out the Telnet and SSH tests until they are ported. -
Documentation (4.1.9):
docs/en/CONNECTORS.mdanddocs/en/PRIVACY.md(what is kept, where, how long, how to
delete), in French too; a threat model per connector (docs/dev/threat-models/); ADR 0008; D19.
Manual passes (D12: reported, not blocking)
0 of 12 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| A real email provider (webhook mode) delivering to a game | not done | |||
| A real IMAP mailbox polled by the email connector | not done | |||
| A real Open Badge (OB2 hosted, OB3 VC-JWT) from a real issuer verified | not done | |||
| SSH and Telnet exposed in a controlled environment, attacked by a person | not done | |||
| A Bridge behind HTTPS with a real connector, a signal delivered after a disconnection | not done | |||
Screen reader (docs/dev/SCREEN-READER.md) |
not done | |||
Safari offline (docs/dev/SAFARI-OFFLINE.md), iPhone install and update |
not done | |||
| Firefox offline on a real machine | not done | |||
Windows: npm run doctor, npm run dev, npm run build by hand |
not done | |||
| Real phone, frame rate of the heaviest scene (≥ 30 FPS) | not done | |||
Playtesters who do not know the puzzles (npm run verify:field) |
not done | |||
Recorded voices; listening; a signed tag (git tag -s) |
not done |