Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2faa46b
[feat] 27-B: the diagnostics ring, global error capture and a copyabl…
AlexZ005 Sep 12, 2026
2a63b9f
[feat] 27-F: the signaling link never gives up, and a closed peer is …
AlexZ005 Sep 12, 2026
35b0038
[fix] 27-C: the runtime loops survive a throw instead of ending the s…
AlexZ005 Sep 12, 2026
dd15947
[fix] 27-A: one bad message can no longer kill a connection
AlexZ005 Sep 12, 2026
34907d4
[feat] 27-I: CI on pull requests, and one place where the baseline lives
AlexZ005 Sep 12, 2026
0dc8b1e
[feat] 27-E approval timeouts and session size
AlexZ005 Sep 12, 2026
7a7f447
[test] 27-I: the mesh regression suite the rig has always pointed at
AlexZ005 Sep 12, 2026
8f0b1f6
[fix] 27-D: a runaway script node no longer freezes every peer
AlexZ005 Sep 12, 2026
032704a
[fix] 27-G: GPU memory comes back, and a lost context says so
AlexZ005 Sep 12, 2026
c3cb8cd
[chore] integration A: the recovery story reaches the bundle, and CI …
AlexZ005 Sep 12, 2026
87c9d72
[test] signaling-reconnect: the retry checks stop racing a peer rebuild
AlexZ005 Sep 12, 2026
b7f5ec1
[fix] 27-H: an IndexedDB operation always settles
AlexZ005 Sep 12, 2026
b5898ec
[fix] 27-H: autosave stops stuttering, stops re-entering itself, and …
AlexZ005 Sep 12, 2026
f3d2dd0
[feat] 27-H: one place that writes a preference, and a gate that keep…
AlexZ005 Sep 12, 2026
f731555
[fix] 27-H: the microphone is given back
AlexZ005 Sep 12, 2026
7646fc2
[fix] 27-H: the dirty flag is read before the export, not beside the …
AlexZ005 Sep 12, 2026
526624f
[feat] 26-B: one poke per frame, an ingest queue that yields, and a w…
AlexZ005 Sep 16, 2026
4f85a81
[feat] 26-A: a scene budget with tiers, a meter in the status line, a…
AlexZ005 Sep 16, 2026
9be2f00
[feat] 26-C: an oversized scene asks before it arrives, and a big fil…
AlexZ005 Sep 16, 2026
d322e7a
[feat] 26-G: a simulation that cannot keep up stops once, and a froze…
AlexZ005 Sep 16, 2026
ec9da2e
[fix] 26-G: a slow machine drawing a light scene is not an overloaded…
AlexZ005 Sep 16, 2026
30ae2b1
Merge pull request #206 from theprototype-app/feat/27-hardening
AlexZ005 Sep 17, 2026
a7c10bb
Merge pull request #208 from theprototype-app/feat/27-storage
AlexZ005 Sep 17, 2026
37b4ecd
[feat] 26-E: a scene-stress rig, and the meter finally counts a whole…
AlexZ005 Sep 17, 2026
898738a
[feat] 25-E: one clock for the session, and the stamps that read it
AlexZ005 Sep 17, 2026
d9a15ed
[feat] 25-G: the mesh regression runs on four peers and its own signa…
AlexZ005 Sep 17, 2026
961c77d
[feat] 25-F: a refused join is told, and a full session says so
AlexZ005 Sep 17, 2026
cf1c200
[feat] 26-D: a heavy scene gives up shadows before it gives up frames…
AlexZ005 Sep 17, 2026
02a5dc1
Merge pull request #209 from theprototype-app/feat/26-overload
AlexZ005 Sep 17, 2026
6a3b6da
Merge pull request #210 from theprototype-app/feat/25-late
AlexZ005 Sep 17, 2026
d9b6810
Merge pull request #211 from theprototype-app/feat/26-measure
AlexZ005 Sep 17, 2026
c8258dd
[docs] 1.12.0: the hardening changelog, and two release facts that ha…
AlexZ005 Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 27-I (hardening audit H5) β€” CI ON PULL REQUESTS.
#
# Until now the only workflow was release.yml, which runs on a v* TAG. So CONTRIBUTING's
# rule ("npm run build should pass and npm run check should not add new errors") was
# enforced by whoever remembered to run it, and a broken main was discovered at release
# time. Everything below is what a maintainer already does by hand.
#
# WHY e2e-smoke IS NOT REQUIRED YET: it renders WebGL through SwiftShader on a GPU-less
# runner, and this project's own notes measure ~4.5 fps there plus a documented family of
# timing-sensitive suites. Blocking every PR on that before it has been observed green on
# GitHub would train people to ignore a red tick, which is worse than not having it. It
# reports, it does not gate, and the comment below says when to flip it.
#
# Two-peer suites stay a MANUAL gate: they meet on the self-hosted signaling box, which a
# public runner cannot reach and should not be pointed at.
name: ci
on:
pull_request:
push:
branches: [main, release/next]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build

