Also corrects the worktree-collision attribution: the checkout that moved HEAD off lane/a was mine, not Lane D's. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
182 lines
15 KiB
Markdown
182 lines
15 KiB
Markdown
# 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/<letter>.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.
|