Merge remote-tracking branch 'origin/lane/d'

# Conflicts:
#	THREADS.md
This commit is contained in:
type-two 2026-07-17 17:59:01 +10:00
commit 7791299ef0

View File

@ -3745,3 +3745,181 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
D: if you want to poke site_02's anchors without playing three nights, this is the fastest door.
(Also `rigging.js`'s panel padded ids to 3 chars; the corner block ships `tr1b` and the column broke
the moment a second site existed. padEnd(4).)
[D] 2026-07-19 — 🎮 **I PLAYED site_02 COLD (§gate 1.3, four sprints owed). The yard is good. Night 3 is
NOT PLAYABLE, and the carport trap is inert data.** Two of those are gate-1 blockers, both with
receipts. Played the real path — nights 1→2→3 on `index.html`, mouse, no shortcuts.
**1. BLOCKER — night 3 SOFT-LOCKS. Lane B, your `setWorld` ask IS the sprint.** Reaching the corner
block the way a player does, prep opens with **last night's sail still hanging in the new yard**
(tension labels P4/H2/H3 live over the carport, a `T2B BLOWN` marker) while the HUD says "pick 4
more corner(s)". The anchor panel lists `h1 h2 h3 t1..t2b p1..p4` — the BACKYARD's twelve — in a
yard that has none of them. The data layer is fine: `world`/`rigging.session`/`rig.anchors` all
re-point to `tr1,tr1b,q1..q4,cb1,cb2,cp1,cp2` correctly. It's the UI that doesn't:
**`typeof rigging.setWorld === 'undefined'`**, so main.js:450's `rigging.setWorld?.(world)` is a
silent no-op, and `createRiggingUI` snapshots `world.anchors` into markers at line 254 — at BOOT,
off backyard_01, once. Measured, not inferred: scene-traversing every `userData.anchorId` on night
3 returns exactly the backyard's twelve, at backyard coordinates (h2 at z=9.95, the old house
line); site_02's ten have **no markers at all**. So:
```
click a ring dead-centre → no error, no ticker, no corner. summary.corners === []
rigging.commit() → false → the Enter handler (main.js:788) returns early
⇒ you cannot rig, cannot start the storm, cannot leave night 3. The week ends there.
```
Nothing is thrown and nothing is logged — it is the quietest possible failure. **This is why the
selftest can say 296/0/0 while the game is unplayable: no test drives a site change through the
rigging UI.** An assert that cannot fail is decoration; so is a green suite that never opens the
door it shipped. `setWorld(world)` — rebuild markers + pickTargets, dispose the old — unblocks it.
**2. Lane A — `boot({site})` is dead, and it fails INVERSELY.** main.js:456 promises "opts.site lets
a debug boot jump straight to a site". It doesn't: :458 loads opts.site, then `showTonight()` at
:691 immediately re-runs `loadSiteInto(week.site)` = backyard_01 and overwrites it — but the
markers were built in between, off site_02. Result is bug 1 mirrored: **markers site_02, world
backyard**, from the first frame. Verified both directions. Either honour opts.site in the week or
delete the hook — right now it's a documented lie, and it's the hook anyone debugging a site
reaches for first.
**3. THE CARPORT TRAP DOES NOT EXIST YET — its data is read by nothing.** This is the one I'd want
ruled on before gate 2 eats the sprint. Two dead fields:
```
anchor.ratingHint world.js:556 writes it → NOTHING reads it. sail.js:871 fails a corner on
`c.load > c.hw.rating` — HARDWARE only.
anchor.collateral world.js:557 writes it → NOTHING reads it. scoreRun() main.js:610-613 only
ever pushes the gnome, on a blanket lost>=2.
```
So beam 0.22 / post 0.30 / fascia 0.35 / tree 1.0 are **inert**: every anchor in the game is exactly
as strong as the carabiner you hang off it. I played the exact line DESIGN warns about — 46 m²
across `cb1+cb2` in the early buster, straight through the funnel:
```
carport line cb1 cb2 q2 q3 → garden 83% 3/4 collateral NONE +$82 "THE GARDEN MADE IT"
honest line q1 q2 q3 q4 → garden 98% 3/4 collateral NONE
```
DESIGN promises "tie 25 m² to it in a southerly and you take the carport, and it lands on whatever's
under it." I tied 46 m² to it and **got paid**. The carport held; `q3` blew — cb1 was carrying 948 N
next to q3's 952 N and the sim has no reason to prefer one. The trap is currently a ~15 HP geometry
penalty (the carport is in the west corner, the bed is centre, so it drags coverage off the bed) —
directionally right, but not a trap. It reads *fine*, not buggy; it just doesn't bite.
**Lane A: the $180 ruling is bigger than SPRINT11 thinks — "change the constant, not the asset"
assumes a constant and a consumer, and there is neither.** It needs wiring: which anchor blew →
that anchor's collateral. **One landmine when you do:** `q1..q4` are built from site JSON, not GLB
nodes, so world.js:556 never runs for them and their `ratingHint` is **undefined** — the honest
posts carry no strength data at all. Wire anchor strength naively and the anchors that should be
the *strongest* read `undefined`. Default before you multiply.
**4. And the enum is why the trap can't even tempt (my SPRINT10 flag, now with a play consequence).**
Pre-rig the panel shows only `type`. The site JSON types all four carport anchors `'post'`, so a
cold player reads **eight identical "post" rows**`q1..q4` and `cb1/cb2/cp1/cp2` are
indistinguishable, and the carport's rating never surfaces at any point before or after rigging.
"Four free anchors that look free" isn't temptation if it looks *identical to the honest steel*
it's a coin flip, and a coin flip you can't lose (see 3). Widening to `'carport'` gives the trap its
only pre-rig read. This is the same bug shape as the ladder one I landed: a rule keyed on a type
string a new site doesn't fit.
**5. Lane C — good news, the venturi WORKS at the shipped numbers, and I measured it in the storm.**
axis 2.1 / gain 1.5, throat (6,0), sampling `wind.speedAt` through the change:
```
t=4..17.5 (before) throat/east 0.95 1.08 ← calm, indistinguishable from the rest of the yard
t=19..40 (after) throat/east 1.08 → 1.79 ← throat peaks 19.7 m/s vs 11.0 east
```
That is exactly the personality the site asks for: **calm until the southerly comes through, then it
screams**, and the throat sits on the carport (x=7) — the funnel howls precisely where the bad
steel is. That's a lovely piece of design and it's already live. Your 1.08 measurement and this
aren't in conflict — the ratio is throat-vs-east, not the bare venturi factor. **Don't drop the gain
on my account**: B, the site is winnable off honest anchors (q1..q4 → **98%**), so the funnel isn't
killing the $80 line, and E doesn't need to move the tree.
**6. The ladder trip — the site's stated thesis — did not fire in either run, and can't reliably.**
`needsLadder` is live and correct (cb1/cb2 y=2.29 > the 2.20 reach, `work: 'bracket'` → ladder;
q-posts are `work: 'cloth'` at y≈4.0 → you pull the cloth corner down, no ladder, which is the rule
doing its job). But the trip only happens when a **cb** corner blows, and nothing makes cb likelier
to blow than q — that's bug 3. Fix the collateral/rating wiring and the thesis fires by itself.
**7. Small one, Lane B:** `<- weak link` is arbitrary when the hardware is uniform. rigging.js:205
reduces with a strict `<`, so ties keep the first pick — it flagged `cb1` only because I clicked cb1
first. It happened to point at the genuinely worst anchor, by luck.
**Method, so anyone can re-run it:** the shipped code cannot give you a coherent cold boot on
site_02 (bug 1 breaks the night-3 path, bug 2 breaks `opts.site`), so I rewrote `NIGHTS[0]` to
`{storm:'storm_03b_earlybuster', site:'site_02_corner_block'}` before `boot()` in a scratch copy of
index.html — then :458, `createRiggingUI` and :691 all agree and :691's rebuild no-ops, which is the
same path the four backyard nights already take. Harness deleted; **nothing of mine is on lane/d
this sprint but this entry.** Also note for anyone playtesting from a headless/hidden tab: rAF is
throttled dead, so `SHADES.step(dt)`/`.render()` are the only honest drive — and the phase change
into prep needs one frame before the markers accept clicks (that's the harness, not the game).
**Verdict as the playtester: the corner block is a GOOD yard and I want it in the game.** It reads
instantly — two streets, the carport squatting in the west corner, the bed exposed in the middle,
the gum tree as the one honest high anchor. The blurb does real work. The 18s change (vs night 2's
30s) genuinely lands as "looks like night 2 and isn't". None of that is the problem. The problem is
that **the yard's whole argument — that the free anchors cost you — is currently unenforced**, and
that you can't reach the yard at all without B's `setWorld`. Both are small, sharp fixes. Neither is
a design question.
[D] 2026-07-19 — ✅ **I PULLED `lane/a` AND PLAYED IT. A, your gate-1 work is good and I can close two of
my four. One residue, and one NEW landmine sitting directly in gate 2's path.** Played 0f288be in a
throwaway worktree, same two lines as my cold pass.
**CLOSED — the enum gives the trap its read back.** The prep panel now says what the steel is:
```
q1..q4 post ← honest
cb1 cb2 carport ← the trap, and you can SEE it now
cp1 cp2 carport_post
```
That was my #4. A cold player can now tell the carport from the honest posts before they commit,
which is the difference between a trap and a coin flip. Checking the enum in `validateSite` instead
of JSDoc'ing it is the right call — "documentation cannot fail" is the whole lesson.
**CLOSED — the carport BILLS, and it is now the best moment in the game.** Rigged cb1+cb2+q2+q3 at
max tension (1.4, the player's own `]` lever). cb1 went first, took two with it, and:
```
collateral garden gnome ($25), the carport ($180) → $205
in the bank $80 → $0
verdict "THE GARDEN MADE IT. The sail didn't — the carabiner at CB1 went first and took
2 more with it. That is what the sail was for."
world.isWrecked('carport') === true ← E's wreck swaps on the same event, lands with the card
```
**You saved the client's garden and went broke doing it.** That is DESIGN's promise delivered
literally, and the line about the sail is the best writing in the aftermath. E — your wreck lands.
**And your $180 is calibrated right, which I can now say with a number instead of a feeling.** The
$0 above is MY harness's fault, not your balance: I forced the corner block onto night 1 ($80 bank)
and it read `OFF THE JOB` — but the block is night THREE, and I measured the real bank there at
**$237** on my cold run. Settled against that: 237 +57 +37 +3 205 20 = **$109**. A good week
turned broke and felt for the rest of the run, without ending a strong one — your words, and they
land exactly. Don't move it.
**RESIDUE — `ratingHint` is still read by NOTHING** (world.js:570 writes it; grep says nothing else
on lane/a touches it). So the carport isn't *weak* steel, it's *expensive* steel — priced, not
rated. That's visible in play and it's a real fork:
```
tension 1.0 (default) q3 blows first, cb1 held at peak 1350 → collateral NONE, +$82
tension 1.4 (max) cb1 blows first, peak 1957 → collateral $205
```
Both are the same rig. The lever that decides whether the trap fires is **tension**, not the anchor
— and at default tension the honest post blows first and the carport costs you nothing. That may be
fine! Price can carry a trap. But then E's `e.test` is pinning 0.22/0.30/0.35 "with the reason" onto
numbers the sim never reads, and DESIGN's "the worst steel in the game" isn't true in the sim — it's
the worst *bill*. Either wire ratingHint into the failure threshold (sail.js:871 compares load to
`c.hw.rating` alone) or say out loud that the price is the trap and the ratings are flavour. Your
call, not mine — but it shouldn't stay ambiguous while E is pinning tests to it.
**⚠️ NEW — A, this one is aimed straight at gate 2, and I hit it by accident.**
```
main.js:31 const CALM_STORM = 'storm_01_gentle'; // hard-coded
main.js:38 const STORMS = [...new Set(NIGHTS.map(...))]; // loaded storms come ONLY from NIGHTS
main.js:374 const calmWind = winds[CALM_STORM]; // undefined if no night uses it
main.js:510 return simT % Math.max(1, calmWind.duration); // throws EVERY frame, forecast + prep
```
The calm-day wind only loads because **night one happens to be `storm_01_gentle`**. Take gentle out
of NIGHTS and `calmWind` is undefined, `windTime()` throws on every non-storm frame, and the game
dies at boot on a cryptic *"Cannot read properties of undefined (reading 'duration')"* — no mention
of storms, calm, or the week. I did exactly that rewriting NIGHTS[0] for my harness and lost ten
minutes to it. **Gate 2 has you rewriting every night entry to hang a client and a brief off it.**
If a re-theme ever drops gentle from night one, boot dies and the message points at nothing.
One-liner: `const STORMS = [...new Set([CALM_STORM, ...NIGHTS.map((_, i) => nightAt(i).storm)])]`.
Cheap now, ugly to diagnose later — it's the same species as the enum: an invariant nothing checks.
**STILL BLOCKING — night 3 is soft-locked.** `lane/b` is unmoved at b383110, so `rigging.setWorld`
still doesn't exist and everything in my previous entry stands: reach the corner block the way a
player does and you cannot rig it, cannot start, cannot leave. **My whole pass above had to go in
through a rewritten NIGHTS[0] to get a coherent boot.** Everything A landed tonight — the enum, the
bill, the wreck, that verdict line — is currently unreachable in the shipped game. B's one method is
the door in front of all of it.