# LANE A — NOTES (round 1)

World v0 is in and **the real game boots it**: `http://localhost:8140/?dbg=1` runs Lane A's
canal on C's `L2_esophagus` (3600 units, hash `cefc4f83`), zero console errors. No boot.js
change was needed — C's `levels/index.js` landing is what unblocked `pickWorld()`.

## What landed

`web/js/world/` — nothing outside it was touched.

| file | what |
|---|---|
| `spline.js` | the math: centreline, arclength LUTs, parallel-transport frames, radius law, wave phase, `project()`, `hash()`, `stats()`. **Imports no three.js** → `node web/js/world/spline.js --selfcheck` runs headless (14 assertions). |
| `biomes.js` | palette/param registry (ART_BIBLE values). C ratified this as the biome-id source of truth. |
| `wall_material.js` | the synthetic-scanner wall: tint × detail + fresnel rim + fog, peristalsis in the vertex shader. |
| `tube.js` | chunked extrusion + streaming window. |
| `arena.js` | displaced-icosphere shells (the stretch goal), v0. |
| `flycam.js` | `?fly=1` noclip. **Not wired** — snippet for F below. |
| `index.js` | `createWorld()` — THE WORLD CONTRACT, drop-in for the stub. |
| `dev.html` | my harness. Dev-only; delete when boot covers it. |
| `_fixture.js` | dev fixture level. Delete when C's levels cover every case (arenas: L3 does now). |

Contract is implemented as written — same surface, units and semantics as the stub, so B's
stub-built work should port unchanged. Extras beyond it: `flowPulse(s,t)`, `hash()`, `stats()`,
`dispose()`, `spline`, and an **optional 2nd arg on `project(pos, hint)`** (see → Lane B).

## Measured (say how you measured it — so: how)

All from `dev.html` on C's real `L2_esophagus` with D's real pack, 1920×1080, via
`renderer.info` (`DBG.pose()` / the sweep in `DBG.sweep()`):

| thing | measured | budget |
|---|---|---|
| draws, worst frame over a full-canal sweep | **8** | ≤150 (charter), ≤300 (TECH) |
| tris, worst frame | **74,420** | ≤500k |
| live chunks | 5–8 (+1 per arena) | — |
| geometries after full sweep + return to s=0 | **6 → 6, no leak** | dispose-clean |
| geometries after `dispose()` | **0** (textures left alone — D owns them) | — |
| level load, C's 3600-unit L2 | **<120 ms** | <2 s |
| pinch ratio (min turn radius ÷ max tube radius) | L2 **3.32**, L3 **1.89**, fixture 2.37 | >1.5 |

**fps is NOT measured and I'm not claiming it.** Screenshot tooling drives the tab hidden,
which pauses rAF; `gl.finish()` didn't sync either (it reported 0.12 ms/frame = 8219 fps,
which is obviously junk). Draws/tris are reliable and are 20× under budget, so I expect fps to
be fine — but someone with a real window has to confirm. → Lane F, at round end.

Determinism: same seed ⇒ same hash, **and node and the browser agree** (`50a5b0ec` for the
fixture in both). Different seed ⇒ different hash. Both asserted in the selfcheck.

## Decisions (mine to make; flagging the ones that touch you)

1. **`curviness` = fraction of the tightest bend this pipe can survive**, not an amplitude.
   Octave amplitudes are solved from a curvature budget `kappa = curviness / (2.2 × baseRadius)`.
   Why: authoring amplitudes in units let a wide, curvy segment fold the tube through itself —
   the selfcheck's pinch guard caught exactly that on its first run (turn radius 9.9 vs tube
   radius 17.2). Now a wide pipe straightens itself and **C never has to think about curvature**.
   C's 0.15–0.95 range works as documented; nothing to change.
2. **Peristalsis: global `OMEGA = 3.08 rad/s`, wavenumber `k(s) = OMEGA / flow(s)`, phase
   `K(s) = ∫k ds`** (baked per-vertex as `aPhase`). Consequence worth knowing: **a wave crest
   travels at exactly the local flow speed**, so "ride the crest for boost" is physically the
   same thing as "ride the current". Plain `k·s − ω·t` tears the wave at every flow change;
   `K(s)` doesn't. For a flow-14 esophagus this reproduces the stub's wave exactly (k = 0.22).
3. **Esophagus `wave.amp` 0.9 → 1.4** (`biomes.js`). The stub's 0.9 is a barely-legible ripple;
   GDD §2 makes riding a crest the level's signature, and B can't surf what the player can't
   see. 1.4 reads as distinct rings and still leaves ~78% of a radius-10 tube flyable. Measured
   in the harness against 0.9 and 3.2. **This moves `wallRho`** → Lane B.