check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
# ONE source of truth for the baseline: check-baseline.json, read by the same
# script release.yml uses. The number used to live in a shell block here and went
# stale by three.
- name: svelte-check baseline gate
run: node scripts/check-ratchet.cjs
# 27-H (audit M4): 507 bare `localStorage` calls were routed through
# $lib/safeStorage in one pass. Without a gate that codemod decays on the next
# feature, because the file you are editing still shows you ninety-three examples
# of the old way. `setItem` throws in Safari private mode and on a full quota, and
# most of these sit inside $effects and store subscribers, where the throw kills
# the subscriber for the session.
- name: no bare localStorage
run: node scripts/check-storage.cjs

unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run test:unit

e2e-smoke:
runs-on: ubuntu-latest
# NOT a gate yet β€” see the header. Flip this to false once it has been green on a few
# PRs in a row; until then a red here is a signal to read, not a block.
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
# WebXR and getUserMedia need HTTPS, so the dev server serves TLS from certs/ β€”
# gitignored, generated per clone.
- run: npm run certs
- name: start the dev server
run: |
npm run dev -- --port 5173 --strictPort --host localhost > /tmp/dev.log 2>&1 &
for i in $(seq 1 60); do
curl -sk -o /dev/null https://localhost:5173/ && break
sleep 1
done
curl -sk -o /dev/null -w 'dev server: %{http_code}\n' https://localhost:5173/
- name: single-peer suites
env:
APP_URL: https://localhost:5173/
run: |
# 27-D/27-E/27-G added three more SINGLE-PEER suites, so they belong here.
# Deliberately NOT added: net-stress and signaling-reconnect are multi-peer and
# meet on the self-hosted signaling box, which a public runner cannot reach and
# should not be pointed at (see the header) β€” they stay a manual gate.
for s in net-backoff net-mesh wire-hardening runtime-resilience diagnostics mesh-budget script-guard dispose approval-timeout; do
echo "::group::$s"
npm run e2e -- "$s" || echo "SUITE FAILED: $s"
echo "::endgroup::"
done
- name: dev server log on failure
if: failure()
run: tail -40 /tmp/dev.log
22 changes: 5 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,12 @@ jobs:
# -> 384/47 with the controls/dock rework: warnings 62 -> 47 when the six
# hand-written pill cells became one {#each} template, errors -1 net from the
# applyLook fold in PointerLockControls)
# 27-I: ONE source of truth. The counts used to be hardcoded in this block and went
# stale (362 here while the tree measured 359); check-baseline.json is the floor now
# and ci.yml's gate runs the very same script.
- name: svelte-check baseline gate
run: |
npm run check 2>&1 | tee check.log || true
# svelte-check prints "N ERRORS N WARNINGS" (machine) or
# "found N errors and N warnings" (human) depending on the TTY β€” parse both
LINE=$(grep -Eo '[0-9]+ ERRORS [0-9]+ WARNINGS' check.log | tail -1)
ERRORS=$(echo "$LINE" | awk '{print $1}')
WARNINGS=$(echo "$LINE" | awk '{print $3}')
if [ -z "$ERRORS" ]; then
LINE=$(grep -Eo 'found [0-9]+ errors and [0-9]+ warnings' check.log | tail -1)
ERRORS=$(echo "$LINE" | awk '{print $2}')
WARNINGS=$(echo "$LINE" | awk '{print $5}')
fi
echo "svelte-check: $ERRORS errors / $WARNINGS warnings (baseline 362/47)"
if [ -z "$ERRORS" ]; then echo "could not parse svelte-check output"; exit 1; fi
if [ "$ERRORS" -gt 362 ] || [ "$WARNINGS" -gt 47 ]; then
echo "baseline exceeded"; exit 1
fi
run: node scripts/check-ratchet.cjs

