Commit Graph

18 Commits

Author SHA1 Message Date
type-two
a180fce8c3 WIP Lane E S17 gate 3.1: corroded tier (PREDECESSOR STATE — negative control M1 still applied)
Preserved verbatim from the interrupted session. NOTE: build_sail_post_corroded
still carries the M1 mutation (clean galvanised steel, DROOP=0), so the two GLBs
in this commit are the UNTELLED post, not the shipped asset. Restored in the
next commit after the red-check is witnessed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 01:18:44 +10:00
type-two
b74383722b Lane E S16: sixteen pins for gate 5 — 10 auto scale/node checks on the five new GLBs + 6 named: pergola resolves through the MANIFEST (never a literal) and prices between gutter and swing; the wreck fails partially with byte-close decks; the glasshouse offers NOTHING from three directions and prices as the apex; panes pinned translucent, wreck pinned emptied-not-felled; the pool's 14 denials pinned per-post BY NAME (RULE 1's regex cannot see them — proven, control M3). 434/0/0 in the browser
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:21:57 +10:00
type-two
2a540523ff Lane E S15 gate 1.2: RULE 4 pins — manifest===GLBs both directions (4a), runtime===manifest across all three shipped sites (4b); compared against the manifest, never a literal; both mutation-checked red-then-green (post 1.0->0.9)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:49:38 +10:00
type-two
b9b64604b3 Palette audit + two temptation props: honest extras, in the factory
SPRINT14 gate 3.1. The editor is about to offer these GLBs to an author by
name, which turns every silent field in them into a trap for whoever places
one. Audited the placeable set, fixed the gaps in the factory (nothing
hand-edited), and added the two props D needs to author a yard worth playing.

THE AUDIT, and what it found:

- Four `*_anchor` nodes carried no rating_hint at all — door_anchor,
  pickup_anchor, grip_anchor, window_light_anchor. adoptAnchor does
  `rating_hint ?? 1`, so silence there does not mean "unrated", it means
  RATED PERFECT: a shed door skin outranking a concreted post, conjured out
  of a missing field. They now deny it explicitly (`tie_off: false`).
- The carport's price resolved only by coincidence: collateralFor() matches a
  STRUCTURE whose site id equals the anchors' collateral string, and site_02
  happens to name its structure "carport". The editor will generate
  "carport_2" for the second one, and the trap would have gone free — the
  gutter bug reborn in the sprint meant to bury it. The GLB now carries
  `collateral_key`, like the house does for "gutter". Lane A: the runtime
  half is yours (collateralFor could fall back to it).
- The trampoline is unpriced, and now says so rather than omitting it.

Three new rules make the audit permanent, and each is proven able to fail:
no silent anchors, no collateral string nobody prices, no baked anchor_type
outside the checked ANCHOR_TYPE enum. Rule 2 found the carport gap on its
first run — an unfaked negative control.

THE PROPS:

- swing_set_01 (+ wrecked) — the palette's honest middle. It had a ceiling
  (gum fork 1.0) and a trap (carport beam 0.22) and nothing in between. Two
  apex anchors at 0.45, typed `swing_frame` (NOT `post`: the type string is
  what the player reads before committing, and "post" promises concrete),
  and a crossbar that is the most anchor-looking object in the game and is
  not one. Priced at $140 — proposal, reasoning baked beside it, A rules the
  number: gnome 25 < gutter 90 < swing 140 < carport 180.
- tree_jacaranda_01 — a second species whose LADDER is the feature.
  Gum: 1.00/0.88/0.76, climb freely, pay 24%.
  Jacaranda: 0.95/0.52/0.40, climb at all, pay 58%.
  That makes tree choice a real decision instead of "is there a tree".
  Unpriced by ruling, same as the bike: no limb-failure event exists to see.

Both wreck/orientation claims are measured in browser coords, not reasoned —
`wreck_falls_toward: "+Z"` is baked AND asserted against the mesh.

