diff --git a/.github/workflows/battery.yml b/.github/workflows/battery.yml index 36309e93b..0ef4517c0 100644 --- a/.github/workflows/battery.yml +++ b/.github/workflows/battery.yml @@ -49,25 +49,25 @@ jobs: E2E_NO_PERF_OFFER: '1' MODE: quick steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: path: core - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: repository: ${{ fromJSON(inputs.refs).scenes.repo }} ref: ${{ fromJSON(inputs.refs).scenes.ref }} path: scenes - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: repository: ${{ fromJSON(inputs.refs).modules.repo }} ref: ${{ fromJSON(inputs.refs).modules.ref }} path: modules - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: repository: ${{ fromJSON(inputs.refs).packs.repo }} ref: ${{ fromJSON(inputs.refs).packs.ref }} path: packs - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7 with: node-version-file: core/.nvmrc cache: npm @@ -81,7 +81,7 @@ jobs: # the module zips the game suites install (MODULES_REPO) (cd modules && npm ci --no-audit --no-fund && node scripts/pack.cjs --all --force) # confirm: what the required shards left red (or unrun), from their artifacts - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 if: inputs.set == 'confirm' with: pattern: battery-required-* @@ -99,7 +99,7 @@ jobs: POOL_TPSCENE=$S/examples/pool-party/scene.tpscene node scripts/ci/battery.cjs run --set ${{ inputs.set }} ${{ inputs.set == 'confirm' && '--from "$W/prev"' || '' }} --shard ${{ matrix.shard }} --of ${{ matrix.of }} --serve --port 5173 \ --deadline ${{ inputs.deadline }} --out "$W/battery-out" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: always() with: name: battery-${{ inputs.set }}-${{ matrix.shard }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f1c18e6e..523279c24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,11 +5,11 @@ # 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. +# e2e-smoke GATES since test-health-1 (feat/60). It renders WebGL through SwiftShader on a +# GPU-less runner (~4.5 fps), so it was report-only until it had been seen green on GitHub: +# the runner step now really fails (it used to `|| echo` every red away), a failed suite runs +# once more (green on the retry = flaky, reported), and a known flake is entered in +# tests/e2e/quarantine.json with an owner and a review date instead of blocking every PR. # # 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. @@ -33,8 +33,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm @@ -57,8 +57,8 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm @@ -92,8 +92,8 @@ jobs: scene-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm @@ -103,12 +103,12 @@ jobs: run: | echo "scenes=$(node -p "require('./perf/budgets.json').scenes.ref")" >> "$GITHUB_OUTPUT" echo "scenes_repo=$(node -p "require('./perf/budgets.json').scenes.repo")" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: repository: ${{ steps.refs.outputs.scenes_repo }} ref: ${{ steps.refs.outputs.scenes }} path: .perf-deps/scenes - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: repository: theprototype-app/packs path: .perf-deps/packs @@ -123,7 +123,7 @@ jobs: supply-chain: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: SBOM (syft, SPDX JSON) uses: anchore/sbom-action@v0.24.3 with: @@ -145,8 +145,8 @@ jobs: unit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm @@ -156,12 +156,10 @@ jobs: e2e-smoke: if: ${{ !inputs.battery }} 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 + # a gate — see the header (green on GitHub for bc453ae8 and e77ca3f7 before the flip) steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm @@ -186,14 +184,20 @@ jobs: # 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 + # test-health-1: through the battery runner, so a red is a red (the old loop's + # `|| echo SUITE FAILED` could never fail the step), a failed suite runs once more + # (green on the retry = flaky, reported) and tests/e2e/quarantine.json applies. + node scripts/ci/battery.cjs run --only net-backoff,net-mesh,wire-hardening,runtime-resilience,diagnostics,mesh-budget,script-guard,dispose,approval-timeout --port 5173 --out /tmp/smoke - name: dev server log on failure if: failure() run: tail -40 /tmp/dev.log + - name: suite logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-smoke-logs + path: /tmp/smoke/logs + if-no-files-found: ignore # 53-reader (A4): THE ACCESSIBILITY GATE. axe-core over the main surfaces (editor shell, object # list, Inspector, Settings, Asset Browser on recorded fixtures, phone shell) FAILS on serious and @@ -204,8 +208,8 @@ jobs: if: ${{ !inputs.battery }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm @@ -229,7 +233,7 @@ jobs: npm run e2e -- a11y-axe a11y-reader || rc=$? [ -f /tmp/axe/axe-after.md ] && cat /tmp/axe/axe-after.md >> "$GITHUB_STEP_SUMMARY" exit $rc - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: always() with: name: axe-report @@ -237,6 +241,13 @@ jobs: - name: dev server log on failure if: failure() run: tail -40 /tmp/dev.log + - name: suite logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-smoke-logs + path: /tmp/smoke/logs + if-no-files-found: ignore # 38 (NOTES-38 #18): THE TOKEN SCREENSHOT DIFF. /kit renders every redesign primitive in every # state; this job shoots it in the dark, light and custom themes on the PR's BASE ref and on its @@ -248,10 +259,10 @@ jobs: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm @@ -284,7 +295,7 @@ jobs: node tests/e2e/tools/kit-diff.cjs diff /tmp/kit/base /tmp/kit/head /tmp/kit/diff; rc=$? cat /tmp/kit/diff/report.md >> "$GITHUB_STEP_SUMMARY" exit $rc - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: always() && steps.base.outputs.has_kit == '1' with: name: kit-diff diff --git a/.github/workflows/deps.yml b/.github/workflows/deps.yml index 4ea9ad455..eda2763f2 100644 --- a/.github/workflows/deps.yml +++ b/.github/workflows/deps.yml @@ -5,8 +5,11 @@ # svelte-check ratchet, vitest, scene-lint, kit-diff, a11y, supply chain) and perf-budget.yml, both # called as reusable workflows, then the integrators' e2e battery split into shards on # GitHub-hosted runners (SwiftShader: no GPU, battery.yml): the REQUIRED set gates; the OPTIONAL set (red on -# SwiftShader with unchanged code, each with its reason in tests/e2e/battery.json) runs after it and is only -# reported. `deps-gate` is the one required check; the ruleset on +# SwiftShader with unchanged code, each with its reason in tests/e2e/battery.json) runs only when started by hand +# (workflow_dispatch) and is only reported. Perf budget + battery run only for a non-major change to the root +# app's npm packages (deps-policy.cjs `battery`): Actions, the agent and majors get ci.yml alone, so one monthly +# Dependabot round does not hold the account's 20 concurrent jobs for half a day. `deps-gate` is the one +# required check; the ruleset on # `deps` also requires the `pc-battery` commit status, which the GPU check on a desktop posts for a # PR that changes what the app ships (scripts/ci/pc-battery.sh — pulled from there, no runner of ours # in this public repo) and this workflow posts as "not needed" for every other PR. @@ -47,15 +50,16 @@ jobs: cls: ${{ steps.policy.outputs.class }} needs_pc: ${{ steps.policy.outputs.needs_pc }} reason: ${{ steps.policy.outputs.reason }} + battery: ${{ steps.policy.outputs.battery }} gate_ci: ${{ github.event_name != 'pull_request' || github.base_ref == 'deps' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: dependabot metadata id: meta if: github.event.pull_request.user.login == 'dependabot[bot]' - uses: dependabot/fetch-metadata@v2 + uses: dependabot/fetch-metadata@v3 - name: policy id: policy env: @@ -68,7 +72,7 @@ jobs: # a person's PR (or a dispatch): no automation; the PC check runs when the app's lockfile moved pc=false if [ -n "$BASE" ] && git diff --name-only "origin/$BASE...HEAD" | grep -qxE 'package(-lock)?\.json'; then pc=true; fi - printf 'class=manual\nneeds_pc=%s\nreason=not a Dependabot PR\n' "$pc" | tee -a "$GITHUB_OUTPUT" + printf 'class=manual\nneeds_pc=%s\nbattery=true\nreason=not a Dependabot PR\n' "$pc" | tee -a "$GITHUB_OUTPUT" fi - name: majors wait for a planned migration if: steps.policy.outputs.class == 'major' @@ -90,7 +94,7 @@ jobs: # the budget gate in three parallel parts (~20 min each instead of ~60 in one) perf: needs: scope - if: needs.scope.outputs.gate_ci == 'true' + if: needs.scope.outputs.gate_ci == 'true' && needs.scope.outputs.battery == 'true' strategy: fail-fast: false matrix: @@ -102,6 +106,7 @@ jobs: plan: needs: scope + if: needs.scope.outputs.battery == 'true' runs-on: ubuntu-latest outputs: matrix: ${{ steps.plan.outputs.matrix }} @@ -110,7 +115,7 @@ jobs: playwright: ${{ steps.pw.outputs.version }} refs: ${{ steps.refs.outputs.refs }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - id: plan run: node scripts/ci/battery.cjs plan - id: pw @@ -133,7 +138,7 @@ jobs: # the deadline left unrun) gets two more tries on a FRESH runner before it counts confirm: needs: [plan, e2e] - if: always() && needs.e2e.result == 'failure' + if: ${{ !cancelled() && needs.e2e.result == 'failure' }} uses: ./.github/workflows/battery.yml with: set: confirm @@ -142,11 +147,12 @@ jobs: refs: ${{ needs.plan.outputs.refs }} deadline: 20 - # red or flaky on SwiftShader with the same red on unchanged code: run, reported, never waited for. They start - # AFTER the required shards and the confirm step (the free plan runs 20 jobs at once); deps-gate does not need them. + # red or flaky on SwiftShader with the same red on unchanged code: reported, never waited for, and only on a run + # started by hand (refreshing battery.json's classification); ~750 runner-min a time is too much for every PR. + # They start AFTER the required shards and the confirm step (the free plan runs 20 jobs at once). e2e-optional: needs: [plan, e2e, confirm] - if: always() && needs.plan.outputs.optional_shards != '0' + if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' && needs.plan.result == 'success' && needs.plan.outputs.optional_shards != '0' }} uses: ./.github/workflows/battery.yml with: set: optional @@ -157,11 +163,11 @@ jobs: optional-report: needs: e2e-optional - if: always() && needs.e2e-optional.result != 'skipped' + if: ${{ !cancelled() && needs.e2e-optional.result != 'skipped' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 with: pattern: battery-optional-* path: battery @@ -172,7 +178,7 @@ jobs: gate: name: deps-gate needs: [scope, ci, perf, plan, e2e, confirm] - if: always() && needs.scope.result == 'success' + if: ${{ !cancelled() && needs.scope.result == 'success' }} runs-on: ubuntu-latest permissions: contents: read @@ -180,28 +186,38 @@ jobs: issues: write statuses: write steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + if: needs.scope.outputs.battery == 'true' with: pattern: battery-required-* path: battery - - uses: actions/download-artifact@v4 - if: needs.confirm.result != 'skipped' + - uses: actions/download-artifact@v8 + if: needs.scope.outputs.battery == 'true' && needs.confirm.result != 'skipped' with: pattern: battery-confirm-* path: battery - name: battery report id: report + if: needs.scope.outputs.battery == 'true' run: node scripts/ci/battery.cjs report battery - name: the release gate env: GATE_CI: ${{ needs.scope.outputs.gate_ci }} + BATTERY: ${{ needs.scope.outputs.battery }} + CLS: ${{ needs.scope.outputs.cls }} CI: ${{ needs.ci.result }} PERF: ${{ needs.perf.result }} + E2E: ${{ needs.e2e.result }} run: | - echo "### Release gate: ci=$CI perf-budget=$PERF (class ${{ needs.scope.outputs.cls }}: ${{ needs.scope.outputs.reason }})" >> "$GITHUB_STEP_SUMMARY" - if [ "$GATE_CI" = true ] && { [ "$CI" != success ] || [ "$PERF" != success ]; }; then exit 1; fi - - uses: actions/upload-artifact@v4 + echo "### Release gate: ci=$CI perf-budget=$PERF battery=$BATTERY (class $CLS: ${{ needs.scope.outputs.reason }})" >> "$GITHUB_STEP_SUMMARY" + if [ "$CLS" = major ]; then + echo "A major is not tested here (no perf budget, no battery): its planned migration runs them. Red on purpose." >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + if [ "$BATTERY" = true ] && [ "$E2E" = skipped ]; then exit 1; fi + if [ "$GATE_CI" = true ] && { [ "$CI" != success ] || { [ "$BATTERY" = true ] && [ "$PERF" != success ]; }; }; then exit 1; fi + - uses: actions/upload-artifact@v7 with: name: battery-durations path: battery/durations.json @@ -221,9 +237,22 @@ jobs: run: | gh label create deps-ready --color 0E8A16 --description 'Green; a runtime minor or a Playwright bump waiting for the deps lane' 2>/dev/null || true gh pr edit "${{ github.event.pull_request.number }}" --add-label deps-ready + + # its own job: turning auto-merge on needs contents: write, which the gate (it checks out and runs the PR's + # code) should not hold. No checkout here; it only runs after a green deps-gate. The condition names the gate's + # result: the implicit success() also looks at the gate's own needs, and perf/plan/e2e are SKIPPED for every + # PR without the battery (Actions, the agent), so without it automerge skipped exactly where it is needed. + automerge: + needs: [scope, gate] + if: ${{ !cancelled() && needs.gate.result == 'success' && needs.scope.outputs.cls == 'auto' && github.base_ref == 'deps' }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: - name: auto-merge - if: needs.scope.outputs.cls == 'auto' && github.base_ref == 'deps' env: GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} # waits for whatever the ruleset still wants (pc-battery from the desktop for a runtime patch) run: gh pr merge "${{ github.event.pull_request.number }}" --auto --squash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d2d9f86c..fda1f295e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,8 +17,8 @@ jobs: needs: budget runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm @@ -64,7 +64,7 @@ jobs: - name: zip the build run: cd build && zip -r "../theprototype-${GITHUB_REF_NAME}.zip" . - - uses: softprops/action-gh-release@v2 + - uses: softprops/action-gh-release@v3 with: generate_release_notes: true files: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 88acb8f78..a1868bf6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,73 @@ per release, newest first. HTML comments like this one are stripped before rendering, so maintainer notes stay out of the user-facing window. --> +## 1.44.0 — Quick wins: play from where you stand, trace over a picture, share files in, embed anywhere ⚡ + +Small things you feel in every session: try the corner you are building without walking there, trace a plan, send a +file from your phone into the app, paste a scene link into Notion — and movement that stays smooth on bad Wi-Fi. + +### ▶️ Play here + +- **Play here** starts Play where your camera is — standing on whatever is under it, facing your view — and Stop puts + the camera and your selection back. Ctrl+Shift+Enter, right-click Play, the + viewport menu's Tools, an optional toolbar button (Customize toolbar), on a phone Play's long press or More, and in VR + Tools ▸ Play here. + +### 🖼️ Reference images + +- Drop an image on the viewport ▸ **Use as reference**: a see-through, unclickable picture on the ground or facing + your view to trace over. Opacity, size and height in Tools ▸ Reference images. Hidden in Play, never in exports or + published scenes; yours only unless you tick **Share with session**. + +### 📲 Share into the app + +- Install the app and it appears in your phone's **share sheet**: share a picture and it lands in your Library (or + becomes a reference image); share a .glb/.gltf and it is placed in your scene; share a .tpscene and it opens. +- **Open files from your desktop**: with the app installed, double-click a .tpscene to open it, or a .glb/.gltf to add + it to the scene you have open — a second file goes to the same window. + +### 🔗 Embed anywhere + +- Paste a public scene link (/s/…) into **Notion, WordPress, Discourse** and other oEmbed / Iframely sites and the + game plays in place. + +### 📶 Smoother together + +- Movement — avatars, VR hands, dragged, held and thrown objects, physics — rides a new fast channel between players, + so a lossy connection no longer freezes and jumps it (5 % loss: 95 % of gaps 74 ms, was 249 ms). Edits stay reliable + and in order; older peers fall back by themselves. +- 👻 People whose app crashed or lost its connection leave the Users list on their own, and leaving never shows + "Connection error". A short Wi-Fi drop heals the session instead of ending it. +- 🚪 When a guest joins your public room automatically you get a notice with **Turn off** and **Remove**, and the + Connect bar says **Public · guests join**. +- 🚩 Report a public room or a person from the room list and the Users list. + +### 💾 Kept on your device + +- Packs, skies, converted textures and CC0 downloads are kept on the device (stored once per content), so scenes + load again without the network. Settings ▸ Storage shows the use, sets a size limit and clears it; the app asks once + to keep the storage, and says why. + +### 🤖 AI assistant + +- As a viewer, the assistant tells you it can't change the scene instead of "creating" things nobody sees. +- Things it builds stay where they are when you press Play, unless you ask for physics. + +### 🔧 Fixes + +- Right-clicking the viewport opens its menu on every platform again. +- Two-peer sync: anything the host does while a just-approved joiner is still connecting now reaches that joiner as + soon as its connection opens, instead of only after the joiner's full scene download (seconds later on slow machines). +- After a network drop that only one side noticed, edits flow both ways again: the side still holding the dead link + switches to the fresh one and catches up on what it missed. +- Offers (like being made admin) keep their Accept / Decline buttons in Notifications. +- A late joiner sees a shader graph on an object that arrives after it; physics actions fire even at very low frame + rates; viewers' Explorer fills in (project, shared files and big material presets reach viewers). +- Deleting a file you never shared no longer tells the session its name. +- The README, the welcome window and the docs now say plainly how it works: sessions are peer-to-peer, and our + servers handle signaling and relay, accounts and cloud saves, assets, community pages, anonymous error reports and + counters. + ## 1.43.2 — A dependency fix 🔧 - 🔧 Updated **body-parser** (a security advisory, GHSA-27r3-jxmh-p828) in the development tools and the agent bridge. diff --git a/CLAUDE-index.md b/CLAUDE-index.md new file mode 100644 index 000000000..830b840ba --- /dev/null +++ b/CLAUDE-index.md @@ -0,0 +1,636 @@ +# CLAUDE-index.md — where things are in CLAUDE.md + +Generated by `node scripts/claude-index.cjs` (never edit; on a merge conflict, run it again). +Grep this file for a word, then read CLAUDE.md from that line (`Read CLAUDE.md offset= limit=60`). +7068 lines indexed. + + 1 # CLAUDE.md — theprototype.app + + 13 ## Architecture map + 15 - `src/stores/` — `appStore` (UI panels + `openSceneSection(label)`/`inspectorScrollTo` + 26 - `src/lib/peerHandler.svelte.js` — PeerConnection class; **all incoming messages** + 36 - Open-core seams (#13 M1 + #14 PM, contract in committed `OPEN-CORE.md`): a closed + 78 - `src/lib/triggerSync.js` (DEVX #18, the gameSync/hudSync shape — data in the leaf + 106 - `src/lib/sim/` (36-sim, 1.22): buoyancy, jiggle, fluid tanks. ONE per-frame entry + 129 - `src/lib/spawner.js` + `src/lib/transientObjects.js` (21-B B7): mid-sim body creation, + 148 - `src/lib/sharedLibrary.js` + `src/lib/transferLedger.js` + `components/editors/ + 205 - `src/lib/mountedVolumes.js` (R22 round 13, a LEAF: svelte/store + idb + toast; `sessions` + 229 - `src/lib/storageUsage.js` + `components/menu/StorageModal.svelte` (R22 round 13, a + 247 - `src/lib/explorerView.js` (R22 round 9, a LEAF — stores + arithmetic, imports NOTHING + 253 · THE MODE IS GLOBAL; COLUMNS AND SORT ARE PER VIEW. + 265 · THE SORT IS APPLIED IN `gridEntries` + 282 · R22 ROUND 11 — THE HEADER BECOMES A REAL ONE + 291 · THE SPACER CELL, and why a resizable table needs one. + 306 - R22 ROUND 36 — DELETED KEEPS ITS STRUCTURE + 395 - R22 ROUND 36 (ROOMS) — THE UNNAMED WORLD IS A ROOM WITH AN IDENTITY + 431 - `src/lib/filePreview.js` + `components/editors/FilePreviewWindow.svelte` + + 436 · THE SIBLING LIST IS PUBLISHED, NOT DERIVED. + 447 · PASSTHROUGH STANDS THE PANEL DOWN, NOT THE BODY + 464 · R22 ROUND 12 — MULTIPLE WINDOWS, AND THE 3D FACE. + 482 · R22 ROUND 13 — THE MODEL IS THE SWITCH, AND THE VIEW IS NAVIGABLE. + 499 · R22 ROUND 15 — THE PREVIEW PLAYS ANIMATIONS + 524 · "TYPING" IS NOT "FOCUS IS ON A CONTROL" + 529 · R22 ROUNDS 19-28 — THE CORNER, THE HEADER, AND THE TRANSPORTS. + 548 - `src/lib/saveAs.js` (R22 round 11) — "SAVE AS…", AND WHAT A PREFAB IS MADE OF. A prefab + 550 · A PREFAB RECORD MAY NOW CARRY A `format` AND THE FILE'S OWN `bytes` + 554 · THE BYTES RIDE BESIDE THE SNAPSHOT, NEVER INSTEAD OF IT. + 572 - WINDOW CHROME, R22 ROUNDS 20/25/26/28 + 580 - ROOMS ARE CONTENT PARTITIONS (R22 round 30, the whole arc) + 595 · THE RE-SYNC that makes the gate safe + 605 · getgame was measured INERT and removed + 608 · ROUND 32: THE ARRIVING REPLY HEALS + 622 · ROUND 32: THE ASK HOLDS BOTH DIRECTIONS + 639 · ROUND 33: THE CONNECT DECISION + 664 · ROUND 34: A SAVE NAMES THE ROOM + 674 · ROUND 35: PRIVATE SCENES + 704 · THE CONNECT CONTRACT + 728 · THE MANIFEST + 741 · THE LIBRARY'S FIRST CONTACT + 752 - `src/lib/objectPermissions.js` (#14, store-only) — viewer object permissions, ONLY + 766 - `src/lib/connectionState.js` (store-only) — `sessionHost` (peer that approved OUR + 772 - `src/components/menu/Connect.svelte` — a 3-state pill (`data-state` + 793 - `src/lib/commandsHandler.svelte.js` — receive-side scene appliers + `sendObjects` + 795 - Domain modules in `src/lib/`: `objectActions`, `geometries.svelte.js`, + 823 · The mesh is a triangle soup, but the face TOPOLOGY IS STORED NOW + 1017 · CL-A colliders v2 + 1044 · THE LOWER PEER ID KEEPS THE WORLD + 1273 · SHADER GRAPHS + 1352 · SH5 + 1372 · THE HUD + 1385 · The anchor is the 9-GRID plus PIXEL offsets, never 0..1 fractions + 1391 · **GOLDEN RULE 8, the decision that matters: the authored DOCUMENT replicates + 1425 · 21-D — THE HUD BECOMES INTERACTIVE, AND THE GAME SHELL + 1493 · 21-E — GAME HARDENING + 1539 · E7 HUD content + 1554 · 21-F — LEVELS, COLLECTIBLES v2, HUD EDITOR POLISH + 1627 · 21-G1 — SCENES NOT LEVELS; RECIPE RE-HOMING + 1652 · 21-G2..G5 — PROJECTS, PER-PLAYER PROGRESS, CROSS-SCENE PRESENCE. + 1666 · R22 ROUND 11 CORRECTS THE "PROVEN STABLE" CLAIM + 1902 · SCOPE, settled after C4 shipped and worth not re-litigating: + 1991 · R22 round 12 + 2005 · R22 round 11 + 2015 - `src/modules/` — core modules (hello = the smallest complete example, button = + 2020 · 17-A moved piano/avatar/essentials/car/DUNGEON OUT to `theprototype-app/modules` + 2030 - UI: `components/menu/*` (drawers/modals; visibility via stores + `hidePanels/ + 2105 - `tests/e2e/` — committed Playwright suites (`npm run e2e`, subset by name); + 2107 - Docs (2026-07-24 split): SDK authoring docs live on the PUBLIC docs site + 2113 - `src/lib/knock.js` + `knockMath.js` (24-A) — A HAND KNOCKS A BODY. `knockMath` is the pure + 2127 - `src/lib/lookPresence.js` (P2) — a peer's LOOK as PRESENCE, in the `campreview` shape: + 2132 - `src/lib/postGraphs.js` + `postGraphPresets.js` (P4) — a shader graph whose output IS a + 2137 - `src/lib/materialSharing.js` (D2) — two objects, ONE material, by id. The id is scene data + 2144 - `src/lib/flowPrefs.js` (114) — the node editor's mouse bindings as a LOCAL pref leaf + 2147 - `docking.js` (81.4) — an edge holds TWO stacked panels: `docked` is + 2151 - `meshEdit.js` (F1/F3) — a proportional drag ends in ONE whole-geometry `meshgeo` commit + 2158 - THE VOCABULARY (roadmap 22 R4, locked): + 2161 · room + 2163 - `src/lib/sceneRename.js` (R22 R5, a LEAF, imports nothing): `manifest.renames = {from:{to,at}}` + 2182 - `peerScenes.peersAtScene(map, scene, mine, host)` (R22 R4) — the per-scene session roster, + 2189 - `src/lib/sceneLoader.js` (33 L1, a LEAF: svelte/store only) — A SCENE LOAD MAY TAKE TIME; IT + 2221 · The autosave writes pristine kit pieces as STUBS + 2230 - `src/lib/flowLayout.js` + `src/lib/coalesce.js` (R29 S1/S2, both LEAVES, vitest-covered): + 2236 - `scripts/templates/` — ONE FILE PER TEMPLATE (34 R4 A3). + 2242 - `scripts/author-templates.cjs` (21-C C6-b): a `MODULE_DEFS` LOADER region (now in `templates/index.cjs`; … + 2254 - `playMode.js` `embedMode` / `embedSceneId` / `embedOpenUrl()` (R29 fork 4): the additive + 2262 - ROADMAP 30 — EDIT / INTERACT, AND THE SELECTION THAT FINDS THINGS + 2278 · THE POINTER-CAPTURE THROW (P0) + 2284 · SELECT-THROUGH + 2293 · MODULE CONTENT IN THE OBJECT LIST + 2305 - ROADMAP 30 — GAMES START ON START + 2317 · FREE-CURSOR GAMES + 2326 · STORAGE + 2333 · CONSOLE HYGIENE + 2338 - ROADMAP 30 — THE AUTHOR KIT + 2358 - ROADMAP 30b — THE QUEST ROUND (games in a headset) + 2396 - ROADMAP 30c — THE MESHY ASSET ROUND + 2409 - ROADMAP 31 — TOWERS BECOMES A GAME + 2438 - ROADMAP 31 — THE SECOND QUEST ROUND + 2470 · 34 R5: + 2475 · 34 PF/R1 — `src/lib/perf/` + 2482 · 36 (1.21) — export, touch, loading, theme tokens. + 2499 - ROADMAP 33 — MODULES ACROSS A SCENE SWITCH (33-scene-switch, L2/L3/L4) + 2543 - ROADMAP 33 — LOD GROUPS (33-lod-editor, contract P1) + 2558 - ROADMAP 33 — FUNCTIONAL PACK ITEMS (33-anim-core, contract P2) + 2559 · NOTHING AUTOPLAYS + 2569 · `animRef` (33-scenes) + 2571 - ROADMAP 33 — KIT INSTANCING (33-scenes) + 2580 - ROADMAP 34 — THE GAME KIT, part 1 (34-kit-core, contract T3) + 2606 - ROADMAP 34 R3 — BEHAVIOURS, code-first game logic with a DERIVED live node view (34-behaviours, D1+D2) + 2639 - ROADMAP 33 — EDITOR UI + GAMES (33-editor-ui, 33-games, 33-untangle-core) + + 2653 ## UI design system (redesign epic) + 2655 - All UI follows the redesign spec kept in the `cloud` repo: `docs/design/redesign/SPEC.md` + 2658 - Build UI only from `src/components/ui` primitives (WindowChrome, Tabs, Segmented, Chips, + 2661 - Colours, radii, spacing and type come from the semantic tokens in `app.css` only — no raw + 2663 - Every icon goes through `ui/Icon.svelte` (sizes 16 or 20 only; custom glyphs as `tool:`; enforced by … + 2664 - A redesign change must not alter behaviour: DragRow scrubbing, modifiers, click-to-type, + + 2668 ## Replication golden rules + + 2768 ## Hard-won gotchas (do not rediscover) + 2770 - RAPIER'S KinematicCharacterController COLLIDES WITH SENSORS unless the query passes `EXCLUDE_SENSORS` + 2774 - HEADLESS GPU SCREENSHOTS ON THIS MACHINE (ANGLE/Vulkan, RADV) CAN MISS TEXTURED KIT MESHES + 2777 - A LEVEL WITH NO DYNAMIC BODY STARTS NO SIMULATION, so its walker collides with nothing + 2779 - A sliced load changes what "right after" means. + 2782 - A two-page "in phase" check must compare CLOCKS, not poses + 2785 - A HEADSET'S EYE BUFFER IS NEVER LOWERED + 2789 - AN INSTALLED USER MODULE NEVER UPDATES ITSELF. + 2793 - A VITE STARTED INSIDE `e2e-slot` INHERITS FDS 8/9 AND HOLDS BOTH MACHINE-WIDE E2E LOCKS + 2796 - A LEGACY-MODE TEMPLATE CALL `fn(x)` IS UNTRACKED BEYOND ITS ARGUMENTS + 2799 - A `$state` (deep) PROXY OVER THREE OBJECTS IS A PER-FRAME COST + 2802 - `a ?? b + '/'` IS `a ?? (b + '/')` + 2805 - **HELPER_LAYER MUST NEVER BE 1 OR 2: three's WebXRManager gives the LEFT eye layer 1 and the + 2809 - three's `Euler.toArray()` IS `[x, y, z, 'XYZ']` + 2813 - A BLANKET SHAPE RENAME HITS EVERY KEY OF THAT SHAPE. + 2817 - A rapier collider with no body is FIXED AND SOLID. + 2822 - A THIRD SOFTWARE-GL PAGE BOOTS PAST setupPage's 30 s ON THE 1.17 UNION + 2827 - An ADDITIVE burst vanishes against a bright sky + 2830 - A module effect/click on a Quest cannot rely on the DOM HUD + 2833 - Known, not fixed (30c pack lanes): + 2837 - `TubeGeometry` HANDED A 2D `Shape` BUILDS EVERY VERTEX AS NaN + 2842 - A rAF TIMESTAMP IS THE FRAME'S START, WHICH CAN PRECEDE THE CALL THAT SCHEDULED IT. + 2847 - A HUD TEXT BOX IS A FLEX ROW, SO `text-align` ALONE NEVER CENTRED A SINGLE LINE. + 2851 - A perRound LATCH READS UN-SET THE INSTANT THE ROUND ENDS + 2854 - PARTICLE EMITTERS ARE COUNTED PER PAIR + 2857 - `--use-angle=gl` IS SWIFTSHADER ON LINUX. + 2860 - **EVERY TOP-LEVEL OBJECT IN objectsGroup BECOMES A FIXED PHYSICS BODY, and a decoration GROUP's + 2863 - DESKTOP PLAY SPAWNS AT Scene.svelte's FIXED `` + 2865 - THE EDITOR GRID DRAWS AT y = 0 + 2868 - A FLAT GROUND DISC ENDS IN A BAND, NOT A HORIZON + 2871 - THREE LANES NUMBERING ONE LIST FROM THE SAME BASE COLLIDE. + 2874 - jsDelivr PARSES `@v2` AS A SEMVER VERSION, AND A VERSION IS NEVER RE-RESOLVED. + 2883 · 29f CLOSED IT BY MEASUREMENT + 2897 - A jsDelivr BRANCH ref is `max-age=604800` IN THE BROWSER + 2904 - flowbite-svelte's `Button` FREEZES its class string at mount. + 2913 - An INVALIDATION KEY must name the thing whose change should invalidate. + 2919 - TWO LANES CAN EACH BE CORRECT AND COMPOSE WRONG — the enumerated-roots edition. + 2927 - A guard that saves INTO a store the guarded action then WIPES is a lie. + 2935 - A movement keydown guard must include ALT. + 2940 - e2e: the runner kills a suite at 8 minutes + 2949 - SessionsManager test hooks + 2957 - Vite watches `tests/` + 2963 - **A mount SAVE is several awaits long, so an edit made DURING one is not in the bytes + 2968 - ConfirmModal RESTORES FOCUS AFTER the thing you open next mounts. + 2973 - A SUITE THAT PASSES WITH SECONDS OF HEADROOM AGAINST THE 480s RUNNER CAP IS A COIN. + 2977 - CONSENT MUST BE RECORDED BEFORE THE WRITE THAT PUBLISHES IT. + 2983 - **A HASH IN `currentLevel` MEANS "THESE ARE THE BYTES I LOADED", and adoption must not + 2991 - A RE-SYNC REQUEST IS NOT A JOIN, and the receiver cannot tell unless it is told. + 2997 - FORK 3'S CARRY SEMANTICS MAKE A PULLED gameState ALWAYS LOSE. + 3003 - **WHEN A FLEX ROW OVERFLOWS, WHAT FALLS OFF THE END IS WHATEVER IS LAST IN THE MARKUP — + 3014 - A GROUPED WINDOW IS PLACED BY ITS GROUP, AND `dragWindow` HAS TO BE TOLD. + 3029 - ZERO IS NOT A WIDTH. + 3034 - A TAB GROUP'S MINIMUM IS THE WORST CASE ACROSS ITS MEMBERS, not a constant. + 3041 - A GATE THAT DISABLES THE GESTURE THAT WOULD UNDO ITS OWN STATE STRANDS THE USER. + 3047 - **A COUNTER THAT GAINS A DIGIT MOVES EVERYTHING AFTER IT, and `tabular-nums` alone does + 3054 - **A FRAME -> SECONDS -> FRAME ROUND TRIP IS NOT THE IDENTITY, and the first casualty is + 3065 - **DUPLICATE DOM ids ACROSS COMPONENT INSTANCES ARE HARMLESS UNTIL A `