- name: zip the build
run: cd build && zip -r "../theprototype-${GITHUB_REF_NAME}.zip" .
- uses: softprops/action-gh-release@v2
Expand Down
100 changes: 99 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,105 @@
per release, newest first. HTML comments like this one are stripped before
rendering, so maintainer notes stay out of the user-facing window. -->

## 1.12.0 β€” Hold together πŸ›‘οΈ

### πŸ›‘οΈ Connections that recover, and sessions with a size (roadmap #27 + #25)

- ⏱️ **A connection request now ends.** The pill counts down while you wait and the
host's card shows how long someone has been waiting. After 90 seconds the request
cancels itself and offers **Try again**, instead of sitting on *Requesting* for
ever. Dialling someone who is not online ends the request too, rather than leaving
it up beside a toast saying they are unreachable.
- πŸ‘₯ **A session has a size.** Settings β–Έ Connection β–Έ **Session size** says how many
people you expect. Past it an approval still works but warns you, and at 16 the
approve buttons say the session is full β€” everyone connects to everyone, so one
more person costs every other person bandwidth. Waiting requests are capped, and
expired cards are dropped before live ones.
- πŸ”Œ **The signaling link stops giving up.** Reconnection retries with a jittered
backoff and no attempt limit, a closed peer is rebuilt rather than abandoned, and
coming back online or returning to the tab retries immediately. The Connect pill
shows a chip while it is retrying, so a dead link no longer looks like a dead app.
- 🧱 **One bad message can no longer kill a connection.** Everything arriving from a
peer is shape-checked before it reaches the code that applies it, and anything
malformed is counted and dropped instead of throwing. A peer sending repeated
rubbish is reported once, not once per message.
- πŸ” **The editor survives a bad frame.** A throw inside the flow runtime or the
physics step no longer ends the session: the frame is skipped, the failure is rate
limited so one broken node cannot flood you, and the runtime can be resumed.
- 🩺 **Diagnostics you can copy.** Settings β–Έ About β–Έ **Copy diagnostics** puts a
bundle on the clipboard β€” recent log entries, the last uncaught error and session
details β€” so a problem can be reported with something in it.
- πŸ” **A runaway script no longer takes the room with it.** Script nodes run on every
peer, every frame, so a `while (true)` in one node used to freeze everybody's tab,
not just its author's. Every loop a script contains is now counted, and one that
runs away stops with a *Script loop limit* badge on the node while the scene keeps
running. A node that is merely slow β€” rather than infinite β€” is paused after it has
spent too long in too many frames in a row, and editing its code starts it again.
- 🧯 **Safe mode.** Adding `#safe` to the app's address opens a scene with the flow
runtime paused, so a scene whose scripts misbehave on load can still be opened,
repaired and resumed. A restore that never completed a frame is also remembered: the
next start offers the prompt with a warning instead of silently loading it again.
- 🧹 **Deleting gives the memory back.** Removing an object used to drop it from the
scene and leave its geometry, materials and textures sitting on the graphics card
until the page was closed, so a session that imported and deleted the same model ten
times paid for ten copies. Deleting, clearing a scene and replacing an object now free
what only that object was using β€” and never what something else still draws with,
which matters because duplicates, clones and a material shared across a selection all
point at the same resources.
- πŸ–₯️ **A lost graphics context now says so.** When the browser takes the 3D context
away β€” a driver update, a graphics reset, a phone under memory pressure β€” the viewport
used to freeze silently while the rest of the app carried on answering, which reads as
the whole thing having crashed. You get a panel explaining what happened, a button to
save the scene (which is still intact, because it lives in the page rather than on the
graphics card), and the view restores itself when the browser hands the context back.

### πŸ’Ύ Storage that keeps your work (roadmap #27, wave 2)

- πŸ’Ύ **Saving can no longer hang forever.** A database operation that is aborted or
stops answering now fails and says so, instead of leaving the app waiting on it.
- πŸͺΆ **Autosave stopped stuttering on big scenes.** It measures what saving actually
costs and spaces itself out accordingly, it can no longer start a second save on top
of the one already running, and when it cannot save at all it tells you rather than
going quiet.
- πŸ•ΆοΈ **Settings keep working in Safari private mode and on a full disk.** Every
preference now goes through one place that falls back to memory for the keys it
cannot write, so a browser that refuses storage costs you the setting, not the app.
- πŸŽ™οΈ **The microphone is given back.** Turning voice off, leaving voice mode or leaving
a session now releases the device, so your operating system stops showing the
recording indicator. Push-to-talk holds the device for a few seconds between presses
on purpose β€” reacquiring it costs a renegotiation with every peer.

