Commit Graph

13 Commits

Author SHA1 Message Date
type-two
38b6fe9c87 Rule the gutter $90 (E's proposal adopted); teach collateralFor/wreckStructure the house; action B's effective-rating flag
Gate 3.3: collateralFor('gutter') returns {90, 'the gutter'} — canonical in
backyard_01.json beside the reasoning, GLB extras as fallback, per E's
collateralKey chain. House wreck wired on the carport's swap contract
(no-op until lane/e's GLB merges; window_glow state copied). site_02 stays
null: no house, no gutter. hud.js bars + main.js verdict now read
rating × ratingHint, matching sail.js's failure line (B's flag).
selftest 319/0/0.
2026-07-17 20:45:49 +10:00
type-two
807de80969 Wire the kid's bike: site key + loader, gnome pattern; decline the $60
Gate 3.2: bike_kid_01_v1 on backyard_01's south fence, rotYDeg 180 so the
baked -Z lean lands ON the fence — bars measured to the rail (0.286 m
reach, z 9.69 -> grip at 9.98), tyres 0.31 m clear, between posts. Unpriced
by ruling: the sim can't knock a bike over, and billing an unseen event is
the lie the invoice kills; the windward-fence pose is the honest one.
a.test pins lean direction, standoff band, and the ruling. 318/0/0.
2026-07-17 20:34:47 +10:00
type-two
a74075cda5 Sprint 11 shepherd: C's venturi correction taken, D's two landmines defused
C AND D BOTH LANDED WHILE I WAS IN GATE 2, AND BOTH CORRECT ME.

VENTURI — reverted to axis 2.1 / gain 1.5. C reached the same physics I did,
independently (|dot| means an axis is a line, mod PI), and then went one step
further and caught my reasoning: -1.08 was never a proposed AXIS. It's
dirAt(40), the southerly's HEADING once the change settles. 2.1 is the GAP'S
ORIENTATION — site geometry, which doesn't move when the wind does. They're
different quantities that sit a hair over PI apart only because the gap was
BUILT along the southerly.

So "measured beats authored" was the wrong rule for this question: I'd have
swapped a geometry fact for a weather fact because they nearly coincide, and
the site's axis would silently mean the wrong thing the day someone authors a
storm from another quarter. "A number gathered from the wrong harness" caught
both of us on the same question from opposite sides in one sprint.

Gain reverted too, and that one I simply jumped: it's a balance lever, B's audit
calls it, and C's standing offer is to drop to 1.35 only if no $80 line survives.
B hasn't run. D then measured the site winnable off honest anchors at 98% — so
the funnel isn't killing the line and the gain stays.

D's LANDMINES, both mine, both in gate 2's path:
 · CALM_STORM loaded only because night one HAPPENED to be storm_01_gentle.
   Gate 2 rewrote every night entry. Drop gentle and windTime() throws every
   non-storm frame on "Cannot read properties of undefined (reading 'duration')"
   — naming neither storms, nor calm, nor the week. Now explicit in the preload,
   and the assert calls stormsToPreload() with a gentle-free ladder, because
   asserting it against the shipped NIGHTS passes either way. That's how it hid.
 · boot({site}) was a documented lie that failed INVERSELY — markers on the
   requested yard, world on the week's. Honoured through the week now (seek to
   that site's night), because a site without its night is half a game: no
   client, no brief, no storm, no bank. Plus opts.bank, so D can boot the corner
   block at the $237 they measured instead of night one's $80 and OFF THE JOB.

RATINGHINT — D is right that nothing reads it, and the naive fix is worse than
the bug: honest posts are built from site JSON, never touch adoptAnchor, so
their ratingHint was undefined — and `load > hw.rating * undefined` is
`load > NaN`, always false. The strongest anchors would have become UNBREAKABLE
while the carport became the only thing that can fail: the trap would look like
it works, for the wrong reason, and read as a successful playtest. Every anchor
carries a real number now (DEFAULT_RATING_HINT), pinned, so the wire is safe
before it's made. The wire itself is sail.js — B's — and the ruling is in THREADS.

selftest 306/0/0 (was 304).
2026-07-17 17:51:30 +10:00
type-two
4aee055f2d Sprint 11 gate 1: three rulings — enum widened, carport priced, venturi reconciled
The corner block's seams, all of them A's to close.

ENUM (D's flag). Anchor.type was a closed JSDoc enum 'house'|'tree'|'post',
so site_02 typed its carport 'post' to fit — quietly enrolling it in the
sail-post family that C's venturi and B's audit both read. Widened to E's own
asset strings (carport, carport_post) and, more to the point, CHECKED: it was
documentation for ten sprints, and documentation cannot fail. validateSite now
rejects an unknown type by name. The ladder is untouched either way — D keyed
it on `work`, the mechanism, which is exactly why that was right.

CARPORT (E's proposal, adopted). $180, with E's reasoning intact: gnome 25, a
night's budget 80, so 180 is 2.25 nights — a good week turned broke and felt
for the rest of the run, without ending a strong one outright. Under ~120 it's
a shrug, over ~250 a silent game over that teaches nothing.

The number lives in site JSON, not the GLB: sites are data, and what a carport
COSTS is this site's economy, not a property of the mesh. E's baked value stays
as the fallback and the proposal.

And it BILLS now. E shipped the trap and said it plainly — the anchors said
collateral "carport" and nothing said what one cost, so nothing scored it. You
could lose the worst steel in the game and pay for a $15 shackle. One broken
corner is enough (the gnome needs two: it needs the sail to land on it; the
carport doesn't need the sail at all), priced per structure so two beams is one
carport gone, not $360. The wreck swaps on the same event — E built it to the
same origin for exactly this.

VENTURI (C's question). There was no disagreement to settle. weather.core
aligns on |dot(wind, axis)| because a gap funnels either way through it, so an
axis is a LINE: -1.08 + PI = 2.0616, and my shipped 2.1 was the same gap read
from the other end. Took C's number because C MEASURED it off storm_03b and I
eyeballed mine at authoring time; the 2.2 deg of my eyeballing was worth
0.44 m/s. gain left at C's 1.35 — that's a balance lever, theirs with B's audit.

Also wired validateSiteWind at site load, C's ask from Sprint 10.

selftest 300/0/0 (was 296).
2026-07-17 17:20:11 +10:00
m3ultra
0f5d69b32d The second site: corner block loads from JSON; night 3 moves to it
SPRINT10 gate 1b. A night is now {storm, site}; the week hands over a yard per
night and night three is the corner block. Nights 1/2/4/5 are the backyard; the
plain-string night form still works and defaults to backyard_01.

site_02_corner_block.json: a smaller 24x16 yard with E's carport as the trap it
was built to be — beam anchors rating_hint 0.22, posts 0.30, both below the
house fascia's 0.35, collateral "carport". The site IS winnable off the honest
anchors (one tree east, four ground posts): I enumerated the honest quads at
authoring time and there are four in-band ones covering the bed, so the carport
is temptation not necessity. Full load audit is Lane B's tools/site_audit.

The carport's beams are keyed work:"bracket" — D's field, the MECHANISM not the
type. A bracket up a bare beam is the one anchor you can't fake from the ground,
so it needs the ladder; keyed on type it would fail open (a carport isn't a
house) and the ladder mechanic would silently vanish, which is exactly the audit
D ran. C's venturi list is in the JSON with two gap coordinates they asked for.

The world is rebuilt on a site change, not re-pointed: createPlayer captures the
world into its ground clamp, collider, ladder and broom (Lane D's modules, no
setters), so a clean rebuild is honest — and a site change only happens at a
forecast, never mid-storm. world.dispose() frees the old yard so nothing leaks.
The re-point of rig + session lives inside loadSiteInto, keyed on the rebuild
itself, after a caller-side version proved fragile.

Verified end to end through real phase transitions: play night 1, hit the
aftermath, and the forecast into night 3 rebuilds into the corner block —
carport present, 10 anchors (cb/cp/q/tr, not h/t/p), smaller bed, player rebuilt,
no scene leak. Then back to the backyard, 12 anchors restored. Selftest 289/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:05:06 +10:00
m3ultra
b98dcad13a Sites are data: extract backyard_01.json; world.js builds from it
SPRINT10 gate 1a, carried since Sprint 8. Everything that was a module constant
— bed rect, house line, tree and post placements, shed, gnome, sun angle, fence
sides — is now data/sites/backyard_01.json, and world.js is a builder.

The extraction is byte-identical, and that's checkable rather than claimable:
E's branch-anchor tripwires pin five positions to 6 dp, a.test's quad-band
asserts pin the whole rigging geometry (>=3 quads in 18-45 m2, full coverage
still >45 m2), D's ladder field and B's balance lines all pass unchanged.
287/0/0.

It's byte-identical by construction, not by luck: the site-derived values keep
the builder's original local names, so ~500 lines of geometry that was already
correct is untouched and only its SOURCE changed.

Two things earned their place:

Anchors carry D's `work: "cloth" | "bracket"` — the MECHANISM, not the type. A
post is tensioned from a cleat at its base and a tree strop is thrown, but a
bracket bolted up a bare wall is the one you cannot fake. Keyed on mechanism
because `type` is a closed enum ('house'|'tree'|'post') that site_02's carport
doesn't fit, and needsLadder keyed on type FAILS OPEN there — the ladder
silently vanishes and you re-rig a 2.6 m bracket standing on the grass.

p4's swept position table moved INTO the JSON's `_why`, beside the coordinate it
justifies. That table is the only thing stopping someone nudging the post toward
the bed and collapsing DESIGN.md's central tension; it belongs with the number,
not in a file the number left.

validateSite() fails loud: sites are content, and a missing bed silently becomes
NaN coverage while a missing anchor list silently becomes an unriggable site.
createWorld now requires a site and says so.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:34:29 +10:00
m3ultra
676b58a2bb Merge Sprint 6 lanes; wire hidden_by_default; document the balance harness dispute
Selftest on merged main: 263 pass / 0 fail (winnable-line assert self-skips
pending SPRINT7 gate 0 harness convergence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:47:29 +10:00
m3ultra
2af4662cd7 Add p4, the one close anchor — the wild night is now winnable (gate 1)
SPRINT6 gate 1's anchor lever, placed by measurement rather than by guess.

The wild night had no winnable line for a geometric reason: every anchor near
the bed (p1/p2/p3) is SOUTH of it, and nothing stands north short of the house
10 m away. So covering rigs had to span the yard and died, while rigs small
enough to survive sat beside the bed rather than over it. p4 supplies the
missing north-west corner.

Its position is swept, not chosen. Against the smallest quad covering 90% of the
bed: (-2.2,-1.2) → 44.4 m², (-3.2,-1.2) → 48.6, (-3.2,-2.0) → 51.7, beyond
z=-4 → no effect at all. Pulling it inward to 44.4 would put full coverage
INSIDE the survivable band and collapse DESIGN.md's central tension — covering
the bed has to cost risk. a.test.js's >45 m² floor is what shouts if anyone
moves it in. With the 8° rake the top anchor lands at (-3.72, -1.40), and full
coverage costs 51.6 m² against 63.5 before: a real new option, 19% cheaper,
still bigger than the 23-38 m² rigs that survive unaided.

Measured through the real $80 shop: t2+p3+p4+t2b, four shackles and a spare
($75), ends hp 58 with 1 corner lost — a WIN, where before every covering rig
lost 2-3 corners and ended at 36%. Hail damage falls 51 → 34 HP versus an
uncovered rig, so decision 13 is visibly doing its job.

Selftest 244/0/0; both yard tensions still green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 09:21:25 +10:00
m3ultra
2b66ce4b9a Give the game a face: HUD, mouse prep, forecast and aftermath (gate 1)
SPRINT4 §Lane A 1-5. Everything the game already simulated was invisible to a
stranger; you can now play a whole round with eyes and a mouse and never open the
console.

hud.js only ever READS — it takes no decisions and owns no state worth keeping,
so it can be this chatty without anything drifting. Corner load bars are
world-anchored rather than screen-anchored because a number in the corner of the
screen can't tell you WHICH shackle is about to go, and that is the entire
"...the shackle, I knew about the shackle" moment. They hold constant screen size
with distance: a house corner is 18 m away from the posts, where a world-sized
bar is a few unreadable pixels.

Prep is Lane B's createRiggingUI wired to the phase machine — their picking was
already complete, this is the hands. Forecast reads the three authored storms, so
the difficulty select came free: Sea Breeze / Southerly Buster / Wild Night with
real peak wind, gust character and change time.

Two bugs the wiring surfaced, both found by playing rather than reading:
- The rigging session survived "play again": a second round started at $35 with
  three corners already hung, and the four anchors you clicked were silently
  ignored because the session was full. resetRig() walks it back through the
  session's own public moves, so the economy stays the one source of truth.
- The forecast card's lifetime was owned by its own button, so any other route
  into prep left a card floating over a live game eating input. The phase owns it
  now.

Verified by playing it: good rig on the wild night wins at 53%, the same yard's
cheap drum-tight span loses 4 corners, the gentle night with a good rig ends at
93% "not a leaf out of place". Selftest 184/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:32:02 +10:00
m3ultra
0cabb19dc7 Anchor rework + house/tree GLBs: give the yard a real choice (decisions 2 & 6)
Posts pulled in to (-4.5,5.5)/(4.0,6.0) with p3 at (0,7), and dress() now swaps
Lane E's house_yardside and both gum trees over the graybox, adopting their baked
anchors rather than my constants (decision 6). E's fascia sits at x=-3..3, not my
guessed -5..5 — narrowing the house span by 4 m is a real part of why small quads
exist at all.

Every anchor now carries E's rating_hint: fascia 0.35 with collateral "gutter"
(they encoded DESIGN.md's "the fascia board is a lie" straight into the asset),
tree branches descending 1.0/0.88/0.76 from fork to thin limb. branch_anchor_01
keeps the t1/t2 ids so nothing referencing them breaks; the rest are added.

The yard went from 7 anchors offering nothing under 110 m² to 11 offering 34
quads in the 18-45 m² band, 8 of which shade a quarter of the bed or more.
Measured through the same storm_02: the big house-to-post span loses its
carabiner at t=3.7 s and cascades to 2/4, while a 37.7 m² tree-to-post rig at
0.85 tension survives all 90 s intact and shades 58% of the bed. That is
DESIGN.md's thesis finally standing up in the yard rather than in a doc.

Lane B's "cascade at t=0.4 s from pre-tension alone" is gone: calm peaks are now
634 N (big) and 200 N (small) against a 1200 N carabiner, and no rig breaks
before the storm starts.

Two asserts pin it, because both halves are easy to lose by accident: at least 3
riggable quads in 18-45 m² must shade the bed, AND full bed coverage must stay
above 45 m² — if a small quad ever covers the whole bed, the rigging puzzle has
no wrong answers left. Selftest 172/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:30:17 +10:00
m3ultra
a5ed4fcdb6 Place the shed and spare table; expose world.shedTable (unblocks Lane D)
SPRINT3 decision 9 / §Lane A.1. Lane D's §7 loop — rig, carry a spare, repair
mid-storm — had nowhere to pick a spare up, so wireYardActions self-skipped the
pickup and nothing in the game could put one in your hands.

world.shedTable.pos is published synchronously from constants even though the
meshes arrive later: createWorld() has to stay sync (a.test.js and the selftest
build a yard with no server), and wireYardActions reads the position at wiring
time. dress() then refines it onto Lane E's baked pickup_anchor — which moved it
5 cm off my guess at where a table top is, so it was worth reading.

dress() is async and separate from createWorld for the same reason: a fetch in
the constructor would make the selftest slow and flaky, or break it. Each load is
guarded individually, so a missing GLB leaves its graybox standing instead of
taking the boot down.

Verified by hand rather than by registration check: walk up, hold E, spare in
hand; a second hold says "hands full"; leaning on the table with E held for 6 s
deals exactly one spare (D's latch works); nothing fires from across the yard.
Selftest 169/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:20:35 +10:00
m3ultra
7f3ef69685 Expose the yard's hemisphere light for skyfx
Lane C's skyfx modulates sun and hemi as the storm builds and hands them back on
dispose() — it doesn't own them. It already had `sun`; `hemi` was private, so the
sky could darken but the sky-bounce fill couldn't follow it down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:38:03 +10:00
m3ultra
a41328a219 Add graybox yard, third-person camera and game shell
The yard's meshes are placeholders; its layout is not. Anchor positions, ground
heights, the garden rect and the sun direction are what the other lanes build
against, so Lane E's GLBs drop into named slots without anyone re-deriving them.

Sun sits in the north (Australian yard), which puts the house's yard-facing wall
in permanent backlight. That's correct, but the fascia line carries three of the
seven anchors, so sky fill is turned up to keep it readable rather than a void.

Camera collision resolves toward the geometry, not away from it: clamping up to
a comfort distance after the raycast is what put the camera 17cm inside the
north wall. Ground is handled by heightAt() instead of a raycast — exact, can't
be tunnelled, and free.

main.js runs a fixed-dt accumulator so sim modules only ever see FIXED_DT. That
is what lets selftest fast-forward a 90s storm and get the numbers the player
got. Nothing auto-runs on import; index.html calls boot().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:33:42 +10:00