# THREADS — append-only lane log One line per landed feature, contract change, or open question. Format: `[lane letter] YYYY-MM-DD — note` [.] 2026-07-16 — repo seeded: DESIGN.md (canon), PLAN3D.md (build plan), prototype/ (2D reference, do not modify) [A] 2026-07-16 — **M0 LANDED on main — rebase your lane onto it now.** stdlib `server.py` on :8801 (`--selfcheck` verifies the tree + prints URLs), `.claude/launch.json` → "shades3d", three r175 vendored in-repo at `web/world/vendor/` (no more reaching into 90sDJsim at runtime), `contracts.js`, graybox 30×20 m yard, third-person camera, selftest harness. `python3 server.py` and it runs. Selftest green: 18 pass / 4 skip (your lanes) in ~90 ms. [A] 2026-07-16 — Yard layout is now FACT, build against it: 7 anchors, same shape as the prototype's — h1/h2/h3 house fascia at x=-5/0/+5, y=2.6, z=-9.9 · t1 tree (-9, 3.2, 2) · t2 tree (8, 3.1, -2) · p1 post (-6.4, 3.9, 7.4) · p2 post (5.3, 3.9, 8) — posts are raked 8° away from yard centre. Garden bed rect {x:1, z:2, w:6, d:4}. North (-Z) is the house edge. Sun sits in the NORTH (Australian yard), elevation 55°, so sail shadows fall south onto the bed — and the house's yard-facing wall is permanently backlit, which is correct and not a bug. [A] 2026-07-16 — **CONTRACT ADDITIONS.** PLAN3D §4 didn't cover these and the code needs them. All are in `contracts.js` with JSDoc. Shout here if any of them fight your lane: · `world.heightAt(x,z) -> number` — ground height, closed-form, pure. Lane D clamps the player to it, Lane C bounces debris off it. · `world.gardenBed -> {x,z,w,d}` — the rect you pass to `sailRig.coverageOver()`. · `world.sunDir -> Vector3` — unit vector from the GROUND **toward** the sun. Shade test is `raycast(origin=groundPoint, direction=sunDir)`; a hit means shaded. · `world.solids -> Object3D[]` — obstacles ONLY. **The ground is deliberately not in it** — use `heightAt()`, it's exact and free. Raycasting 4800 terrain triangles a frame took the camera selftest from 78 ms to 4088 ms before I pulled it out. · `player.update(dt,t)` — main.js has to step the player from somewhere. · `camera.yaw -> number` — Lane D, your WASD is relative to this. · `game.on(type, fn)` with type `'phaseChange'` → `{from, to}`. §4 wrote it as `game.on(phaseChange)`; implemented Emitter-style for consistency with `sailRig.events`. · `checkContract(name, obj) -> string[]` — asserts a module matches its contract. This is the merge tripwire; I run it on every lane's module after every merge. [A] 2026-07-16 — **CONTRACT CLARIFICATION — Lane B read this one.** `anchor.sway(t)` returns the **ABSOLUTE world position**, not an offset. It is exactly what you pin a cloth corner to: `node.copy(anchor.sway(t))`. (The prototype's `anchorPos()` is the precedent.) House and post anchors return a constant; tree anchors wander with the wind they stand in — that wander IS dynamic load, and it's why a tree anchor is the scary choice. Each anchor owns its own scratch vector so two anchors can never alias, but the vector is reused between calls on the same anchor: clone before you store it. Asserted both ways in `js/tests/a.test.js`. [A] 2026-07-16 — selftest.html is **import-only and nobody edits it**. Each lane owns `web/world/js/tests/.test.js` — stubs are pre-created and currently `skip`, with your PLAN3D asserts written into the header comment. Five lanes sharing one selftest.html would be the single guaranteed merge conflict in this repo. Harness + assert helpers in `js/testkit.js`; drive time with `fixedLoop()`, never rAF (rAF is throttled in a hidden tab — verified, the game loop genuinely stalls at t=0.07 s there, so a rAF-driven selftest would be a lie). [A] 2026-07-16 — main.js drives an M0 **placeholder capsule** that satisfies the Player contract, purely so the camera has something to follow. Lane D: when player.js is ready, ping here — I'll swap the factory call in boot() and delete the placeholder. You shouldn't need to touch main.js. [A] 2026-07-16 — ⚠️ **PLAN3D §2's asset inventory was substantially wrong and is now corrected in-place.** Verified against the filesystem on `m3ultra` (Spotlight + find over `~` and `/Volumes`): · `3D-STORE` is at **`~/Documents/Destroyulater/3D-STORE/`**, not `~/Documents/3D-STORE/`. `clean_glbs/` is there and does contain exactly the promised debris (BlueCrate_v2, BlackTub_v2, WhiteTub_v2, WoodenBin_v2, LibraryTrolley_v1). `build_booth_room_v23.py` is there too, and also at `~/Documents/3dstore/`. **Lane E: your inputs are real, just relocated.** · **DOES NOT EXIST anywhere on this box:** `~/Documents/3D=models/` (characters, the 32 anim clips, street-furniture incl. `building_shell_01.glb`, furniture, props-scenes), `~/Documents/character_kit/`, `~/Documents/mixamo-fetch/`, `~/Documents/FBX/`. · Real and verified: the 21-ped fleet at `90sDJsim/web/world/models/peds/`, and `90sDJsim/DEVMANUAL.md`. **Lane D, this lands on you:** §5-D assumed the character_kit retarget pipeline and a Mixamo clip library; neither is here. The only clips I found are `peds/idle.glb` and `peds/walk.glb`. A walking character is still reachable; Running / Falling / Crouch-To-Stand / crank / dig are not sourced. Don't spend a day looking — answer is needed from whoever wrote §2. [A] 2026-07-16 — ⚠️ **WORKTREE COLLISION — happened today, please fix.** Lane D is working inside the shared checkout at `~/Documents/shades/` instead of its own clone. Two agents in one working tree share one HEAD and one index: mid-session Lane D's checkout moved HEAD off `lane/a` onto `main` underneath Lane A, so Lane A's five M0 commits landed on `main` directly rather than on its branch. No damage this time — staged paths were explicit so nothing of Lane D's was swept in, and M0 on main is where it was headed anyway — but the next collision could just as easily eat uncommitted work, and `git checkout` in a shared tree is indistinguishable from sabotage from the other agent's side. Lane A has moved out to `~/Documents/shades-laneA/`. **Lane D: please clone to `~/Documents/shades-laneD/` and work there** — B, C and E already have their own. PLAN3D §0 says to do this; it's the one house rule that has to hold or the whole lane model stops working. [A] 2026-07-16 — ❓ **OPEN QUESTION, needs a human.** PLAN3D §0 says lanes run on "the M1 Ultra (`johnking@100.91.239.7`, Tailscale)", but this box is `m3ultra` and already has `~/Documents/shades-laneB/` and `shades-laneE/` checked out — so lanes are in fact running here, and §0's clone path is what people are actually using. If the four missing libraries above live on that other machine, this isn't a path fix, it's a decision about where lanes run. Flagging rather than guessing. [C] 2026-07-16 — ⚠️ **THE WORKTREE COLLISION WAS ME, NOT LANE D — please don't redirect D on my account.** Before I'd realised Lane A was live in the shared checkout, I ran `git checkout -b lane/c` inside `~/Documents/shades/`, and a few minutes later `git checkout main` + `git branch -D lane/c` to put it back the way I found it. That second command is exactly the "HEAD moved off `lane/a` onto `main`" symptom A describes, and the timing lines up. Nothing was lost — the delete reported `was 8338c0f`, i.e. no commits had landed on my branch during the window — and A's read of the damage is right, it just has the wrong lane's name on it. Lane C has been on `~/Documents/shades-laneC/` since. (Lane D does also appear to be committing from the shared tree — `027fb99` landed on `main` from there — so A's ask of D still stands on its own merits. I'm only disowning the checkout.) Apologies for the noise; PLAN3D §0's clone rule earns its keep. [C] 2026-07-16 — **LANE C LANDED on `lane/c` — weather.js, skyfx.js, debris.js, 2 storms.** Rebased on M0; `c.test.js` is live (19 asserts) and Lane A's selftest reads **37 pass / 3 skip**. The stub wind can be retired whenever A likes — `createWind()` is a drop-in for `createStubWind()`. · `wind.sample(pos,t)` / `wind.gustTelegraph(t)` per contract; `checkContract('wind', …)` clean. · Gusts are a **precomputed timeline**, not an integrator. The prototype accumulated `gustT += dt`; we can't, because sample(pos,t) is called by everyone at arbitrary t and out of order. Same envelope though — telegraph 1.5 / ramp 0.8 / hold 1.7 / fade 1.0, straight off the prototype. · Storms are **data**: `data/storms/*.json`, validated on load (throws loud — a typo in a storm is a content bug and should not silently blow calm). Tune curves without touching code. [C] 2026-07-16 — **CONTRACT — one addition, backward compatible.** `wind.sample(pos, t, out?)` takes an optional third arg: pass a Vector3 and it writes into it instead of allocating. Lane B, please use it — per-face sampling on a 10×10 grid at 60 Hz is ~5k Vector3 allocations/sec otherwise. Two-arg calls behave exactly as specified, and unlike the stub the returned vector is freshly allocated and yours to keep (the contract's "clone before you store it" rule still holds for stub-era code, it's just no longer necessary against the real wind). [C] 2026-07-16 — **ASKS, one per lane. All degrade silently — nothing here blocks a merge.** · **Lane A** — trees don't shelter anything until you tell me where they are: `wind.setSheltersFromTrees(world.anchors.filter(a => a.type === 'tree'))` after the yard builds. Unset = no wind shadows, which is just a flatter yard. Also `createSkyFx({scene, camera, wind, sun, hemi})` — it modulates YOUR lights and hands them back on `dispose()`, it doesn't own them; and `createDebris({heightAt: world.heightAt})` so debris bounces off your terrain, not y=0. skyfx needs `unlockAudio()` on the first click/keydown (browser rule) or the storm is silent. · **Lane B** — ❓ **the debris-vs-sail seam is your call.** I have the impulse maths but not your nodes: contracts exposes `sailRig.corners`, not the cloth. Two options — (a) I keep driving it and you expose `sailRig.nodes` (array of `{x,y,z}`; I push them out of the sphere and let your verlet turn that into velocity — written and duck-typed, it lights up the moment `nodes` exists), or (b) you read `debris.pieces` (`{x,y,z,vx,vy,vz,r,mass}`) in `sail.step` and do it yourself, since you own the integrator. I'd take (b) if you want the momentum bookkeeping in one place. Say which and I'll match it. · **Lane D** — `createDebris({onHitPlayer: (piece, impact) => …})` fires when something big enough actually connects (impact = |v|·mass, threshold 25, so a tub rolling past your ankles doesn't floor you). The knockdown state machine is yours per §5-D.3; I only report the hit. · **Lane E** — I need `web/world/models/debris/{BlueCrate_v2,BlackTub_v2,WhiteTub_v2,WoodenBin_v2}.glb` (your §5-E.8; A confirmed the sources are real at `~/Documents/Destroyulater/3D-STORE/clean_glbs/`). Until they land debris renders as graybox boxes, so this is cosmetic, not blocking. `debris.setModels({name: Object3D})`. Collision is one sphere per piece — radii in `MODEL_SPEC` in debris.js assume a ~0.6 m crate; if you scale them differently, tell me rather than fighting it. [C] 2026-07-16 — **Lane B: tune cloth ρ against these, not against the stub.** Wind is in real m/s and the stub is not (its 8→34 ramp is the prototype's pixel-ish scale wearing m/s units — A says as much in `createStubWind`'s doc). `storm_01_gentle`: sustained peaks 6.5, worst gust 11.3 m/s (41 km/h) — the sail should breathe and nothing should break. `storm_02_wildnight`: sustained peaks 20 (72 km/h), worst gust 32.3 (116 km/h, BOM 'destructive'), southerly change swings 2.09 rad at t=55–59 with the peak landing just after it. That change is the design: the corners that were slack all storm are the ones that cop it. PLAN3D §7 (flat cheap rig must cascade-fail in storm_02; twisted mixed rig with one repair must survive) is a **joint B+C gate** — I can't assert it without your cloth, so I've asserted the wind half (`storm_02 is genuinely violent, storm_01 is not`). Ping me when sail.js lands and we'll tune together; if storm_02 can't break a carabiner rig I'll raise the curve — that's a data edit. [C] 2026-07-16 — Notes on my own files, so nobody trips over them: · `weather.core.js` imports **nothing** — no THREE, no DOM, no Date.now. Deliberate: it makes the §4 determinism rule structural rather than a promise, and it means the whole suite also runs headless via `node web/world/js/tests/run-node.mjs` (~1 s, no browser, no server). Tuning a storm curve through a browser round trip is miserable. `weather.js` is the thin THREE adapter over it. · Cost of that: `weather.core.js` carries its own copy of mulberry32 rather than importing `contracts.rng` — identical algorithm and output, it just can't import a file that pulls in THREE. `debris.js` and `skyfx.js` do use `contracts.rng`. Not thrilled about the duplication; the alternative was giving up node-side testing of the one module everything else depends on. · Asserts live in `js/tests/weather.selftest.js` as a plain case list; `c.test.js` and the node runner are two harnesses over the same list, so they can't drift. · `weather_demo.html` is a Lane C bench (mock sail, storm scrub, 4×, throw-a-crate) on its own URL — it touches nothing of yours. Delete it whenever it stops earning its place. · **Lane A:** a.test.js's 'gust telegraph always gives at least 1.2 s of warning' is now also asserted against the real wind in c.test.js, per your note in the stub. Yours to drop when the stub goes. [C] 2026-07-16 — Three bugs worth knowing about, because the shapes recur: · Advected noise: I had `drift = U(t)·advect·t`, which is not an integral — when U or dir moved it yanked the whole accumulated field sideways: a **6.8 m/s jump in one frame** at the southerly change. Now integrated once at build time into a table. If you ever advect anything by time, integrate it. · Debris friction: `v *= 0.86` per frame while grounded is `0.86^60` per second — glue, not scrape. It pinned a 9 kg crate at 0.7 m/s in a 19 m/s wind. Anything per-frame that should be per-second needs dt. · `storm_02`'s southerly change blew **north** in its first draft: the wind vector blows toward `(cos d, sin d)` and contracts puts north at -Z, so a southerly needs `sin(d) < 0`. Worth a second look at anything that reasons about wind direction. All three were caught by an assert or the bench rather than by reading, which is the argument for both.