4. **Chunk seams align to biome joins**; an arena owns its whole s-span and the tube is skipped
   there (otherwise the corridor renders *inside* the room and `collide()` disagrees with what
   you can see). Seam is chunk-quantised ±20u — fine until sphincter geometry exists.
5. **Arena fog is sized to the room**, not the biome: `min(biome.fog, 1.09/radius)`. Biome fog
   is tuned for ~100u corridor sightlines; reused in C's 360u-wide acid sea it renders a black
   rectangle (that's what my first arena shot literally was). Small lairs keep the murk.
6. **One `uv` attribute = (θ/2π, s in world units)**, no uv2 — tiling is derived in-shader from
   a `uTile` uniform, so a second UV set would be dead weight. Charter said uv2; this is the
   simplification, flagged rather than done silently.
7. **Colorspace: matched to the stub** — the wall writes its colour with no
   `<colorspace_fragment>` conversion. That's a whole-game decision (it moves every colour at
   once) so I'm not making it unilaterally → Lane F.

## Known limitations (v0, stated plainly)

- **Arena pole pinch.** Arena UVs are spherical, so the fold pattern converges into a starburst
  at the poles — visible top-right in `round1_L3_arena_acid_sea.png`. A real texture will pinch
  there too. Round 2: triplanar projection for arena shells.
- **Arena shape.** A sphere is the wrong shape for a stomach and C says so in their own
  `L3` note. Round 2 should discuss a capsule/lobed form; the `arenaAt` contract (centre+radius)
  survives either way for B's clamp.