Selftest 376/0/0. Two consecutive full factory runs: 44 files byte-identical.
2026-07-18 16:01:27 +10:00
type-two
b7f065cd57 e.test: pin the gutter price band, the DOWN/UP swap truth, and anchor absence in the wreck
Five new asserts (Lane E 67 -> 72; selftest 315 -> 320):
- price exists, sits between gnome and carport, keyed to the anchors'
  collateral string, wreck carries the same bill and names its twin
- the torn gutter is DOWN in the wreck and UP in the intact house — negative
  control run: rebuilt with the ground run at eave height, selftest went red
  ('gutter_down tops out at y=2.68', 1 failed / 319 passed) while the Blender
  verify PASSed the same bad file (plan position unmoved, no box span changed);
  restored build round-tripped to identical hashes
- wreck height equals house height (a house does not fall when its gutter does)
- no fascia_anchor_* survives into the wreck
- house pair added to the shared ground-plane test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:21:40 +10:00
type-two
8ec01028f9 Lane E, Sprint 11: client juice — job sheet, invoice, and the kid's bike
The letterhead is the point: one business, two documents, same masthead, so
the job sheet and the invoice read as a pair of papers off the same outfit's
printer rather than two game screens. DESIGN.md has opened with "you run a
small landscaping outfit" since day one and it has never been on the glass.

tools/jobsheet/ — paste-ready for Lane A between CUT markers, purely additive
(.card/.row/.ledger untouched), rendered to four PNGs before handover, each
state addressable as ?v=. The ledger already WAS an invoice; it just never
said so. The line worth keeping is .void: a bonus that was on the job sheet
and didn't survive the night, struck through in the same column as the money
that did — a bonus you never watch fail is one you never learn to protect.

