Skip to content

Releases: wanoo/web-scumm

v4.1.15-rc.1

v4.1.15-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Oct 00:51
35b4143

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.yml runs from main while 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 = WAL ran outside the store's busy wait, so of several serve processes 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 own timeout too), and past its wait answers StoreBusyError (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 the WorldVariant the game was played in. parseSave reads 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 with SaveWorldMismatch, which names the world to rebuild. A host that wrote or checked
    schema: 3 itself sees schema: 4 and a variant field (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.1 release job timed
    out on apt-get install ffmpeg on 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 from main
    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, initialJsKB 140, holds; a game without reality still 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:assets is part of build: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 check npm run e2e:remix is written, not yet run); the world is plain data (reserved flags remix.* 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:variants validates 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 in verify:game, and a release publishes the bundled games' reports. npm run remix -- --seed|--preview shows a world. Measured: demo 3 worlds, reference 24 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 minigame code-wheel (parody by default), seeded by the world,
    accessible by buttons, keys, gamepad and a text list, and npm run code-wheel prints 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.random is forbidden in src/engine/core and
    src/engine/minigames by the linter; the minigames draw from the run's minigame:<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; newSeed throws 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

Choose a tag to compare

@github-actions github-actions released this 07 Oct 23:50

"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.random draws from the run's seeded stream (4.1.14). By default the engine no longer calls Math.random:
    it draws from the logic stream of the run's seed (xoshiro128**, core/prng.ts). A host or a test that sets
    engine.random is unchanged; a session whose host chose the seed (Engine.sessions.nextSeed) writes Session.seed.

Changes

  • CI's install steps get 20 minutes instead of 8 (4.1.14): npm ci, apt-get install ffmpeg and playwright install on
    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 own timeout-minutes.

  • Speedrun categories as content (4.1.14, docs/en/SPEEDRUN.md). GameDef.speedrun declares 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 validate checks 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.runClock reads RTA (monotonicNow, never an authority), logical
    steps (one per input) and logical time (microticks: the declared durations of core/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's web-scumm speedrun verify and the MCP tool speedrun_verify replay 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). .wsroute routes (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:overlay serves an OBS Browser Source (full, compact,
    transparent); npm run speedrun:livesplit exports 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 uses Math.sqrt, not Math.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 a valid run; SPEEDRUN, ADR 0016 and ADR 0017 say what replay-valid admits (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 reality mutation set is gated again (4.1.10): the 20 survivors the v4.1.10-rc.1 run left
    unnamed (18 in bridge.ts, 2 in lock.ts): 19 killed by tests (tests/bridge-mutants-tenant.test.ts: the slow
    pass's timer and its clearInterval, a pairing's origin and session id, a code confirmed or claimed twice, a claim
    racing a confirmation, the backlog and propose.ms values 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

v4.1.14-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Oct 22:50

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.random draws from the run's seeded stream (4.1.14). By default the engine no longer calls Math.random:
    it draws from the logic stream of the run's seed (xoshiro128**, core/prng.ts). A host or a test that sets
    engine.random is unchanged; a session whose host chose the seed (Engine.sessions.nextSeed) writes Session.seed.

Changes

  • CI's install steps get 20 minutes instead of 8 (4.1.14): npm ci, apt-get install ffmpeg and playwright install on
    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 own timeout-minutes.

  • Speedrun categories as content (4.1.14, docs/en/SPEEDRUN.md). GameDef.speedrun declares 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 validate checks 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.runClock reads RTA (monotonicNow, never an authority), logical
    steps (one per input) and logical time (microticks: the declared durations of core/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's web-scumm speedrun verify and the MCP tool speedrun_verify replay 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). .wsroute routes (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:overlay serves an OBS Browser Source (full, compact,
    transparent); npm run speedrun:livesplit exports 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 uses Math.sqrt, not Math.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 a valid run; SPEEDRUN, ADR 0016 and ADR 0017 say what replay-valid admits (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 reality mutation set is gated again (4.1.10): the 20 survivors the v4.1.10-rc.1 run left
    unnamed (18 in bridge.ts, 2 in lock.ts): 19 killed by tests (tests/bridge-mutants-tenant.test.ts: the slow
    pass's timer and its clearInterval, a pairing's origin and session id, a code confirmed or claimed twice, a claim
    racing a confirmation, the backlog and propose.ms values 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

Choose a tag to compare

@github-actions github-actions released this 07 Oct 22:51

"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=objects keeps 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; --resume takes 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 as truncated
    past a heap size. A budget that cuts a search never gives proved.

  • The proof profile says what multiplies the states (4.1.13). npm run solve -- --prove --profile attributes 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 replay plays 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). --symmetry folds 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.ts branches 96. The strict ratchet on the
    v4.1.10-rc.1 tag 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:reality waits 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.ts branches 90, bridge/src/server.ts lines 94 and branches 85, bridge/src/cli.ts branches 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:reality acts 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

Choose a tag to compare

@github-actions github-actions released this 07 Oct 19:47
943cfdc

"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 adds ui.fingerprint (and ui.objectives when it declares objectives) to its
    ui and 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
    says objectiveCompleted once, 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 whose done can never hold (naming a custom command without
    declared effects), an unknown parent and a cycle of parents, and warns about a done the 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). compileIR turns 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 the file:line that writes each id. npm run ir -- --game <id> [--json] prints it; the Studio's new Language tab shows it with the objectives; MCP's new get_ir returns
    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) and engine (its version). A rule changed moves logic only, a
    decor presentation only, a custom command's code trustedExtensions only. The pause menu shows the short form
    (ui.fingerprint, "Build" by default); a build writes site.json with the trusted extensions' hash and the
    engine's version, which npm run verify:dist expects.

  • 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:canonical compares 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's set_value writes them in the game file
    with id: "@game"; list_rooms shows them.

  • The DSL's reference is generated (4.1.12, D22). docs/en/DSL.md and docs/fr/DSL.md list 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.md says 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, GameIR and the IR's types. web-scumm/testing: fingerprint,
    fingerprintGame, presentationOf, hashSources, sha256Hex, shortFingerprint, GameFingerprint. MCP: get_ir.
    Engine.objectives, App.fingerprint(), App.objectivesMenu(), BootOptions.build and two ui keys
    (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 (initialJsKB 140) 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.ts goes from 1 140 to 1 113 lines, and its cap with it.

  • e2e:pwa on 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.yml runs 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-check without 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 (and chain --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 tag fetches before reading the commit (4.1.10): chain tagged 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

Choose a tag to compare

@github-actions github-actions released this 07 Oct 20:05

"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.journal numbers 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; null when 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 replay prints it and exits 1 when the replay's differs; a session longer than the journal's window
    (10 000 events) is exported with journalTruncated: true and no journal, and npm run replay then 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). App composes the engine, a Presenter (dom/presenter.ts:
    the scene's calls, lines, overlays, minigames, the ending) and the room view's Renderer; a verb on a target, a
    walk, a choice, a skip or a screen opened goes through the presenter's intent(). 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) is core/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 called window.__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.ts zoneRoute), 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 fails npm 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.journal and Engine.sessionSeq are new
    members of Engine.
  • 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 (initialJsKB 140) 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

v4.1.11-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Oct 18:42

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.journal numbers 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; null when 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 replay prints it and exits 1 when the replay's differs; a session longer than the journal's window
    (10 000 events) is exported with journalTruncated: true and no journal, and npm run replay then 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). App composes the engine, a Presenter (dom/presenter.ts:
    the scene's calls, lines, overlays, minigames, the ending) and the room view's Renderer; a verb on a target, a
    walk, a choice, a skip or a screen opened goes through the presenter's intent(). 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) is core/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 called window.__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.ts zoneRoute), 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 fails npm 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.journal and Engine.sessionSeq are new
    members of Engine.
  • 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 (initialJsKB 140) 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

Choose a tag to compare

@github-actions github-actions released this 07 Oct 22:54

"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). WorldSignalV2 adds tenantId,
    environment, audience (the origin the player paired from), sessionId and keyId to the signed payload;
    verifySignal accepts the versions its expectation allows (both by default), refuses a V2 signed for another
    tenant, environment, origin or link (audience-mismatch) and a keyId that is not the header's kid (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;
    RealityClient checks the page's origin by default and the shipped player passes the link's sessionId. 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, streamAlive and playerOf return promises; the
    routes /v1/* are unchanged. --trust-proxy alone trusts the loopback only; the client is the rightmost
    X-Forwarded-For address 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 through RealityStore, every method taking the tenant first; appendSignal decides the
    deduplication, the sequence (MAX + 1), the quotas and the signature in one transaction. Stores: SQLite through
    node:sqlite (the local profile; Node 22.13+, no native dependency), Postgres through pg (the distributed
    profile, experimental until a real deployment), the 4.1.9 journal (still served; npm run bridge -- migrate --from=jsonl --to=sqlite moves it). The schema is versioned (bridge/migrations/, up and down). One server serves
    several tenants (serve --tenants=…, routed by Host), 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 by NOTIFY or a short poll). New: /livez, /readyz, /healthz; OpenTelemetry
    metrics when @opentelemetry/api is installed; tenant export|delete, backup, restore; quarantine of rows that
    no longer verify (or name another player, sequence or tenant than their row), listed by doctor;
    streamsPerInstance; a store busy beyond 5 s answers 503 with Retry-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:load measures three instances, 1 000 players
    and 50 000 proposals (docs/dev/BENCH-BRIDGE.md; nightly on SQLite and Postgres); CI runs a bridge-postgres job.

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

v4.1.10-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Oct 20:15

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). WorldSignalV2 adds tenantId,
    environment, audience (the origin the player paired from), sessionId and keyId to the signed payload;
    verifySignal accepts the versions its expectation allows (both by default), refuses a V2 signed for another
    tenant, environment, origin or link (audience-mismatch) and a keyId that is not the header's kid (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;
    RealityClient checks the page's origin by default and the shipped player passes the link's sessionId. 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, streamAlive and playerOf return promises; the
    routes /v1/* are unchanged. --trust-proxy alone trusts the loopback only; the client is the rightmost
    X-Forwarded-For address 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 through RealityStore, every method taking the tenant first; appendSignal decides the
    deduplication, the sequence (MAX + 1), the quotas and the signature in one transaction. Stores: SQLite through
    node:sqlite (the local profile; Node 22.13+, no native dependency), Postgres through pg (the distributed
    profile, experimental until a real deployment), the 4.1.9 journal (still served; npm run bridge -- migrate --from=jsonl --to=sqlite moves it). The schema is versioned (bridge/migrations/, up and down). One server serves
    several tenants (serve --tenants=…, routed by Host), 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 by NOTIFY or a short poll). New: /livez, /readyz, /healthz; OpenTelemetry
    metrics when @opentelemetry/api is installed; tenant export|delete, backup, restore; quarantine of rows that
    no longer verify (or name another player, sequence or tenant than their row), listed by doctor;
    streamsPerInstance; a store busy beyond 5 s answers 503 with Retry-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:load measures three instances, 1 000 players
    and 50 000 proposals (docs/dev/BENCH-BRIDGE.md; nightly on SQLite and Postgres); CI runs a bridge-postgres job.

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

Choose a tag to compare

@github-actions github-actions released this 07 Oct 11:48
59ddfab

"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 -- verify on a release candidate (4.1.9). It looked for web-scumm-4.1.8-rc.1.tgz where the
    packages carry package.json's version (web-scumm-4.1.8.tgz): the rc's sums and eight attestations verified, then
    the command failed on that name, in release.yml too. 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 writes changes/<slug>.md (its bullets under ### Breaking, ### Fixed or ### Changes) and, for a LOG
    entry, changes/<slug>.log.md; npm run changes -- --assemble folds them into CHANGELOG.md's Unreleased and
    numbers the LOG entries in the order the fragments reached main; CI's check job 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:game instead of build, the
    baseline, the proof; coverage: the unit suite once), then only the heavier jobs its diff can affect, as
    tools/ci-plan.ts classifies 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 the dist/ that check built instead of building it seven times; node-24 runs the suite
    without quality again; audit:deps runs 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 on main, tags, the nightly and release-check. On main, on a tag and with the
    full-ci label, everything runs as before; a new pr-gate job sums every result up, the candidate single required
    check (CONTRIBUTING.md, "What CI runs"; docs/en/SUPPORT.md says what a pull request no longer checks). The mutation job no longer runs on a push to main either: 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-check measure the sets, a full-ci label 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>, or npm 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 per Message-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, no exec, no sftp, 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 Integrity eddsa-jcs-2022), issuer,
    recipient, dates and revocation checked, every document fetched under an SSRF-safe network policy; the verdict is
    valid, invalid, expired, revoked or indeterminate. 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.connectors holds 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 validate checks 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 /health in 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-features and nan map to a refusing stub): in the
    repository npm ci still runs ssh2's install script, which attempts a native build and fails without the nan
    headers, so no .node file results (tested); the package is installed with --ignore-scripts. npm run pack makes four tarballs; npm run fresh-install installs this one without native
    code and runs web-scumm-connector --help.

  • A build that carries server code fails (4.1.9): verify:dist (in npm 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:reality proves 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 game games/signals gains "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); a connectors CI 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; a connectors mutation set, outside all, not gated yet;
    e2e:reality sends 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.md and docs/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