- **No acid plane** (GDD's animated `#c8ff3a` level) — round 2, needs C's event to drive height.
- **No villi** (small-intestine InstancedMesh band) — round 2, no level needs it yet.
- **No matcap / normal map.** D ships `normal` maps already; using them needs a TBN in the wall
  shader. Round 2. Untested code paths are worse than absent ones.
- **`project()` in arena mode** is step-clamped rather than properly solved; it's accurate in
  tube mode (recovers s to <0.25, θ to <0.02 rad — asserted). Arena collision doesn't use it
  (sphere test), so this only matters if B calls `project` on a point way off the centreline.
- Segment param blending lags the march by one 0.5u step. Deterministic, sub-step, harmless.

---

## → Lane F

1. **Pass `assets` into `createWorld`.** The game currently boots my world but **D's textures
   are loaded and then unused** — I verified every material comes up `USE_DETAIL`-less under
   `boot.js`. In `pickWorld()`:
   ```js
   const mod = await import('./world/index.js');
   const level = await loadLevel(flags.lvl);
   const assets = await (await import('./core/assets.js')).createAssets({ flags, renderer });
   return mod.createWorld(level, { rng: createRng(flags.seed ?? level.seed), assets });
   ```
   (`createWorld` already ignores a missing/failed `assets` — the fallback law holds either way.)
2. **Add the world selfcheck to the qa gate** — it has caught two real defects already:
   ```bash
   # 4b. Lane A world math (headless; no three.js needed)
   node web/js/world/spline.js --selfcheck >/dev/null 2>&1 && green "world selfcheck" || red "world selfcheck failed"
   ```
3. **Screenshot tooling traps**, which will bite you at round-end boot-matrix time. The pane
   drives tabs **hidden**: rAF never fires (so the loop isn't running when you shoot) and
   `innerWidth` is **0** (so the canvas is 0×0 and `toDataURL()` returns `"data:,"`). The pane
   screenshot still looks right because grabbing it fronts the tab. `DBG.shot()`'s anchor-click
   download also silently no-ops here. My harness works around all three in `DBG.size/pose/poseAt`
   — worth lifting into `boot.js`'s DBG. I got PNGs out by having the page `fetch(PUT)` them to
   a 30-line local sink (in my scratchpad, happy to promote it to `tools/` if you want it).
4. **Dev port**: 8140 was already held by another session's server, and `launch.json` is yours,
   so I left it alone and reused the running one. If lanes are meant to run concurrently on one
   box this needs a call.
5. Contract freeze: I'd like `project(pos, hint)` and `flowPulse/hash/stats/dispose` folded into
   TECH.md's world contract, and `assets.texture()` added to the asset contract (see → Lane D).

## → Lane B

- **`wallRho` shrank**: it subtracts `wave.amp + wave.breathe + 0.6` and esophagus `wave.amp`
  is now 1.4 (was 0.9). Playable radius in a base-10 tube ≈ **7.85**. Re-check your graze
  distances. It's time-independent on purpose — a collision surface that breathed would have
  you fighting the wall.
- **`project(pos, hint)`** — pass your previous `s` as `hint` and it's near-free and exact. With
  no hint it seeds from `-pos.z` and still converges (6 fixed-point steps), but the hint is
  strictly better in your per-frame path. Both are asserted in the selfcheck.
- **Crest speed == `biomeAt(s).flow`**, exactly, by construction. `world.flowPulse(s, t)` is
  the *same* function the vertex shader runs (0 = trough, 1 = crest), so pulse ≈ 1 means the
  wall is at its narrowest AND the crest is moving at flow speed right there. Boost window.
- `biomeAt(s).flow` is **blended across segment joins**, not stepped — safe to read per-frame.
- Arena mode: `modeAt(s)` / `arenaAt(s)` → `{center, radius}` for your 6DOF clamp. `collide()`
  branches automatically (sphere test inside a room, radius test in a tube); you don't have to.
- `sample()` allocates fresh Vector3s per call, same as the stub. If that shows up in your
  profile, ask and I'll add `sampleInto(s, out)`.

## → Lane C

- Your schema v1 reads clean here — v1 is additive and my reader consumes L1/L2/L3 unchanged.
  Your ratified arena semantics (sphere spanning `[at-radius, at+radius]`) are exactly what's
  implemented, and `modeAt`/`arenaAt` agree with your notes on all three L3 arenas.
- **Biome ids are the registry in `world/biomes.js`**: `oral · esophagus · stomach ·
  small_intestine · large_intestine · appendix`. Unknown id ⇒ neutral fallback + one warning,
  never a crash. Thanks for writing the "biome = tissue family, not anatomy" rule into TECH —
  that's exactly right and it's why `name` carries the anatomy.
- `curviness` behaves as you documented (0 straight … 1 writhing). Under the hood 1.0 now means
  "as curvy as this radius can safely be", so **you can't author a pinch** — L2 measures 3.32×
  clear, L3 1.89×. Author freely.
- Free lever if you want it: `flow` is per-segment and it now sets the peristalsis wavelength
  (`k = 3.08/flow`). Low flow ⇒ tighter, slower rings. A "sluggish" segment reads sluggish.
- Your L3 note that a sphere is the wrong shape for the stomach body: agreed, see limitations.

## → Lane D

- **Naming mismatch, silent by construction.** Your keys are `wall_smallint_a` / (future)
  colon; my biome ids are `small_intestine` / `large_intestine`. `wall_${biomeId}_a` therefore
  misses, and because assets are optional it falls back to procedural **forever with no error**.
  I've added an explicit slug map in `index.js` so it works today. Proposal for round 2:
  standardise on the biome ids (`wall_small_intestine_a`) and delete the map — one name, one
  place. Your call, just let's not leave it implicit.
- **Your `tile` semantics and mine converged independently** — `[repeats around theta, units of
  s per repeat]`, both read off the stub's uv. Ratified from my side; `[4,16]` looks right in
  engine (`round1_L2_textured.png`).
- **`texture()` is the useful entry point, not `get()`.** TECH names `get(category,name)` as THE
  contract, but it returns raw JSON a shader can't eat. I prefer `assets.texture(name)` and keep
  `get` duck-typed as the documented floor. Suggest F promotes `texture()` into TECH.
- FYI: tiling is applied **in-shader** from `tile`. A raw ShaderMaterial has no `uvTransform`, so
  the `repeat` you pre-apply on the texture is inert here. Not a problem — just don't count on it.
- `wall_esophagus_a` looks genuinely good in engine. The `_b` variants and matcap aren't wired
  yet (round 2, with the TBN work).

## → Lane E

- `world.flowPulse(s, t)` (0..1, crest = 1) is yours to pulse audio/HUD with; it's exact, cheap,
  and matches what the wall is visibly doing.
- `world.biomeAt(s)` → `{id, palette, fog, flow, coatDrain}`. `palette.tint/rim/void` are the
  ART_BIBLE hexes — use `rim` if you want HUD accents that match the biome you're in.

## Round 2 (mine)

Triplanar arena shells + acid plane · villi band (InstancedMesh, small intestine) · normal-map
TBN + matcap · sphincter joint geometry at biome seams · `sampleInto` if B needs it · delete
`_fixture.js`/`dev.html` once boot + C's levels cover them.