Two design calls left as A's with the reasoning beside them, same as the
carport's $180: the business name (proposal: HARD YARDS) and the ABN
(sequential digits on purpose — a plausible one could be a real business's).

bike_kid_01.glb — 1.15 x 0.33 x 0.74 m, 700 tris, lean baked in because a
bike doesn't stand up on its own. Two bugs caught by LOOKING at the render,
neither catchable by the existing suite: a comment claiming a step-through
above a diamond frame, and a fork with negative rake that drew head tube and
fork as one pole through the front wheel. Both passed the Blender verify —
a bounding box can't answer "is this the right shape".

The lean is an export-time axis fact (+Y in Blender is -Z in three.js), so
e.test.js now pins it in browser coords. Proved the assert fails: rebuilt at
lean=0 and Blender still said PASS while the selftest went red.

selftest 299/0/0. 32/32 GLBs byte-identical across three full factory runs.
2026-07-17 17:39:58 +10:00
m3ultra
0cee198702 Price the carport trap and add its wreckage
The carport anchors have said collateral="carport" since Sprint 9, but the root
carried no collateral_value — so main.js, which reads world.gnome.collateralValue
for the gnome, had no number to reach for. A trap with no price is a warning
label. The root now carries it in the same shape and place, pinned above the
gnome's 25, with the proposed 180 and its reasoning as a named constant so Lane A
can argue with the number rather than the asset.

carport_01_wrecked is the payoff, on the intact one's origin for a mesh-for-mesh
swap: the pads stay (they were never the weak part), the frame racks over like a
parallelogram, the roof peels off downwind in one piece.

Two bugs found by measuring rather than reasoning, both now comments where the
numbers are: the beams were rotated when a beam spans the axis the frame racks
in, so it rides level — tilting it made the wreck taller than the carport; and
the roof sheets dipped below ground, which is what inflated the height. Asserts
now pin the wreck shorter than its twin and sitting on the grass.

291/0/0, Lane E 64 asserts, 40/40 output files byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:30:37 +10:00
m3ultra
4d4f2d7d7f Add carport_01 for site_02, and the pyrrhic end card
carport_01 is site_02's dressing and its whole design. DESIGN.md wants the
corner block anchor-poor, and the interesting way to build that isn't to give
the yard nothing — an empty yard is just a smaller yard. It's to give it
something that looks like four free anchors and isn't. Light posts on small
pads, a beam sized for one sheet of roofing: tie 25 m2 to it in a southerly and
you take the carport, not the shackle. Ships with the worst ratings in the game
(beam 0.22 vs the fascia's 0.35) and collateral="carport", pinned in e.test.js
below the fascia with the reason attached, so nobody quietly fixes them upward
and turns the site back into a small yard.

The pyrrhic card is the third outcome on the same camera: dawn, bed green, gnome
standing, sail post down across the yard behind him. Warm light on purpose — a
pyrrhic result is a win, and the wreckage is the price, not the verdict. Copy
goes to Lane A in THREADS; cardpreview renders all three.

277/0/0, Lane E 60 asserts, 39/39 output files byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:29:08 +10:00
m3ultra
f8710b56bc Sweep and taper the tree limbs; pin the anchors that ride them
Each limb was one straight uniform tube — a peg poking out of a pole, and the
most visible art problem in docs/yard_day.jpg. They now leave the trunk thick,
sweep up on a bezier and taper to nothing, which is what a gum does. +96 tris on
gum_01, silhouette bounds unchanged.

Nothing physical moved. The tip is a bezier endpoint and the four rng draws per
branch are the same four in the same order, so every branch_anchor_* is
bit-for-bit where it was — verified before and after, all five identical to 6dp.
Lane A's winning line rigs off t2 and is untouched; no balance number can shift.

e.test.js now hard-codes those five world positions as a tripwire, so no future
art change of mine can quietly invalidate a sprint of someone else's physics. If
it goes red, the art moved an anchor — fix the art, not the numbers.

Card text and the dawn-tint treatment go to Lane A in THREADS. The tint is a CSS
snippet rather than a ninth texture: only two of my eight are consumed, and a
full-screen gradient needs no asset at all.

Selftest 262/0/0, 38/38 output files byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:25:03 +10:00
m3ultra
08a1d95903 Add night dressing and end cards; fix the hide_render bug
hide_render does not survive glTF export — verified in three r175, every node
arrives visible:true. So garden_bed has been drawing plants_full, tattered AND
dead superimposed since Sprint 1, and my THREADS note told Lane A the opposite.
The asserts missed it by checking the nodes existed rather than that only one
was on. The flag now rides in extras, which does export, and consumers apply it
in one line; e.test.js pins the flag instead of the node.

Night dressing: house_yardside gains window_glow, an unlit emissive pane marked
hidden_by_default, plus window_light_anchor. A warm window means someone is
inside, which is why the player is out in the dark keeping the sail on. Plus a
moon sprite whose halo, not its disc, is the part that survives cloud.

End cards are rendered from the game's own props as a diptych: identical camera
and yard, only the night's outcome differs. The left third is left empty for
Lane A's text.

Cards are opt-in behind --cards because EEVEE's shadow pass is not
byte-reproducible across processes in 5.1 — proven with a minimal repro, not
PYTHONHASHSEED, and present even with a hard sun. The dawn shadow is the art, so
the cards become committed art rather than build output. Everything the game
loads stays byte-identical: 38/38 verified.

Selftest 241/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 09:28:26 +10:00
m3ultra
f2527055a5 Add hail juice, plant shred, and a way to screenshot the game
Hail (SPRINT5 §E-1) ships two ways because Lane C's rain is a BoxGeometry with a
flat material and skyfx loads no external texture at all: hail_stone_01 as
geometry that drops into their existing InstancedMesh pattern, and hail_pips.png
as a 4-cell atlas for impacts. A flat quad can't be round and an impact is round
— that's the only reason the atlas is a texture. Both come with a copy-paste
recipe, because a grep showed exactly one of my five shipped textures is
consumed: sail_weave, the one I wrote a recipe for. grass_atlas has sat
unreferenced for four sprints.

plant_shred (§E-2) is elongated after the first pass came out radial and read as
green potatoes — it's the long axis plus the midrib that says "leaf".

tools/yardshot/ finally lands the DESIGN.md pictures, carried since Sprint 2. It
does NOT touch server.py or main.js: it's a Lane E stdlib server that serves the
repo and takes POST /shot, so the browser posts a Blob and bytes go straight to
disk instead of base64 through a console. do_POST is ~25 lines and Lane A is
welcome to lift it and delete this.

Three traps found on the way, all documented: toBlob is async so the WebGL buffer
is already cleared (toDataURL is sync); a backgrounded tab lays the canvas out at
0x0 and toDataURL then returns "data:,"; rAF is paused there so step() must be
driven by hand.

Selftest 209/0/0, 36 output files byte-identical across two runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:03:54 +10:00
m3ultra
4bb0efcfd1 Add the pond visual, the broom, and rename the snapped panel
Pond (SPRINT4 §E-1, decision 10): pond_water.png and pond_normal.png, for a
patch Lane B builds from the cloth's own nodes — same ride-the-nodes rule as the
tears, since a rigid disc would sit still while the belly moves under it. Alpha
is a radial feather and RGB darkens toward the centre, both encoded radially so
the shading survives being scaled per pondMass (radius ~ sqrt(mass)). Water is
grey-green, not blue: rain caught in a sail is shallow, murky and mirrors an
overcast sky.

Both at 256², not 512²: smooth low-frequency content, and at 512 they were
256 KB + 320 KB against ~20 KB for every other texture here. The chop is seven
waves at golden-angle directions rather than three crossed sines, which
interfered into a lattice and read as basketweave; the normal map gets six
integer-frequency waves for the same reason, and still asserts its own tiling.

broom_01 (§E-2): poke_tip sits on the bristle end deliberately — a broomstick
jabbed at a loaded sail holes it.

Renames fence_panel_broken to fence_panel_snapped to match SPRINT4 §A-4/§E-3.
Lane A's dress() guards every load, so the mismatch would not have crashed —
the wreckage would silently never have appeared, which is worse.

All 21 assets pass, 33 output files byte-identical across two runs, 186/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:27:55 +10:00
m3ultra
fccbeb9319 Add aftermath wreckage props and verify glTF extras survive
The aftermath screen needs something to point at (SPRINT3 §Lane E-2).
garden_gnome_01_broken snaps at the ankles with the base left where the player
last saw him, head rolled clear with the beard still on, hat off — recognisably
him rather than a shattered pile, because wreckage nobody can identify is just
gravel. fence_panel_broken loses a few palings and hangs one off a nail; most of
it stays standing, which is what makes the hole read as damage rather than as a
design choice. Both keep their intact twin's origin and ground plane so Lane A
swaps mesh-for-mesh with no offsets, asserted both ways.

Also asserts something claimed since Sprint 1 and never checked: that glTF
extras reach three's userData. They do — the gnome's collateral_value, the
canopy's sway_amp, branch_anchor rating_hint and the bin's mass_hint all arrive
as numbers. Had that silently dropped, Lane A's gnome would score $0 and every
anchor would rate identical, which reads as a gameplay decision rather than a
missing field.

Contact-sheet framing now keys the capsule off height, not max(dims): the broken
gnome is 0.39 m across but stands 0.11, so spread-based framing buried it the
same way it once buried the shackle.

Selftest 175/0/0, 30 output files byte-identical across two runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:24:19 +10:00
m3ultra
0f5f4d8368 Make rake_pivot a real pivot; log Sprint 2 in THREADS
rake_pivot shipped as a childless empty, so rotating it moved nothing — and
rotating the whole GLB instead, which is the only alternative, tips the concrete
footing out of the ground along with the post. DESIGN.md makes raking away from
the load a player decision, so the handle has to actually work.

It now holds post + pad_eye + top_anchor, with footing left on the root: rake it
8° and the post leans while the concrete stays planted. Asserted both directions
in e.test.js — the head must travel over 0.3 m and the footing under 0.01.

Same class as the canopy bug, found the same way: by driving the handle in a
test instead of eyeballing the model. Exported dims unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:56:15 +10:00
m3ultra
d3e849fcc3 Add sail cloth textures and the storm dressing set
Textures (SPRINT2 §Lane E-2). sail_weave.png is a seamless 512² knitted
shade-cloth weave with the stripe banding real cloth has; the script proves the
wrap by evaluating a second tile and requiring an exact match, because Lane B is
being told to set RepeatWrapping and a bad wrap would seam across the whole
sail. Luminance rides in a narrow band so it multiplies the base colour instead
of replacing it — high contrast reads as burlap, not HDPE. Dropped the per-pixel
noise: invisible at ±0.012 and incompressible, it cost 305 KB of the 323.

sail_tears.png is a strip of 4 escalating rips. Each is a lens, not a slit —
fabric under tension parts widest in the middle and tapers to a point, and a
constant-width gap reads as a drawn line. Threads bridge the gap, scaled to the
local width; without them a dark lens is a hole rather than a tear.

Dressing (§Lane E-3): wheelie_bin_01 (240 L, 12 kg, `lid` on its own node so it
flaps before the bin goes over), washing_line_01 (a Hills Hoist — the `head`
freewheels, giving a second wind tell at head height), garden_gnome_01
(collateral bait: a smashed gnome reads where a damage number doesn't).

Bin and tramp land in models/debris/ for Lane C to glob. All 19 assets pass, 28
output files byte-identical across two runs, selftest 129/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:51:51 +10:00
m3ultra
7cd25a5d2a Give the gum trees a canopy sway handle
world.js sways a tree by rotating a `canopy` group whose origin sits at the
trunk top, so the blobs swing about the trunk. The trees shipped canopy_01..03
as siblings of `trunk` with each origin at its own blob centre — rotating one
spins a sphere in place, which is visually nothing. Lane A could not have swayed
these trees, and the canopy lean is the gust telegraph the player reads a beat
before it hits the sail, so the tell would have gone missing rather than looked
wrong.

Adds the `canopy` empty at the trunk top with the blobs parented under it, so
A's existing code works unchanged, plus sway_amp / sway_phase / sway_pivot_y
for the per-tree tuning SPRINT2 §Lane E asks for (gum_01 is big and leans less
at 0.85; gum_02 is whippy at 1.20 and shows a gust first).

sway_phase draws from its own RNG stream: taking it from the shared one would
consume a value and shift every blob draw after it, resilhouetting a tree the
other lanes have already tuned against. Exported dims are identical to Sprint 1.

e.test.js asserts the pivot by rotating the handle and measuring that a blob
actually travels — red before this change, green after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:42:30 +10:00
m3ultra
302972cc6e Fill in Lane E selftest suite: assets verified in three.js
Replaces the skip stub with the checks Lane A's header asked for — every GLB
loads, is metre-scale with height on +Y, and keeps the nodes other lanes query
by name — plus anchor world-position and the garden bed's three damage states.

This catches what the Blender side structurally cannot: Blender exports
Z-up->Y-up and imports Y-up->Z-up, so a broken export_yup round-trips green.
A native glTF reader is the only thing that can prove the file.

GLTFLoader is imported dynamically on purpose. Three.js addons import the bare
specifier `three`, no page in the repo has an importmap yet, and selftest.html
turns an un-importable lane module into a hard FAIL — so a static import would
redden Lane A's merge gate over a harness gap rather than a real defect. It
skips with the fix instead, and lights up by itself once the importmap lands.
Verified behind a temporary probe first: 36/36 pass. Need logged in THREADS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:50:43 +10:00
m3ultra
1049dece24 Add selftest harness with one suite per lane
selftest.html only imports; each lane owns js/tests/<letter>.test.js. Five lanes
sharing one selftest.html would be the single guaranteed merge conflict in the
repo, so the stubs are pre-created with each lane's PLAN3D asserts written into
the header.

No rAF anywhere: it's throttled in a hidden tab, so a rAF-driven selftest would
pass only while you watch it. fixedLoop() drives time instead.

Lane A's suite covers the §5-A acceptance criteria, including "camera never
clips through house" — stated as the underlying invariant (no solid between the
player's head and the camera) so it survives Lane E swapping the house GLB.

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