### 🚦 A scene that will not freeze you out (roadmap #26)

- 🚦 **A big scene stays responsive while it arrives.** Objects are created in slices
rather than all at once, the viewport is refreshed once a frame instead of once per
object, and an object list of thousands of rows draws only the rows you can see.
- πŸ“Š **A Statistics panel, and a budget you can see.** The burger menu β–Έ **Statistics**
opens frame timings, draw calls, triangles, memory and per-message network counters.
The object count in the status line carries a coloured dot that names whatever is over
budget, and the numbers ride along in a diagnostics bundle.
- πŸ›‘ **An oversized scene asks before it arrives.** A scene big enough to hurt is held at
the door with **Load all / Load the first N / Cancel** instead of arriving and
wedging the tab, and opening an oversized file warns you first.
- ⏸️ **A window that cannot keep up pauses instead of freezing.** A simulation that
falls too far behind stops once, with a Resume button, and a viewport that has stopped
drawing offers **Save now**, **Reduce** and **Resume** rather than appearing to have
crashed.

### ⏱️ One clock, and a joiner that is told (roadmap #25 + #26, wave 3)

- ⏱️ **Everyone in a session now shares one clock.** Peers used to stamp events with
their own machine's time, so anything comparing ages or ordering across peers was
wrong by the difference between two computers. The session now runs on the host's
clock, and a machine whose clock is minutes out is corrected on its first exchange.
- πŸšͺ **A refused join is told it was refused.** Being declined, and arriving at a session
that is already full, now say so plainly instead of leaving you watching a request
that never resolves.
- 🎚️ **A heavy scene gives up shadows before it gives up frames.** When a scene is too
much for the machine, quality is reduced automatically and reversibly β€” starting with
the passes that cost the most and show the least β€” so the room keeps moving. The
budgets it steers by are measured on real scenes rather than estimated.

## 1.11.0 β€” Muscle memory ⌨️

### ⌨️ Input parity (roadmap #24, batch A)
Expand Down Expand Up @@ -359,7 +458,6 @@ select more than one thing β€” including on a phone.
- 🎨 Modules can add their own post-processing effects and shader compilers.
- 🧹 Settings descriptions read as sentences again instead of one word per line.


## 1.5.0 β€” Move it, properly 🎬

Animation you can trust: movements play from where the object actually is, pause
Expand Down
24 changes: 21 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,31 @@ git push origin main --follow-tags

After that: `npm version minor` (features) or `npm version patch` (fixes), then the
same push. The tag triggers `.github/workflows/release.yml`, which builds, gates on
the svelte-check baseline (the error/warning counts live in the workflow β€” update
them when the baseline moves), zips `build/`, and publishes a GitHub Release with
generated notes.
the svelte-check baseline (27-I moved the error/warning counts OUT of the workflow
and into `check-baseline.json` at the repo root, read only by
`scripts/check-ratchet.cjs`, which `release.yml` and `ci.yml` both call β€” ratchet it
DOWN with `node scripts/check-ratchet.cjs --update` whenever a change legitimately
removes errors, and never hardcode the number anywhere again), zips `build/`, and
publishes a GitHub Release with generated notes.

MAJOR = a breaking file-format or wire-protocol change (`SESSION_FORMAT` /
`MODULE_FORMAT` bumps, incompatible peer messages).

## Recreate `release/next` after the tag push

The repo has `delete_branch_on_merge: true`, so merging the release PR DELETES
`release/next` on origin β€” and GitHub then silently retargets every open PR that was
based on it to `main`, which would let an ungated batch land straight on main. This
bit the 1.11.0 release (PR #206 was retargeted). After pushing the tag:

```sh
git push origin main:release/next # recreate it at the released commit
gh pr list --base main # anything retargeted goes back to release/next
```

Recreating it from main also keeps `release/next:package.json` in step with the
released version instead of drifting (it carried a stale 1.8.0 before 1.11.0).

## After tagging

- Update `CHANGELOG.md` (the in-app What's new window renders it).
Expand Down
6 changes: 6 additions & 0 deletions check-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"comment": "27-I: the svelte-check floor, read ONLY by scripts/check-ratchet.cjs. It used to be hardcoded in release.yml's shell block, where it went stale (362 while the tree measured 359). Ratchet it DOWN whenever a change legitimately removes errors - that is the project convention, and --update does it in one command.",
"errors": 341,
"warnings": 47,
"measured": "2026-09-17"
}
Loading
Loading