# 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. [B] 2026-07-16 — **sail.js + rigging.js landed on `lane/b`, rebased on M0.** `checkContract('sailRig')` conforms; `js/tests/b.test.js` runs 28 asserts green. 3D verlet cloth, N=10, structural/shear/bend, 5 iterations at a fixed 1/60 substep, wind per FACE. `step(dt, wind, t)` takes ragged frame dt and does its own fixed-dt substepping — asserted that a 4-24 ms ragged loop converges on the fixed-dt trace, so what selftest proves actually applies to the running game. [B] 2026-07-16 — **⚠️ UNITS CHANGED — Lane A (HUD) read this one.** `corner.load` and `hw.rating` are in NEWTONS now, not the prototype's arbitrary scale. I retuned `HARDWARE` in contracts.js to real WLLs (carabiner 1200 N, shackle 3200 N, rated 6500 N) under the standing note in that file that Lane B owns these numbers — costs and tier shape untouched, and $80 still buys rated hardware on at most 2 of 4 corners (asserted). **HUD: show `load/1000` as kN.** A 5×5 m sail pulls ~1-4 kN per corner in a 34 m/s storm, which is exactly why real shade sails use 3 kN+ shackles. That's DESIGN.md's Kerbal trick working — the number on the meter is one you could take to a hardware shop. [B] 2026-07-16 — thanks for the `sway(t)` clarification, it caught a real bug: I had it as an offset and was adding it to `pos`, which would have flung every tree-anchored corner to double its coordinates. Also consuming `world.sunDir` and `world.gardenBed` as specified (centre+size rect; a hit along sunDir means shaded). One nit: `coverageOver()` starts its rays at y=0 rather than `heightAt(x,z)`. On ±0.3 m terrain under a 3 m sail that's ~0.2 m of shadow error — not worth a contract change now, flagging so it isn't a surprise later. [B] 2026-07-16 — **⚠️ FINDING FOR LANE A — the yard's anchors imply enormous sails.** Every 4-anchor quad a player can pick from the 7 fixed anchors, by area: h1,h2,t1,p1 = 70 m² · h2,h3,t2,p2 = 71 m² · t1,t2,p1,p2 = 111 m² · h1,h3,t1,t2 = 133 m² · h2,t1,p1,p2 = 143 m² · h1,h3,p1,p2 = **192 m²**. Real domestic shade sails are 20-50 m², and DESIGN.md itself pictures "a 30 m² kite". Wind load scales with area, so at 192 m² nothing affordable on an $80 budget survives a real storm. The sim is saying "you cannot span the whole yard", which is correct physics and arguably correct design — but it means the natural, obvious pick (house corners out to both posts) is an instant loss. Options in my order of preference: (1) more anchors, closer together, so a sensible 25-40 m² quad exists at all, (2) posts moved in, (3) accept it and let the prep-phase load bars teach it. Not my call — flagging with numbers rather than guessing. Nothing blocks on it; M1 is playable either way. [B] 2026-07-16 — **❓ OPEN — the flat-horizontal loophole. Needs Lane C, or the water spike.** DESIGN.md's core tension is "big, flat, low = great shade, death in a storm". My sim disagrees, and it is right to. Peak corner load over 8 wind directions, same footprint: flat *pitched* 3.06 kN, hypar 1.86 kN, flat *horizontal* **1.14 kN** — the lowest of all three. A horizontal plate in horizontal wind genuinely has almost no drag. What kills real flat sails is ponding (water weight), flutter and leeward suction, none of which are in scope for me: ponding is DESIGN.md's second prototype spike, and proper separated-flow aero is not happening in a hand-rolled cloth sim. So a player who plants four posts at equal height currently gets the *safest* possible rig, which is the exact inverse of the design's intent. Not fixable inside sail.js. Lane C: a vertical gust component would load a horizontal sail and would partly close this. [B] 2026-07-16 — **the thesis assert is scored on WORST CASE over 8 wind directions, not per-direction.** PLAN3D §5-B says "twisted peak < flat peak, same storm". Per-direction is a false assert and I won't ship it: from the one angle where a flat sail sits edge-on it genuinely beats the hypar, and forcing that green would mean tuning the sim into a lie. Worst-case is also the honest game question, since Lane C's storms veer and the player never gets to pick the wind. Result: flat worst 3.06 kN (from S) vs hypar worst 1.86 kN (from N) — the hypar sheds 39% off its worst moment. Thesis holds. [B] 2026-07-16 — two notes for whoever next reads sail.js, because both look "simplifiable" and aren't. (1) Corner load is read from each constraint's **XPBD Lagrange multiplier** (|λ|/dt²), NOT from `FABRIC_K × leftover stretch`. After a fixed 5 iterations the leftover stretch is *solver error*, not fabric strain, so the obvious reading measures the solver — it came out ~50× hot, 60 kN peaks on a 5×5 sail. The `statics` assert is what keeps this honest: corner reactions must sum to the real aerodynamic + weight force on the fabric (Newton's third law). It balances to 8.3%. If someone "simplifies" the load reading, that assert is what goes red. (2) The **tension dial was remapped** off the prototype's `rest = rest/tension`, which asks for 29% pre-strain at dial 1.4 and put 68 kN on a corner before any wind blew. It is now a real pre-strain (0.10/dial → 4% at 1.4). [B] 2026-07-16 — **BUG worth knowing about, fixed:** a corner that blew was marked `broken` but never got its mass back, so it stayed pinned — a "blown" corner sat welded in mid-air and the sail quietly went dead instead of flogging. PLAN3D §5-B wants flogging emergent from the freed node, and it is now. The cascade test missed it entirely because it forced the break by hand and called `_repin()` itself; the replacement drives a real overload failure and asserts the corner tears free of its anchor and keeps moving. Lesson for other lanes: a test that sets up state by hand can pass over a dead code path. [B] 2026-07-16 — **Lane D — your API is ready.** `sailRig.repairCorner(i, hw)` re-pins a blown corner (your 2.5 s hold-E; returns false if it isn't broken). `sailRig.trimCorner(i, ±delta)` is the per-corner turnbuckle (your 1.2 s hold; clamps 0.85-1.15, scales rest lengths near that corner only). Both emit on `sailRig.events`. Spare count lives on `RiggingSession.spares` — gate `canUse()` on it and decrement on use. [B] 2026-07-16 — **Lane A — wiring the rendered sail.** `const view = await createSailView(rig); scene.add(view);` then `view.update()` each frame after `rig.step()`. Returns a THREE.Group, double-sided, `castShadow` on — the shadow IS the product. three is imported lazily inside it so the sim core stays headless-runnable. Verified headless (100 verts / 162 tris, normals recomputed, verts track the sim) but **not yet eyeballed in a browser** — it wants a look once it's in main.js. The prep-phase picking UI is NOT landed: it needs your camera and anchor markers to raycast against. All the rules behind it are done and tested in `RiggingSession` (rigging.js), so it's a thin click-to-session adapter once M0's camera is available to me. [B] 2026-07-16 — selftest convention, slightly off your stub and I think worth keeping: my asserts live next to the code in `js/sail.selftest.js` and `js/rigging.selftest.js` as exported `[name, fn]` arrays, and `js/tests/b.test.js` is a 3-line adapter that feeds them to your Suite. The reason is that those modules ALSO run under plain `node web/world/js/sail.selftest.js` — no browser, no server, ~7 s — which is how the cloth got proven before M0 existed. Same array both ways, so the two harnesses cannot drift. `contracts.js` importing three is no obstacle to that: node imports `three.module.js` fine.