v1.13.0 β Knock it about πͺ (roadmap #24 lanes A/B, looks & materials, mesh/windows/flow) - #217
Merged
Merged
Conversation
β¦ body Core could grab and throw, and a node could push a body, but nothing let a hand or a walking player HIT an object and have it leave at the speed it was hit - VR hands were grab-only, On Impact only sees falling contacts, and the character capsule never touches anything. The knock is that one seam, and both roadmap 24 games (Stars Room, VR Football) stand on it. - knockMath.js (a LEAF: THREE + throwVelocity) - the probe ring, the sphere-vs-bounding-sphere contact test with the approach speed along the normal, the response v' = v_body + n * approach * gain (an infinite-mass hand), the spin off the TANGENTIAL slip (a sphere contact is always central, so the plan's normal-impulse cross product is zero by construction), one clamp (clampThrow, then the scene's maxSpeed below it), and the one-knock- per-pass cooldown with a 60 ms hysteresis on the way back in. - knock.js (the runtime, playInteract's shape) - VR hands through a seam Scene passes in (handSnapshot; knock.js never imports vrControls), the desktop camera as a 0.35 m head probe, both carried into the objects group's frame so a bent VR rig cannot put a hand and a ball in two spaces; candidates from listPhysicsObjects (refreshed every 200 ms for spawned bodies) minus what this peer carries; body velocity exact off the initiator's rapier body, a pose ring off the move stream elsewhere; the `hit` message from the HITTER whoever it is; the non-initiator's local PREDICTION behind knock.predict, withdrawn after 400 ms if authority never confirms it; the hit log (last per body + a ring of 32, runtime state, no history kind, no handshake reply) and registerHitListener, the seam A2's onhit and api.onHit hang on; feedProbe, the test hook that sweeps a probe on its own clock. - physics.applyHit - the throw's sibling: initiator-only, clampThrow plus the scene's maxSpeed, CCD over 5 m/s, a held body refuses; bodyVelocityOf. - scenePhysics gains the ADDITIVE `knock` nested block, enabled:false by default, so Towers and every saved scene are byte-identical (the suite's counterfactual). Zero new singleton, zero handshake work. - `hit` is CONTENT: gateable by canApply like `throw`, ROOM_SCOPED like `move`, `by` stamped from the connection, never the payload. knock-physics (77 checks, node-pure + one page + two peers): monotonic in probe speed, gain, maxSpeed, receding/resting/slow do nothing, one per pass, held bodies skipped, the log, the block-off counterfactual, the play and sim gates, the wire crossing with the same stamp on both logs, prediction on and off, the capability gate dropping it and the prediction reverting, the initiator's own knock reaching the other log. scene-physics-state grows the block's defaults, clamps, merge and the pre-knock-file restore. STATUS-24a.md carries the as-built and the one finding worth a follow-up: a late joiner is never told a sim is running (`simulate` goes out at start/stop, not in the handshake), so its probes - and play-mode grab - stay down until the sim restarts. Pre-existing; the suite starts the sim after the join. svelte-check 362/47 (the pristine baseline, re-measured here first). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2ike21X4j6x1eKZTXJjia
- a proportional vertex drag ends with ONE whole-geometry meshgeo commit (commitMeshGeoSnapshot: applied locally, broadcast, recorded) instead of the selection-only verts stream β the falloff neighbourhood was never on the wire before, so a peer saw the selected vertex move and the bulge around it never arrive (19-A P4 gap; plan option A, locked) - the LOCAL apply is load-bearing: applyMeshGeo rebuilds the receiver non-indexed while a /create Plane is indexed, so both sides swap together or the sender's next verts indices address a layout the peer no longer holds (the undo path's rule) - the swap rebuilds handles in triangle order and the refresher clamps the selection by COUNT only, so the selection is captured as positions before the commit and re-found by position after (commitFalloffSnapshot) - a commit refused by MAX_SNAPSHOT falls back to the old verts + entry path - suite mesh-falloff-sync (two peers, 24 checks): B's neighbours match A's corner for corner, both hold the same layout, a later plain drag lands on the same corner for B, one undo flattens the bulge on both - counterfactual: with the end-of-drag commit replaced by `false`, mesh-falloff-sync reads B's halfway neighbour 0.0000 vs A's 0.5000 (red) - held: mesh-proportional 63, mesh-pivot-gizmo 131, mesh-edge-gizmo 26 (base) - svelte-check 361/47, list identical to base; build green (server stopped) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgtYLde61S8THqad37dDQm
- decision (plan option A, the interaction itself stays WONTFIX): the slide measures from the PROXY and a placed pivot seats the proxy away from the vertex, so a slide under a pivot would project from an unrelated point; slide constrains ONE vertex to its own edge, a pivot transforms a SET about a chosen centre β different questions, no evidence anyone wants both at once - the tool silently doing nothing was the only real problem: proxyLocal now toasts once per session when the slide is armed, a drag has started and a custom pivot is placed (slidePivotTold, reset in exitEditMode) - no wire, no history, no toolbox change; held mesh suites at base - svelte-check 361/47, list identical to base; build green (server stopped) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgtYLde61S8THqad37dDQm
β¦mulate in the handshake - `onhit` trigger node (Triggers, beside onimpact): minSpeed, who anyone|me|others; fired on EVERY peer from the `hit` application (fireObjectHit) as a trigger stamp derived from the message's own `at` β one message per knock, no nodetrigger, the stamp literally equal on every peer; value outputs speed/byMe as a handle map with a `__default` pulse on the unnamed dot (unwrapHandle grew `__default`; every earlier handle-map producer omits it, so they are byte-unchanged) - own card OnHitNode.svelte (pulse dot + speed/byMe rows, the MoveInput shape), registered in Nodes.svelte's CORE_NODE_TYPES (the two-registry gotcha) - api.onHit(cb) -> unsubscribe + api.hitLog(); the knock import is PRIMED and kept as a promise so a listener registered at module boot is not dropped (the DEVX #8 family) - a LOCAL VR hit buzzes the hitting hand (min(1, 0.2 + speed/10), 30 ms) through a haptic seam Scene passes into startKnock (hapticPulse); the head probe buzzes nothing - sendHandshake pushes `simulate` when this peer is simulating (A1's late-joiner finding: the probes and play-mode grab never armed for a joiner mid-run), held with the singletons under the same !holdContent condition; additive, the start message's own shape - Inspector > Physics > Knock: enabled checkbox + Gain / Max speed / Probe radius / Spin rows, data-anchor Knock -> openSceneSection('Physics:Knock') - suite knock-node (50 checks: two peers + a late joiner, PEER_CONFIG, GPU_ARGS): who and minSpeed on both peers, the equal stamp, speed/byMe, the per-player touches banked once per hit per peer (the 21-F3 double-bank counter-case), the SDK payload agreeing with the node's stamp, the handshake simulate, the Inspector rows + deep link, the haptic seam - counterfactuals, each red then restored: fireObjectHit without its applyNodeTrigger -> 23 pass / 11 fail (2.2, 2.4, 2.5, 2.7, 2.11, 3.2-3.4: nothing stamped anywhere); the who gate removed -> 44 / 7 (2.3, 2.6, 3.3, 3.4 and the double-bank 2.11/3.6: `me` fired on the other peer); the handshake simulate push removed -> 47 / 4 (5.1-5.3: the late joiner's remoteSimulating stays null); unwrapHandle without __default -> 49 / 2 (2.10: the unnamed edge reads undefined, the Math node its 5 fallback) - held: knock-physics 77/0, scene-physics-state 37/0, play-interact 46/0, throw-peer 28/0, flow-unknown-node 24/0 (the new card resolves), game-towers 20/0; flow-spawner 39/0 39 pass then `SCRIPT FAILED: page.waitForSelector('.svelte-flow') Timeout 15000ms` β A/B'd AND PRE-EXISTING: pristine origin/release/next src on this same box, server and lock fails IDENTICALLY (39 pass, same timeout at the same point). It is the suite's own last check in Β§7, which opens the node editor by clicking `p[title="Node editor (N)"]`; that string exists only as a menu-item PROPERTY in Controls.svelte, so the opener looks stale on this build β a separate ticket, not this lane's (flow-unknown-node mounts the same editor 24/0 with the new card); palette-groups 10/1 β the one red is PRE-EXISTING on release/next (the Music group has no NodeWrapper accent; NodeWrapper untouched here) - svelte-check 361/47 (base 361/47, identical entry list); build green Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013je8UxeJpoMSRcyejTiNFj
The PR #192 follow-up, done before the second game's graph is written. - graphBuilder() at module scope: N/E hoisted from towersGraph()/beatGraph(); E takes both handles (the beat superset β a 3-arg call yields the Towers id verbatim), and both graphs return g.done() - remapData walks every OWN string field and string array, replacing a def-local object name with its uuid, except HUMAN_TEXT_KEYS (label/format/text/placeholder/ name); uuid/selected/camera/hash fall out as ordinary cases; the Set crosses into the page as a list argument - scripts/compare-authored.cjs: canonicalises what a build mints afresh (session id/createdAt/appVersion/thumbnail, changedAt/startedAt/at stamps, every uuid by first appearance) and diffs two authored trees; the ritual is in the script header - PROVEN byte-identical on Towers: `--only towers --out` before (the pre-A3 script) and after -> SAME games/towers/scene.tpscene (76676 canonical chars, the inline thumbnail 6607/6607 and thumb.webp 2968/2968 identical), SAME index.json; the collectible module loaded in both builds (no SKIP) - held: game-towers 20/0 - svelte-check 361/47 unchanged (no src change) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013je8UxeJpoMSRcyejTiNFj
- proportional editing was a TRANSLATE tool: applyPivotTransform turned/scaled the selected set only and left the falloff neighbourhood exactly where it was. It now blends the WHOLE weighted neighbourhood β the plan's option 3, the weighted transform blend: per vertex the gesture's rotation is slerped and its scale lerped toward identity by that vertex's smoothstep weight, then applied. For a pure rotation that is the conventional weighted ANGLE (a w=0.5 vertex turns half way, so a straight row becomes a spiral, which is what Blender does) and it is the only reading that stays defined when rotate and scale combine. - the selection keeps w = 1 by construction (beginFalloff), so a rotate with nothing in range is byte-identical to before. - fixes a pre-existing hole the blend exposed: recaptureVertexFalloff (the mid-drag wheel resize) always re-applied as a TRANSLATE, so resizing the radius during a rotate/scale displaced the neighbours; it re-applies through applyProxyGesture (which dispatches by mode) when a gizmo gesture is live, keeping the translate shape for the VR/no-gesture path. - replication and undo come free: a falloff gesture already commits one whole-geometry meshgeo (F3). - suite mesh-falloff-rotate-scale (22 checks): swept ANGLES about the pivot, never distances (every invariant a rotation preserves is preserved by a WRONG rotation too) β 75.94 / 45.00 / 14.06 degrees at the three weighted rings, the rim vertex unturned, a vertex beyond the radius byte-identical, one undo exact, the scale factors lerp(1, 2, w), and proportional OFF still turning the selection alone. - counterfactual: with the weight forced to 1 the suite reads 7 FAILURES β every weighted vertex sweeps 90.00 degrees, the spiral twist is 0.000 and every scale factor is 2. - two fixture traps this suite paid for, both recorded in its header: an attribute INDEX recorded before a meshgeo commit addresses a DIFFERENT vertex after it (F3's commit rebuilds the mesh index-expanded), so every tracked vertex is recorded in both layouts; and a 4-wide plane has no vertex beyond a radius of 2, so the grid is 6 wide. - held: mesh-proportional 63, mesh-pivot-gizmo 131, mesh-falloff-sync 24 (base) - svelte-check 361/47, list identical to base; build green (server stopped) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgtYLde61S8THqad37dDQm
Watching a peer already adopts their CAMERA; it now adopts the state that decides
what that camera LOOKS like, which is the only way a camera-scoped or locally
overridden look is observable from outside. Presence, never data.
- `lookPresence.js` (a LEAF, the `campreview` shape): a per-peer `lookstate` row
{camera, mode, overrides:{post,shaders}, look} published on CHANGE (signature
gated, never on a timer), replied to `getmodulestate` beside
sendCameraPreviewState, and dropped at both finalizeDisconnect sites. ADDITIVE:
an older build never sends one, its row stays ABSENT, and the watcher falls back
to its own state β which is exactly what it did before this existed.
- Outline.svelte resolves the chain from the WATCHED peer's row while
`specatorMode` names them: the camera they look through and its own look, their
view mode, their local post switch and their Set Look overrides.
`resolvedDoc(key, overrides?)` takes the override map as an argument so the
watcher never writes anything of theirs into its own stores β leaving the watch
reverts by construction. `__postDebug().adoptedFrom` names whose state it is.
- The watch banner SAYS when adoption cannot take effect (they shared no row, or
they have the scene look switched off) β the P1 lesson that a viewpoint-scoped
feature must speak on its own surface, since silence there is indistinguishable
from a dead wire.
- Suite `watch-look`, 45 checks on two peers: the handshake row both ways, live
changes (camera, Set Look, the local switch, wireframe), watch adopts, stop
reverts, an absent row falls back, and A disconnecting mid-watch strands nothing.
Counterfactuals, each proven by breaking the code and watching it go red:
- drop the `adopted` branch in Outline's chain effect -> 10 red (2.3/2.7/2.9/2.10/
2.13/2.14/2.16/3.1/4.2/4.6): B renders its own chain while watching.
- remove `dropPeerLook` from the two disconnect sites -> 5.1/5.2/5.3 red: B stays
stranded on a departed peer's look.
- remove the module-level send-on-change subscribes -> 1.5 red (the camera change
never reaches B's row) and Β§2 follows it down.
Also fixed on the way, both found by those runs:
- a duplicated, mis-indented `dropPeerLook` in leaveSession's loop (idempotent, so
harmless, but it read as a mistake).
- the suite selected `.peer-watch`, which is ALSO worn by the join-a-peer's-camera
button rendered beside Watch whenever that peer is previewing β the exact fixture
this suite builds. B joined A's camera instead of watching it, so every reading
was "fill-blue": the right answer for the wrong reason. It selects by exclusion
now and asserts the button count. The same trap sits on `.spectator-exit`, which
the camera-preview banner shares with the watch banner.
Gates: watch-look 45/45 Β· duplicate-parity 28/0 Β· scene-post-effects 41/1 and
shader-editor 64/2 (both pre-existing, reproduced at the base commit) Β·
svelte-check 361 errors / 47 warnings with the message set identical to base Β·
`npm run build` green with the dev server down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qw58R9VNZNVDGPYsFwfaPR
- an edge holds up to TWO windows now, stacked: dropping a second floating
window onto a docked panel (or onto that edge) splits the column
vertically, a draggable divider sets the share, and undocking either
member collapses it back to one full-height panel. The quiz decision
stands β splits are for DOCKED panels and tabbing (83) for floating ones,
and they cannot compete because headerTargetAt already skips docked nodes.
- the half you release over is the slot you take (upper half = the newcomer
goes on top), shown before release by #dock-zone[data-split] over that
half; a THIRD window on a full edge still wiggles the occupant.
- `docked` became {left: string[], right: string[]}; a persisted string from
before reads as a one-element list, so an existing dock survives untouched.
Heights are CSS calc against --bottom-inset, so the dock still pushes them
and a SINGLE panel's style is byte-identical to before. The share persists
per side (dockSplit:<side>, 0.15..0.85); the width is one column, so the
side handle writes dockWidth: for every member and either grip resizes it.
- the divider sits INSIDE the top panel: every docked window is
overflow-hidden, so a handle hung past the edge is clipped away and takes
no pointer events at all (measured β the first one moved nothing and wrote
no ratio; the width grip only works because half of its 6px is inside).
- a docked window's geometry belongs to docking.js, so the two "reveal"
clamps that re-assert a window's own floating rect when it becomes visible
again now stand down for it (dragWindow.js, and the object list's dragMe
in Controls.svelte) β the same rule they already keep for a tab member.
Without it, reopening a split edge after a reload threw both panels back
to wherever they last floated: measured 164px -> 250px, the object list's
default floating height.
- suite dock-splits (17 checks): the affordance, the two-panel geometry,
the divider drag, the persisted share and stack, a refused third window,
a reload, and the collapse on undock. docking.test grew the split case
(8 -> 10) β an occupied edge no longer refuses.
- counterfactual: with the split target disabled the drop wiggles and one
panel remains β dock-splits reads 7 FAILURES, starting at the affordance.
- held: docking 10, dock-float-exclusivity 8, dock-tab-drag 57,
window-bottom-clamp 14, window-size-clamp 36, window-tabs 14,
window-tabs-move 6 (all at base)
- svelte-check 361/47, identical to base once line numbers are normalised
(a 4-line insertion in Controls.svelte shifts them); build green
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgtYLde61S8THqad37dDQm
Layer 1 of the look has had a stack, a registry and twelve built-in kinds since
L1-L5; what it has not had is a way to AUTHOR a new one without writing a module.
A post graph compiles to a fragment over SCREEN buffers and enters the ordinary
scene stack as one more entry β so it replicates, saves, undoes, reorders and
MERGES with its neighbours with nothing new on the wire and no new history kind.
- `shaderCatalog`: a third stage, `post` (absent `stages` still means every stage,
so the arithmetic and channel nodes serve both domains). New **Post** group, all
post-only: Scene colour, Scene sample, Scene depth (linear + raw), Scene normal,
Resolution, Bayer pattern, Edge detect, Ambient occlusion β plus `postOutput`,
the domain's own terminal, and `outputNodeFor(domain)`. `normal` is now declared
fragment+vertex: a screen pixel has no surface of its own, and the post domain
reads the normal BUFFER instead.
- `shaderCompile`: the evaluator core extracted into `createCompiler(graph,
outputType)` and shared, with a new `compilePostGraphToIR` walking the Post
output's colour/alpha taps. `STAGE_EQUIVALENT` generalises the vertex-stage
default translation to post (`vUv` -> the screen `uv`; a surface normal has NO
equivalent, so a socket defaulting to one is refused BY NAME rather than reading
a varying that does not exist in an EffectPass). `tpDepthAt` is emitted once
whenever any node wants depth, so Edge detect and the AO node work in a graph
that has no Scene depth node in it.
- `postGraphs.js`: the bridge β a post effect KIND `graph` whose `params.graph`
names a `post:`-keyed document (the prefix SH1 reserved). `scenePost` never
learns what a shader graph is and this module never touches the composer.
- `postGraphPresets.js` (pure data): posterise, ordered dither, depth+normal edge
detect, graph AO. They PROVE THE SEAM rather than being hardcoded β each is an
ordinary graph you can take apart, and between them they touch every input the
domain has. Every number in them is a Float NODE: the arithmetic nodes take
operands from sockets and have no params, so an authored `{b: 0.5}` would be
silently ignored and the socket's 0.0 used.
- `scenePost`: two OPTIONAL registry members, both absent on every built-in so
their signatures stay byte-identical β `signature(params)` for a kind whose
output depends on state its params only POINT at, and `tick` for a live uniform.
A post graph's signature is its compiled FRAGMENT, so a value scrub writes the
uniform and only a STRUCTURAL edit rebuilds the chain.
- `Outline`: ONE NormalPass, built only while something in the stack reads normals
(a second scene render is not an ambient cost), and the per-frame `tick` that
feeds the shared shader clock without a rebuild.
- UI: a `graph` group in the Post-processing add menu offering the presets and the
scene's existing graphs (its bare leaf would have added a row pointing at
nothing), a graph picker + Edit on the row, and a Surface|Post switch in the
shader editor β same cards, same palette filtered by domain, with the post half
getting a scope picker because a post graph belongs to no object.
Counterfactuals, each proven by breaking the code:
- drop the `signature` fold in postStackSignature -> 5.4 red: a structural edit
leaves the stale pass compiled into the chain.
- remove the NormalPass block in rebuildStack -> 4.2 and 7.3 red: edge detect
renders with an empty normal buffer.
- remove POST_EQUIVALENT from STAGE_EQUIVALENT -> 1.8 red: a shared node's `vUv`
default is emitted into a post shader, where it compiles and reads nothing.
Two suites carried an assertion this invalidates, updated here: the post library
is 14 kinds now (`graph:graph`), and `postOutput` joins `surface` as a terminal
that emits nothing.
Gates: shader-post-domain 48/48 NEW Β· the 19-suite shader/post battery at or above
base (camera-looks 43 Β· scene-post 93 Β· scene-post-ui 44 Β· shader-graph 67 Β·
shader-sync 20 Β· shader-persist 19 Β· shader-editor 64/2 and scene-post-effects
42/1, both pre-existing Β· watch-look 45) Β· svelte-check 361/47, message set
identical to base Β· `npm run build` green with the dev server down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qw58R9VNZNVDGPYsFwfaPR
β¦ms measured
Most of layer 2 shipped with SH6b β `graphKeyFor` already resolves own graph ->
scene default -> the object's real material, `defaultTargetsFor('scene')` already
drives every mesh without one, and each object already keeps its own base colour
because the compile clones per object. What was missing was the LOCAL half, and
proof for two things the plan said were free.
- `viewportOverrides.shaders` finally renders something. It was DECLARED in B
ahead of this phase precisely so layer 2 would add a renderer and not a new
concept, and until now nothing read it β the Inspector even filtered the
checkbox out of the Overrides list because it would have done nothing.
`applyShaderLayer` in shaderGraph swaps every shader-driven object to its own
material and back; the checkbox is in the list now, with a hint that says what
it does.
- OFF IS A SWAP, NEVER A DETACH: the documents, the compiled materials and the
base materials all stay, so switching back costs no compile and a peer sees
nothing at all. A compile that lands WHILE it is off is remembered and not
installed, which is not hypothetical β a peer editing the scene graph
recompiles on my machine through that same path.
- Deliberately not `scene.overrideMaterial`: that replaces EVERY material in the
scene, and this layer is only the ones a graph drives. Which is also why
wireframe and the UV checker suppress layers 2 and 3 for free β they own that
slot β and the plan asked for it to be asserted rather than assumed.
Suite `scene-default-material` (33 checks, two peers, pixels): the resolution
order and own-before-scene as two DIFFERENT material instances; one graph over
three objects each keeping its own colour; wireframe and the UV checker taking
overrideMaterial while the graphs stay attached underneath; the local override
including a recompile while it is off; a late joiner inheriting the scene default
through the scene key; and a scene using none of it carrying no documents.
Counterfactuals:
- remove the viewportOverrides subscribe -> 4.2/4.3 red: the switch does nothing.
- remove the install guard in applyMaterial -> 4.6 red: a recompile while the
layer is off puts the material back on.
A trap worth recording, because the first version of that second counterfactual
PASSED against the bug: the injected material is a CLONE of the base, so both
read `MeshStandardMaterial` and a type check cannot tell "the layer is off" from
"the layer just installed something". The metric is material IDENTITY now
(`isBase`), which is what makes the guard provable.
Gates: scene-default-material 33/33 NEW Β· shader-graph 67 Β· shader-scene-default
16 Β· shader-persist 19 Β· shader-inspector 24 Β· shader-sync 20 Β· shader-post-domain
48 Β· shader-window 40 Β· shader-editor 64/2 (pre-existing) Β· svelte-check 361/47
with the message set identical to base Β· `npm run build` green, server down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qw58R9VNZNVDGPYsFwfaPR
- Settings > Input > Node editor > Mouse bindings, a LOCAL pref in the new
leaf src/lib/flowPrefs.js (localStorage flow:mouseBindings):
* Classic (the DEFAULT, and byte-identical to every shipped version): a
left drag on the pane pans, Shift+drag draws a selection box.
* Select-first: a left drag rectangle-selects, dragging the selection
moves the whole set, Shift+click adds to or removes from it, and the
middle or right button pans β while a right click that does not travel
still opens the pane menu.
- the user's call (2026-07-11) was to keep the current behaviour and make it
adjustable, so nothing changes for anyone who never opens the setting.
- xyflow is 1.6.5 here and the plan was written for 0.1.x, so every prop was
re-verified in node_modules: panOnDrag / selectionOnDrag / selectionMode
survived, but the key prop is `multiSelectionKey`, NOT the documented
`multiSelectionKeyCode`. Node moves still broadcast per node through the
existing onnodedragstop, so 114.2 needed nothing on the wire.
- THE PART THAT NEEDED CODE: once panOnDrag includes the right button,
xyflow's Pane preventDefaults EVERY contextmenu and forwards none. Its
system layer would re-emit a press that did not travel, but the svelte
wrapper never passes that callback through β so the editor tracks the
gesture itself and re-emits the pane menu.
- that decision is made on POINTERUP, not on the contextmenu event:
Chromium fires `contextmenu` on the PRESS, so at that moment a gesture has
travelled zero pixels whether it is a click or a 200px pan β measured, and
the first version opened the menu on every right drag.
- suite flow-mouse-bindings (23 checks, real mouse): Classic pans and selects
nothing and still opens its menu; Select-first selects, moves the set by one
delta, toggles with Shift, pans on the right button without a menu, opens the
menu on a stationary right click, and survives a reload with the Settings row
present.
- counterfactual: with the setting ignored (selectFirst pinned false) the
Select-first section reads 3 FAILURES β the left drag pans instead of
selecting. Classic IS the default, so that half is its own control.
- three geometry traps recorded in the suite header, all found by printing
document.elementFromPoint rather than by reading handlers: the pane's
"empty" bottom-right corner is the MINIMAP (a drag there panned 631px for
an 80px gesture), the docked pane is ~300px tall so two cards 160 units
apart do not both fit, and a rectangle selection renders an overlay across
the selected cards that eats a later click aimed at one of them.
- held: flow-nodes-core 15, flow-dock-toggle 19, flow-node-undocked 4,
node-drag-fields 24, flow-palette 7 (+ its one PRE-EXISTING red, the hover
delay, identical to base)
- svelte-check 361/47, error and warning lists identical to base; build green
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgtYLde61S8THqad37dDQm
β¦ decisions The four questions the plan says are only answerable once the three layers meet. ONE STORY. Configure Scene read as unrelated sections β "Post-processing" for layer 1, nothing at all for layers 2 and 3 β which is how "must my peers switch this on?" became a separate question three times over. There is one **Scene look** section now: what the look IS (three layers, all scene data, only the right to switch one off is local), then the stack, then the materials layer as a summary plus the way in, because its editing surface is a dock tab and belongs there. The label moved and the DEEP-LINK NAME did not. `Section` takes `aliases` now, so a rename lists what the section used to be called instead of hunting every menu, component and suite that wrote the old name down β and silently missing one. The 21-G1 rule, generalised into the component that enforces it. THE COST LINE, EXTENDED. L3's "Effects: N, passes: M" was the only place a look's cost was visible. The materials layer speaks in the same voice now: "A scene default and 2 objects with their own β driving 9 objects, 3 programs." PROGRAMS rather than objects is the honest number β `customProgramCacheKey` hashes the injected source, so N objects on one graph compile one program (measured 22 -> 23 for 24 objects when SH6b's compile-once optimisation was declined on evidence). THE SAVE-PATH AUDIT, done once and written where the carriers are (shaderGraph.js). All three layers are a KEYED DOCUMENT plus a runtime product, and the rule is the same each time: save the document, never the product. Wire, autosave, sessions and undo each carry all three; the products are carried by nobody and rebuilt on the other side. The conclusion worth recording is a NEGATIVE one: `parkShaderMaterials` exists only because layers 2 and 3 attach their product to the scene TREE, and a post Effect lives in the composer, which no serializer walks β so P4 needed no fourth park, and a reader looking for one now finds the paragraph saying why. THE CAPABILITY GATE, DECIDED: they stay separate, reason in viewMode.js beside `postSupported`. Three properties of the measured ANGLE/D3D11 failure do not transfer to a material β blast radius (a broken pass takes the whole viewport, a broken material keeps its last good one and reports), where they run (post is skipped in VR, materials are the only layer that works there, so one gate would switch off the half that works), and who compiles (a material goes through three's own program path, so gating it is gating three). Materials get a CHOICE (`viewportOverrides.shaders`, P5); post keeps its REFUSAL. Counterfactual: remove `aliases` from Section's deep-link match -> scene-post-ui 1.3 red (gap -3090px: the old name lands nowhere). Gates: scene-post-ui 50/50 (44 base + 6 new) Β· scene-post 93 Β· panel-deeplinks 23 Β· shader-inspector 24 Β· camera-looks 43 Β· watch-look 45 Β· shader-post-domain 48 Β· scene-default-material 33 Β· scene-post-effects 42/1 and shader-editor 64/2, both pre-existing Β· svelte-check 361/47 message set identical to base Β· build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qw58R9VNZNVDGPYsFwfaPR
β¦der lane proved The user's ask from 2026-08-17 β "by default copy, add into app settings an option to share" β held since then because sharing is one line LOCALLY and everything hard about it is replication: every material change is broadcast PER OBJECT, so two objects sharing an instance would diverge the moment a peer applied one. The plan said to wait for the shader lane and reuse whatever identity it established. THE IDENTITY IS `userData.materialId` β the carrier `userData.physics`, `userData.origin`, `userData.camera` and `__uuid` already ride, which is the four-carrier problem (wire, autosave, sessions, undo) solved once rather than designed twice. `materialSharing.js` is a leaf holding it and three rules: 1. THE ID IS THE TRUTH; THE INSTANCE IS AN OPTIMISATION. Objects sharing an id should share one THREE.Material so a local edit is instant on both, but every carrier splits instances somewhere β GLTF rebuilds one per mesh, a peer receives objects one message at a time, undo re-parses a subtree. So nothing depends on the instance: `reconcileSharedMaterials` re-unifies by id whenever the scene changes (the shaderGraph reconcile shape). 2. THE SENDER FANS. Rather than mint a material-addressed message β a new type, a new applier, a capability-gate entry and a story for every older peer β the sender repeats the per-object messages a receiver ALREADY understands, once per sharer. One choke point (`materialsHandler.broadcast`) covers colour, params, maps, the slot array and the type switch; the wire is byte-unchanged. 3. COPY REMAINS THE DEFAULT (`shareDuplicatedMaterials`, LOCAL, off). A duplicate is a working copy of everything that belongs to the object β D1's DCC rule β and Blender's linked duplicate is its own command, not its default. Geometry is always copied: two questions, and only one of them has a setting. The one op that REPLACES a material rather than writing into it β the type switch β relinks the sharers locally, or the next reconcile would put the old material back on the object that just changed. UI: the Settings toggle under the other Duplicate options, and a shared-material notice in the Inspector's Material section with **Unlink** beside it (the `shader-driven` notice's shape, one concern over), because an edit that quietly changes another object needs to say so where the edit happens. Suite `material-sharing` (42 checks, three peers): copy still the default and PROVEN by editing, sharing on, unlink, the reconcile, the id in a save, two peers both ways, a first share made while connected, a type switch, and a late joiner. Counterfactuals β and the first two are the reason this suite is bigger than it looks, because BOTH passed against the first version of it: - remove the fan -> 6d.2 red. It took a receiver whose instances are still SPLIT to isolate: with them already unified, one message reaches both objects for free, so the check was passing for the wrong reason. Β§6d stops that peer's reconcile and separates its materials by hand, which is the real window (just after objects arrive, just after a duplicate, and always for an older peer). - remove the applier's link -> 6b.3 red. Likewise: the id rides the full-state sync, so a peer that connected AFTER the sharing needs no applier at all. The case that needs one is a FIRST share made while connected, where the id is minted on a source whose userData never gets re-sent β Β§6b creates a fresh object for exactly that. - make the reconcile a no-op -> 4.3 red. Gates: material-sharing 42/42 NEW Β· duplicate-parity 28 Β· material-types 11 Β· uv-materials 25 Β· mesh-edit-materials 34 Β· shader-inspector 24 Β· object-sync 12 Β· animation-autokey-material 11, all at base Β· svelte-check 361/47 with the message set identical to base Β· `npm run build` green with the dev server down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qw58R9VNZNVDGPYsFwfaPR
THE GAME. A zero-gravity room of 24 glowing stars you knock around with your hands in
VR or by walking into them; free play is the default and P opens the round ("light
every star") with a per-player touch leaderboard. The first game that needs NO module
download β pure core, so the Games tab shows it with no requirement card.
- `STARS_DEF` in scripts/author-templates.cjs, authored as DATA on A3's graphBuilder:
a 12x7x12 room (see-through walls you bounce off), 24 stars on a SEEDED jittered
lattice (an LCG at module scope, so the def is stable data and two builds place every
star alike), 2 planets for contrast, a template under the floor for the spawner,
and two physical onclick PADS β the DOM HUD is invisible in a headset (F8), so Start
and More stars have in-scene buttons as well as menu ones.
- physics: gravity 0, ground off, bounds respawn, damping 0.35/0.2, grab + simOnPlay,
and the A1 `knock` block ON (gain 1, maxSpeed 10, spin 0.6). Look: a LIT preset at
exposure 0.55 + bloom (F14 β never `night` alone), ao/AGX/vignette/smaa.
- the graph: per star an `onhit` scaling a particle burst by its `speed` output and
ringing a chime, an `onhit who:'me'` banking a per-player `touches`, and a perRound
latch -> Select -> Set Color that lights it during a round; a 23-node add chain ->
"Lit: n / 24" -> compare -> allplayers -> `over`. Towers' menu/pause/over shell
re-skinned; NO shared HUD builder (D1 decides that with three documents on the table).
- the default screen is `input: 'game'`, deliberately: a `menu`-input screen visible
while playing releases the pointer lock (21-E3), which would make free play unplayable
on desktop. Every button lives on the P menu or a pad.
- NEW def field `sounds: [{key, name, url, sha256}]` (additive): fetched in node like
`music`, dropped into the Explorer, addressed from a Sound node as `'$sound:<key>'`
through A3's widened remap β but NOT the scene's music slot, because a chime is an
asset a node plays, not background music. The chime is audio-essentials'
impact-glass.ogg (Kenney, CC0), sha256-pinned; its 8400 bytes ride the .tpscene.
TWO CORE BUGS, both found by authoring and both fixed here:
- `peervariable` was MISSING from flowRuntime's `valueTypes`, so `resolveInputs` refused
it as a wire source and a Player Variable node delivered NOTHING to anything wired to
it β the consumer silently kept its own dialled value. Shipped since 21-G4, and it is
the exact `peervariable -> hudtext` shape CLAUDE.md prescribes for this game. Measured:
two readouts rendered 0 while the leaderboard beside them, which reads peerVars
directly rather than through a wire, read 1. `peer-variables` gains section 1c, which
wires the node into a Math node and asks the CONSUMER what it resolved β the existing
1b evaluates the node IN ISOLATION, proves only the evaluator, and is why this shipped.
- A SUSPENDED object lost its whole effect list, and `physics.trackBody` suspends every
dynamic body that is a flow-animation target for the whole run ("dynamic wins over an
animation"). So a Set Color on a dynamic body applied once in the frames before the
sim's bodies existed and then STUCK β material colour is not base-managed, so nothing
repainted it. `POSE_FREE_EFFECTS` (setcolor, setuniform, deviceparam, notetrigger) now
still run for a suspended object, with no base restore and no pose effects; visibility,
module effects, scripts and custom nodes stay suspended because they may write a pose.
Diagnosed in this order: the wire resolved correctly under both clocks, the file's
wiring was right, and then changing the node's DIALLED colour live did nothing β which
is what separates "not applying" from "resolving wrong".
- suite `game-stars-room` (36 checks, skip-never-fail, driving the REAL .tpscene; takes
`STARS_ROOM_TPSCENE` so a lane can point it at a scratch build): the physics block
restored from the file, 36 objects / 27 dynamic, the chime's bytes in the Explorer, a
probe pass leaving Star 1 at ~4 m/s and damping bleeding it by half in 3 s, a 10 m/s
star still inside the room 2 s later, More stars +3 with the cap holding at 59 (the
spawner RECYCLES oldest-out, it does not refuse), one touch banked in my own row, the
round lighting every star and ending in `over`, and a NEW round un-lighting them.
- counterfactuals, each red then restored: reverting POSE_FREE_EFFECTS to the blanket skip turns `...and Star 3 is painted lit` red (the star reads the dialled #3b3f66 again); removing `peervariable` from valueTypes turns `my touch row reads 1` red (the HUD keeps the node's own 0 while the leaderboard reads 1).
- held: peer-variables 73/0 (incl. the new 1c), knock-node 50/0, play-interact 46/0, logic-nodes 77/0,
animation-node 35/0 in the 9-suite battery. Its four reds A/B'd against base: collectibles-v2 is
WORSE on base (68/2 vs 69/1); flow-physics-actions' 8.5 fails identically on base and its 2.1 is a
first-sample timing read (-0.00 -> 1.28, same end state); possess asserts the avatar module, which
left core in 17-A and has no zip on this box; game-towers' 'Restart cleared the height latches'
went 19/1 once alone and then 20/0 three times on the branch (20/0 on base src) - a flake (likely a crate
resting in a height sensor re-lights the 1m latch), and Towers holds none of the four
POSE_FREE_EFFECTS node types or a peervariable.
- 'all 24 latches read lit' asserts the TRIGGER LOG (every star's hit stamp at or after the
round's startedAt), not the count node: the instant the last star lights, allplayers flips the
round to `over`, whose Infinity cutoff reads every perRound latch un-lit again, and flowValues
publishes only every 150ms - so the count read 24 for at most one publish and a poll lost the
race (measured `last: 0` with the `over` check right after it green, even on a 25s budget).
The `over` check still proves the count chain reached 24. The stamp read folds startedAt
(epoch ms) into the tick clock's seconds-of-day exactly as retiredByRound does - a raw `>=`
read 0 - and a premise check right after Star 3 lights (the log counts exactly 1, Star 1 having
been knocked before Start) keeps it from passing vacuously.
- svelte-check 361/47, entry list identical to base; build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
β¦ws-flow The lane forked at 4d7dce2 (1.11.0); release/next has since taken roadmaps 25-27 and shipped as 1.12.0, which rewrote the files this lane also touches. Resolved to BOTH sides' intent rather than either wholesale: - `docking.js` (the one textual conflict, both hunks): the lane's edge-split logic on top of 27-H's storage codemod β every read and write of `dockedWindows`, `dockWidth:` and the new `dockSplit:` share goes through `safeStorage`, so a docked layout cannot kill its own subscriber in private mode. The 81.4 sideList/persist shape is unchanged. - `flowPrefs.js` (114's new leaf, no conflict β the gate caught it): same conversion. `scripts/check-storage.cjs`, which CI runs, was red on these four call sites and is green now; the `typeof localStorage` guard it carried is safeStorage's job (a SecurityError walks straight through that guard, which is why the codemod exists). - `meshEdit.js`, `Controls.svelte`, `Settings.svelte`, `Nodes.svelte`, `dragWindow.js` auto-merged; checked by hand that both sides survived β the lane's F1/F2/F3 and reveal clamps, and hardening's `pokeScene()`, `safeStorage` and VR vertex cap. - 26-B's invariant holds for free on F3's new path: `commitMeshGeoSnapshot` applies through `applyMeshGeo`, which ends in `pokeScene()`. Gates on the merged tree, all green: - `node scripts/check-ratchet.cjs` β 341 errors / 47 warnings, exactly the committed floor (the lane measured 361/47 on the old base; the floor moved with the batch). - `npm run build` exit 0 with the dev server stopped; `npx vitest run` 133/133 (11 files β the unit layer did not exist when this lane forked). - e2e, one batch on port 5179 under `/tmp/tp-e2e.lock`: **249 PASS, 0 FAIL** across dock-splits 17 Β· docking 10 Β· flow-mouse-bindings 23 Β· mesh-falloff-rotate-scale 22 Β· mesh-falloff-sync 24 Β· mesh-proportional 63 Β· **scene-poke** Β· **storage-hardening** β the last two are the hardening suites for the two invariants this merge touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Knf7qCs3U1pQN8T3fCQCGw
The lane forked at 4d7dce2 (1.11.0); release/next has since taken roadmaps 25-27 and shipped as 1.12.0, which rewrote peerHandler, physics, flowRuntime, Scene and App β every file A1-A4 also touches. Resolved to BOTH sides' intent, and in three places the merge is more than a textual union: - **`knock.js`'s wire stamps move to 25-E's session clock.** `at` is stamped by the hitter and then folded into the trigger log on EVERY peer (A2 keys the `onhit` pulse by it), so it is exactly the kind of number sessionClock's contract names: "only a number another machine will compare needs to be on the session's clock". It was `Date.now()`, which on a peer whose clock runs minutes out would have placed every On Hit pulse somewhere else on the shared timeline than every other stamp in the same log. - **`hit` gets a shape in 27-A's `wireValidate` table.** Absent means ALLOW, so it worked without one; but the velocities are applied to a body the moment the message lands, and the table is where a malformed one is counted and dropped instead of reaching `applyHit`. - **App.svelte's three debug tails** are the union: the lane's `knock` beside the batch's diagnostics / wireValidate / wireErrors / safeStorage / sceneBudget / overloadGuard / qualityGovernor. All three lists count 205 and stay in the same order β the one real trap here is that `git checkout --theirs` on this file drops the lane's `import('./lib/knock')` 100 lines above the conflict and silently shifts every later binding by one. Everything else auto-merged and was checked by hand: A2's `simulate` push still sits in `sendHandshake` beside the batch's `gettriggers`/`joinresult`, the `hit` case still sits behind 27-A's shape-then-gate dispatch, A4's `POSE_FREE_EFFECTS` and `peervariable` valueType survived, and 26-B needs nothing from `applyHit` (it writes rapier velocities, not the THREE tree). Gates on the merged tree: - `node scripts/check-ratchet.cjs` β **341 errors / 47 warnings**, exactly the committed floor (the lane measured 361/47 on the old base; the floor moved with the batch). - `npm run build` exit 0 with the dev server stopped; `npx vitest run` **133/133**. - e2e, one batch on 5182 under `/tmp/tp-e2e.lock`: **377 PASS** β game-stars-room 36 Β· knock-node 50 Β· knock-physics 77 Β· peer-variables 73 Β· scene-physics-state 37 Β· net-handshake 9 Β· **wire-hardening 15** Β· **session-clock 26** Β· **runtime-resilience 15** Β· flow-spawner 39. The last four are the batch's own suites for the seams this merge touched. - `flow-spawner` fails after those 39 checks (`waitForSelector('.svelte-flow')`, Β§7's reopen). **A/B'd and PRE-EXISTING**: the same suite on a pristine `origin/main` @3f71477 worktree fails identically β 39 PASS then the same timeout. The lane had recorded it as a suspected standing red and could not prove it; this settles it, and it is a red on release/next today, not something this lane or this merge introduced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Knf7qCs3U1pQN8T3fCQCGw
β¦rials The lane forked at 4d7dce2 (1.11.0); release/next has since taken roadmaps 25-27 and shipped as 1.12.0. Three textual conflicts, each resolved to BOTH sides' intent, plus three of the batch's invariants applied to this lane's new code: - **`Outline.svelte`'s post chain** is where P2 and 26-D met on the same call. The authored documents and the view mode now come from the WATCHED peer when watching (`adopted`), and the quality governor still applies on top of that: the governor is this machine giving up post to hold its frames, and watching somebody must not undo it. So `mode` degrades `shaded-ao` β `shaded` on the ADOPTED mode, and `localEnabled` is the adopted switch AND `!reduced.postOff`. - **`materialsHandler.js`** β D2's fan (the material-TYPE swap is the one op that REPLACES the instance, so the sharers must be handed the new one) now ends in 26-B's `pokeScene()` instead of the direct `objectsGroup.update`. Both imports kept. - **App.svelte's three debug tails** are the union: the lane's lookPresence / postGraphs / postGraphPresets / materialSharing beside the batch's seven. All three lists count 208, same order. - **`materialSharing.js` moves to `safeStorage`** (27-H): the share pref was a bare `localStorage` pair with its own try/catch, which `scripts/check-storage.cjs` β a gate CI runs β was red on. safeStorage keeps a failed write in memory for the session, and the `typeof localStorage` guard it carried is exactly the one a SecurityError walks through. - **`materialSharing.js` Γ2 and `shaderGraph.js` Γ1 call `pokeScene()`** instead of `objectsGroup.update((value) => value)`. Release/next has exactly ONE of those left, in `flushScenePoke` itself; every other call site in the tree is the coalesced poke. - `postGraphs`/`materialSharing` id minting keeps `Date.now()` on purpose: an id built from a timestamp is never compared as a time, and 25-E's contract is about numbers another machine compares. - `lookstate` gets a shape in 27-A's `wireValidate` table β `overrides` and `look` are read as objects the moment the row lands. Gates on the merged tree: - `node scripts/check-ratchet.cjs` β **341 errors / 47 warnings**, exactly the floor. - `npm run build` exit 0 with the dev server stopped; `npx vitest run` **133/133**. - e2e, one batch on 5178 under `/tmp/tp-e2e.lock`: **591 PASS** β watch-look 45 Β· shader-post-domain 48 Β· scene-default-material 33 Β· material-sharing 42 Β· scene-post-ui 50 Β· scene-post 93 Β· shader-compile 85 Β· shader-graph 67 Β· scene-post-effects 42 Β· **perf-governor 39** Β· **scene-poke 32** Β· **wire-hardening 15**. The last three are the batch's suites for the invariants above. - `scene-post-effects` Β§4.5 ("assigning a LUT PUSHES its bytes to the mesh") fails. **A/B'd and PRE-EXISTING**: the same check fails on a pristine `origin/main` @3f71477 worktree (41 pass there, 42 here β this lane legitimately grew the suite by one when the post library became 14 kinds). The lane had already reproduced it at its own base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Knf7qCs3U1pQN8T3fCQCGw
feat/1.13 = origin/release/next (1.12.0) + the three v1.13 feature lanes, each of which already carries its own merge of the batch. This commit is where the lanes meet each other, which is the part no individual lane PR can show: - **`wireValidate.js`** β 24-A's `hit` and P2's `lookstate` both landed right after `throw:`. Both kept. - **App.svelte's three debug tails** β the union of all five new modules (knock, lookPresence, postGraphs, postGraphPresets, materialSharing) on top of the batch's seven. All three lists count 209, same order. - `Inspector.svelte` (Knock rows + the Scene look section), `Settings.svelte` (Node editor mouse bindings + Duplicate βΈ share materials), `Nodes.svelte` (the On Hit card + the Select-first pane) and `peerHandler` (the `hit` and `lookstate` cases) auto-merged; each was checked by hand for both lanes' symbols. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Knf7qCs3U1pQN8T3fCQCGw
β¦s the lanes paid for - CHANGELOG: a `## Unreleased` section ABOVE the shipped `## 1.12.0`, in five parts β the knock and the Stars Room, one scene look and shared materials, modelling/windows/node editor, VR Football, and the SDK additions. Written against the CURRENT file on purpose: the trap c8258dd had to undo was a lane writing `## Unreleased` while branched off an older release, after which the next version bump renamed the heading around it. This branch is based on 1.12.0, so the 1.13 bump will rename exactly this heading. - CLAUDE.md architecture map: `knock.js`/`knockMath.js` (incl. the `hit` message being CONTENT and its `at` being `sessionNow()`), `lookPresence.js`, `postGraphs.js` + `postGraphPresets.js`, `materialSharing.js`, `flowPrefs.js`, and the docking/meshEdit entries for edge splits and the falloff meshgeo commit. - CLAUDE.md gotchas, each one paid for this round: `git checkout --theirs` dropping the whole ours side of a file (it cost a silently shifted debug-hook binding during this integration); an attribute index recorded before a meshgeo commit addressing a different vertex after it; a handle hung past a docked window's edge being clipped; Chromium firing `contextmenu` on the press; the node pane's bottom-right corner being the minimap; `.peer-watch` being worn by two buttons; a type check not telling a shader-driven material from its base; a guard that is redundant on the happy path not being proven by it; a shader-graph constant having to be a Float node; and a perRound latch count reading 0 the instant the round ends. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Knf7qCs3U1pQN8T3fCQCGw
The entry claimed Football "joins the Games tab". It does not, and the docs pass found the same thing independently: `FOOTBALL_DEF` is absent from `scripts/author-templates.cjs`, the scenes feed's `games` list holds towers and stars-room only, and the football lane's handover records that the authoring run was never made β its `index.json` row already points at `games/football`, so the gallery card points at a template that is not published. What ships is the MODULE: install it from Browse, and its toolbox's "Build pitch" recipe lays out the pitch, the gates and the rules. That is what the entry says now, with the missing template named rather than implied. Authoring the def into the scenes repo is a small, separate job (import the modules repo's `football.def.json` into the author script, run `--only football --out`, review the thumbnail by eye) and it is the only thing between the module and a Games-tab card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Knf7qCs3U1pQN8T3fCQCGw
v1.13 β the three feature lanes, stacked on 1.12.0
The section was still "## Unreleased". Leaving it that way is the exact trap the hardening batch fell into: a lane writes into "## Unreleased" while branched off an older release, the next npm version bump renames that heading to ITS number, and the entries end up filed under a release that already shipped. RELEASING.md now warns about it; this is the warning being followed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The four v1.13 feature lanes, stacked onto 1.12.0 and integrated in #216 after the hardening batch rewrote the files they shared.
onhitnode andapi.onHit, author-script hoist, the Stars RoomAlso merged alongside: scenes #3 (the Stars Room artefact) and modules #3 (the football module).
Worth knowing
release/nexthad since taken roadmaps 25-27, which rewrotepeerHandler,commandsHandler,Toasts,App,autosave,physicsandsceneStore. v1.13 β the three feature lanes, stacked on 1.12.0Β #216 resolved that against both sides' intent.git checkout --theirs -- <file>discards the WHOLE ours side of that file. It silently dropped animport('./lib/knock')a hundred lines above the conflict and shifted every laterwindow.__storesbinding by one. The three debug-hook tails must count equal β 209 onfeat/1.13.## Unreleasedto## 1.13.0before the bump, deliberately: leaving it is what filed the hardening entries under the shipped 1.11.0 heading last time.Owed, not done
FOOTBALL_DEFis absent fromscripts/author-templates.cjsand the scenes feed liststowersandstars-roomonly, though the galleryindex.jsonrow already points atgames/football. The CHANGELOG describes the module and its Build pitch recipe instead, which is what actually ships.π€ Generated with Claude Code