3432ea0d1c
22 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f3bf038515 |
Sprint 14 gate 1 close: round-trip pinned, pool items landed, free-carport fixed
Gate 1.5 -- the round trip. a.test drives the editor's OWN placement function (placeEntry, exported for exactly this) through template -> place -> export -> parse -> validateSite -> createWorld -> dress, then asserts the GLB-backed anchors ADOPTED and the carport's 0.22/0.30 came out the far side. Mutation-checked with five deliberate breakages; four went red and ONE DID NOT -- the key-order test shuffled only root keys, every one of which is in KEY_ORDER, so the alphabetical fallback it claimed to protect was never reached. Rewritten around unknown keys, re-mutated, red in both halves. A determinism test that passed while testing nothing. Picking bug, found by dispatching real PointerEvents rather than reading the code: clicking an object's FOOT selected nothing, because the pick handle is a cylinder centred 1.7 m up and from a raised camera the ray has already run ~2 m horizontally by the time it climbs there. pickRef now falls back to nearest-placed-thing within 1.3 m of the ground hit -- camera-angle independent, so it can't rot when the pitch changes. Phantom sail (pool, filed unlanded in S13) -- LANDED and negative-controlled. Teardown factored into disposeSailView() and called from loadSiteInto before refreshCameraSolids (which reads sailView and would re-register a disposed mesh as a camera collider). Rig state goes with the view. Verified through the real game both ways: with the fix removed the cloth still hangs over the corner block, screenshotted. Dev-line (pool) -- two findings, both landed. The line counted `pieces` only, so it read "debris 0" while seven leaves flew; now reports both populations off C's leafCount. And index.html's boot-failure handler wrote into #dev, which is display:none off localhost -- so a failed boot on partly.party showed a stranger a blank page. It has its own visible #fatal element now. E's gap 2, runtime half -- and it was worse than "one click away": nextId namespaces structures s1, s2..., so the FIRST carport the editor places was already free. collateralFor/wreckStructure/isWrecked now resolve through structKey (site collateralKey, then GLB collateral_key, then id last so every existing site is untouched). Negative-controlled: id-equality prices an editor carport to null. E's tie_off:false honoured at both ends. E's swing set and jacaranda are in the palette behind a `requires` gate, so they switch themselves on when the ANCHOR_TYPE widening merges. Also: `return t.skip(msg)` is a fake pass -- Suite.skip pushes a new result and lets the current test record as pass. a.test had six; all converted to the documented 'SKIPPED --' idiom. Selftest 373/0/0, no skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cc9849d538 |
Sprint 13 gate 1.4: the played run is the ruling's centre — canon written, pin labeled sim-chain, THREADS
- Played night 4 on the pinned line through the real UI (D's protocol): hp 55.7 WIN, 2/4 lost, pyrrhic verdict — the ~8 gap to the sim chain is the t=38 crate + ponding landing on p2's 4% margin. Ruled deliberate: the wild night sells no clean line; the pyrrhic win is the intended reading. - Site block records _playedReference; a.test pin re-labeled as the deterministic sim-chain tripwire whose margin is what keeps the PLAYED night over the win line. Band 18-45 ruled an availability floor (B's 1c). - THREADS: full second ruling, night-5 canon, Sprint-14 notes. |
||
|
|
1c0fcc757b |
Sprint 13 gate 1.4 RE-RULED on B's skew finding: the pin measures the game now, and the target is restated
- a.test's separation flight had the phantom 12 s settle B caught (SailRig samples wind at its INTERNAL clock; the storm flew 12 s off the authored curve). Settle deleted — commit->attach->storm from t=0, C's corrected bench pattern; venturi wired from site data so the flight can never assume a yard. - Game-true the line reads 63.6, not 68.3 — FULL (>66) was met by the harness, not the game, and the re-sweep says nothing buyable reaches 66 (the twin-post covering quad tears at 6.9 kN). Target RESTATED to the game's real stakes, with headroom: held >60 and 0/4 (win line 50; measured 63.6), bare <50 (35.7), separation >=25 (27.9). Field names kept — B's gardenfly reads them. - DESIGN CANON in the site block: the wild night's garden is WON SCARRED, never FULL; the week's arc is full/full/full, won-tattered, beyond-saving. - p5 _why table corrected to game-true numbers (first sweep ran ~4 HP hot). |
||
|
|
482803a480 |
Sprint 13 gate 1 RULINGS: p5 the clothesline post; the separation target, amended and PINNED
- backyard_01 gains p5 (5.5,-2) h2.6, honest steel NE of the bed: before it the yard could not beat tattered on the wild night at any price (covering quads need a fascia corner capped at 2.27 kN; the holding line ceilings at 61.3). Swept against the $80 wallet; position/height table in the site JSON. - Separation target adopted AMENDED (ruled on D's funnel-corrected picture): wild night, best buyable line FULL (>66) and WIN; bare bed LOSES (<50). Pinned as site data (backyard_01.separation names the $75 recipe p5=rated + p1/p2/p3=shackle) and FLOWN by a.test through the real chain: shop at START_BUDGET -> commit -> attach -> skyfx -> garden.js. Measured 68.3 FULL / 0 lost vs bare 35.7 (sep 32.6). - Full cover stays unbuyable (covering quads still pull 6.4-6.9 kN), the >45 m2 floor holds, and the ice night still refuses $80 (needs $105 - p1/p2 miss their shackles by 0.05 kN) - night 5 stays bank-funded canon. - New asserts: M reaches the real bus / is only advertised when real; anchor census now 13 (5 posts). Selftest 341/0/0. |
||
|
|
701403ab1d |
Sprint 13 gate 2.5: two camera fixes — spawn framing, aftermath cloth-swallow
SPAWN FRAMING. The QA pass: "a pole dead-centre through the player on every single first impression." Measured, not eyeballed — player spawns at (0,6) looking up the yard, and post p3 stands at (0,7), one metre behind, dead on the view line. spawnYawFor() sweeps outward from the ideal frame (camera opposite the bed, so the player stands in front of what they protect) and takes the first yaw that clears every obstacle. Per YARD, off site data: site_02 spawns elsewhere with its posts elsewhere, so a hand-tuned constant would frame the corner block by luck. Two numbers here are measured and both were wrong first. Clearance 0.6, not 1.1 — my first pass asked 1.1m and produced a WORSE frame than the bug, swinging 105° to stare at a fence, because the greatest distance any obstacle can have from the camera->head line is its own distance from the player, and p3 is only 1m away, so 1.1 was unreachable by construction. maxOff 90° — a frame turned more than a quarter-circle off the bed is no longer a frame of the bed, so the "roomiest" fallback is bounded to the arc. The assert reproduces the 105° from the real yard and pins that the cap brings it to 68°. AFTERMATH CLOTH-SWALLOW. "The dead draped sail can swallow the camera whole." The camera has collided with the house since Sprint 2; the sail was just never in the list. While up it hangs above head height and nothing notices — but a FAILED sail lies in the yard at head height, the one moment you most want to look at it. refreshCameraSolids() adds the cloth to the set, called from both rebuilds (loadSiteInto makes new world.solids, rigSail makes a new cloth; either alone strands the camera on a disposed mesh). Its bounding sphere recomputes every frame, so the raycast reads live geometry. Verified live: camera solids 9 -> 10 the moment a sail rigs, and the cloth is in the set. Added camera.solids getter so that was checkable at all — "did the sail join the set" was otherwise a claim with no seam. And hoisted sailView's declaration with rig/rigging: loadSiteInto's boot-time call reads it before any cloth exists, which is a TDZ throw (page goes blank) that the same block already documents for rig/rigging. selftest 339/0/0 (was 338). |
||
|
|
6e5f4b5222 |
Sprint 13 gate 3: the front door — splash, pause, touch notice, Enter pinned
The URL is public, so this is the first thing a stranger meets.
ENTER, PINNED. The exploit shipped because the rule lived in a keydown closure
inside boot(), behind a canvas and a WebGL context — nowhere a test could
reach. That's WHY it went out: not carelessness, no seam. `enterCommits(phase,
cardOpen)` is a value now, asserted exhaustively over PHASES so a sixth phase
is covered the day it appears, and the safe default (may not advance) is the
one it gets. Mutation-checked: reverting the guard reddens forecast, storm and
aftermath.
SPLASH. A cold visitor used to land on a job sheet — an invoice-shaped card
from a business they'd never heard of, quoting money for a job nobody had
explained, controls revealed later on prep's help line. It's a good SECOND
card. One more .card, not a menu system: there is nothing to configure. Built
on E's letterhead because the masthead IS the game's name. boot({splash:false})
for benches and D's harness.
P PAUSE — and the test is the story. The pause lives in frame(), frame() is
only called by rAF, and rAF DOES NOT FIRE IN A HIDDEN TAB. My first probe drove
the real loop and reported success: simT advanced 0 while paused... and 0 while
running. It was measuring a frozen tab, and it would have passed no matter what
I wrote. So the rule is a value: accumulate(acc, raw, paused). Paused DRAINS
the accumulator rather than carrying it — a kept `acc` spends time that elapsed
while you were paused on the first frame after resume, which is a free sixtieth
of a second of storm and exactly how two runs of a deterministic sim come to
disagree. Storm-only, and cleared at every phase change: a pause flag left set
where P is inert would be a soft-lock built out of a comfort feature.
M MUTE — wired, honest, and not advertised yet. C's bus has no tap: createAudio
holds `master` inside skyfx's closure and exposes only unlockAudio(). Asked in
THREADS. `sky.setMute?.(on)` behind a key the splash promises would be D's
night-3 soft-lock with a keycap on it — `rigging.setWorld?.(world)` sat in this
same file for a sprint doing nothing, silently, because ?.() on a missing method
looks exactly like a call. So the UI asks before it advertises, and M lights up
by itself the day C lands the tap.
TOUCH NOTICE. The naive check is the bug: `(pointer: coarse)` asks what the
PRIMARY pointer is, so a touchscreen laptop answers coarse and gets locked out
of a game it can play — worse than the dead canvas it replaces. `(any-pointer:
fine)` asks whether a mouse exists at all. Detection errs toward letting people
in; unknown browsers get the game, not a lecture.
DEV LINE. Out from under B's anchor panel (both were top-left, typesetting on
each other), and off unless localhost or ?dev=1 — a stranger on partly.party was
reading "60 fps · storm 12.3s · debris 4" on the front door.
selftest 338/0/0 (was 335).
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
3d89ce8fe8 |
Paste E's letterhead/invoice kit into hud.js; rule HARD YARDS + sequential ABN
Gate 3.1: the CUT block from tools/jobsheet verbatim — letterhead, billto, brief, sched, .void, AMOUNT DUE, terms. Docket numbers shared across the pair (JOB/INV 1040+night); week.js nights carry an addr. Cards scroll when taller than the window (safe center + overflow). Both rulings adopted and pinned by DOM asserts; selftest 317/0/0. |
||
|
|
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).
|
||
|
|
6bcc2540cf |
Sprint 11 gate 2: the job sheet — five nights become five jobs
DESIGN.md has opened with this since line 3 — "every callout is a different site, a different client" — and the game had the site and not the client. The forecast card is a job sheet now and the aftermath is an invoice. THE WEEK, once it was asked to name a client, mostly wrote itself: four nights are the same yard, so they're one client on retainer while they're away (the Hendersons, in Cairns from Friday — SPRINT11's own line). Night 3 is a different SITE, so it is necessarily a different CLIENT: a short-notice callout to a corner block you've never rigged, on the night the change comes early. That isn't flavour bolted on, it's what `site: site_02_corner_block` already meant, finally said out loud. BUDGET Y IS THE FEATURE, not the letterhead. DESIGN.md's brief is "client wants X, budget Y, forecast Z" and this card had Z alone. The fee has existed since Sprint 8 and the player has never seen it until the money was already spent — you chose what to rig without knowing what the job was worth, which is a reveal, not a decision. The sheet quotes base + garden + clean up front, and a test pins that a perfect night pays exactly what the sheet advertised: a job sheet that over-promises is the game lying on paper. THE CLEAN BONUS is the new money, and it's the one number in week.js that isn't measured. It gives the invoice three axes that mean three different things — you turned up, their garden lived, you didn't wreck the place — and it's NOT gated on winning, because a lost night where you broke nothing of theirs is a different fact and deserves its own row. It also makes the carport bite twice: 180 + 20 = 200, inside E's own "over ~250 is a silent game over" ceiling. Measured: it adds ~$100 to a competent week (385 -> 485), which makes BROKE_BELOW's known runaway worse. I did NOT pay for it by quietly trimming feeFor or gardenBonusMax — those carry measured evidence, and re-tuning someone else's evidence to fund my own feature is the move this repo keeps catching. If gate 3 says the bank runs away, this is the lever to cut first and it's data. TWO BUGS, BOTH CAUGHT BY LOOKING AT THE CARD, neither by a test: · "THE HENDERSONS" over "The Backyard — Your own place". The blurb and the letterhead called each other liars on screen. The blurb gave way: "your own backyard" is a leftover from when the game had one yard, and DESIGN.md never wanted it. The line's job survives — night 1 is the tutorial because you know this yard, now from having worked it for years rather than owning it. · C's `lead` is a 0..1 haze dial (confidence = 1 − lead), not "nights out". I read it as nights, passed 1, and the sheet advertised "forecast confidence 0%". Now 0.6, because C's own hail rule calls L > 0.55 too distant to promise ice, and tomorrow is exactly that. C: the slot is mine, the number is yours. Lane C's forecastLines(def, lead) finally has a caller above 0 — two sprints after they built it. Lane E: .letterhead / .brief / .jobsheet / .tomorrow are structure only and yours to style. selftest 304/0/0 (was 300). |
||
|
|
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). |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
391722970a |
Rule the pyrrhic win: WIN = hp >= 50, corners priced not gated (SPRINT9 dec 1)
The garden is the client's. The sail is yours. Saving the bed with a rig that tore itself apart is the job done expensively — not a failure. DESIGN.md doesn't call a cascade a loss, it calls it "a firework you paid for", and *paid for* is the point: the aftermath already bills the broken hardware, the collateral, and a week's fee you didn't earn cleanly. Gating the win on corners bills you twice for one night and then lies about it — the same species as the verdict that used to tell a 4/4 hold they'd skimped. B and D arrived here independently with the numbers, and B checked what it does NOT break: cheap rigs still lose (4x carabiner -> hp 38), rigs that miss the bed still lose (hp 36), $80 still cannot buy immunity. C's exhaustive sweep found the only winnable $80 line on the wild night lands exactly here — hp 52, 1 lost. The pyrrhic ending gets its own verdict, and it had to be checked FIRST: `lost >= 2` was unconditionally a cascade-and-a-loss, and under the new rule it can be the best night the game has. Two asserts pin both halves — garden saved + sail destroyed is a win that still names the weakest link; the same wreck WITHOUT the garden is still a plain cascade, because the garden is the whole job. Selftest 276 pass / 0 fail (Lane A 32 -> 34). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
52bdcd0a5e |
The week: five nights, one wallet, and an ending that tells the truth
SPRINT8 gate 1, carried since Sprint 6. week.js is a thin wrapper over the phase machine, not a new system: it answers three questions between rounds — which storm is tonight, how much money is there, and is it over. Zero THREE, pure data, so the ladder and the money are testable at no cost. The forecast card stops being a difficulty picker and becomes NIGHT n OF 5 with the ladder as pips, tonight's storm, and the bank. Aftermath itemises the settlement (fee / bonus / gear recovered / collateral / spent → bank), and E's dawn tint comes up on their 2.2s ease BEFORE the scoreboard — their note is right that you sell the survival first. E's diptych and copy are wired verbatim. Two economy decisions, both made because measuring the week exposed them: A lost night pays a FRACTION of the fee. Zero is unrecoverable and reads as punishment; full pay means failure only costs the broken shackle and the bank only ever climbs. Gear comes back at HALF — a full refund makes hardware free, so the only sink is breakage and "broke = game over" becomes unreachable. Half is also honest: a shackle that rode out a gale has been fatigued, which DESIGN.md already says. And the one that matters: REACHING night five is not SURVIVING it. My first draft said it was, and playing the week found it — a $20-carabiner player loses four gardens, never dips under the broke line because a cheap rig barely costs anything, and got handed "THE WEEK HELD — everything's still where you put it" over four dead gardens. The end card is the last thing this game says; it doesn't get to lie the way the aftermath verdict used to. Words now scale with gardens held: clean (5), scraped (3-4, E's own alternate copy), solvent (0-2, mine — "STILL TRADING · The books balanced. The garden didn't." E never wrote words for that ending because nobody knew the game could produce it). Two economy questions I could NOT settle from a harness and have documented rather than quietly tuned: a competent week runs the bank 80 → 350, so night five stops being a decision; and a floor-scraping week may never actually go broke. My bad-player model handed back a fixed $30 of gear whatever it spent, which flattered the bottom of the curve — the same class of mistake this repo keeps catching me in. Both want a human playing five nights. Selftest 270 pass; the single red is gate 0's balance line, which is B+D's this sprint and was already red on main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
aac1030131 |
Gate 0: name the harness dispute's cause; make skips real (testkit)
THE CAUSE, and it is not a winner: balance.test.js built skyfx with no camera, and skyfx.step() opens `if (!camera) return;` — so the hail shadow grid was never rebuilt across all 5400 calls and every loadout was scored as though it had no sail. hp 36 IS the bare-bed number. Measured with the camera as the only variable: hailShadowOver(bed) peaks 0.000 vs 1.000, hp 36 vs 69. Neither harness was lying; one was flying a yard with no cloth in it. Camera + wind shelters added — the suite must drive what the game drives. MY ERROR, named: the hp-58 win was measured at tension 1.0 and I never declared it. At the shop's default 0.9 my own end-to-end also loses 2 corners. STILL OPEN: this suite says 2 corners lost, my end-to-end says 1 at tension 1.0. Ruled out by measurement, each worth <=0.02 kN: shelters, frozen vs live tree sway, the scripted repair. Next suspects named in the skip: session.commit() vs rigSail(), and main.js passing debris as rig.step's 4th arg (debris ADDS load, so it should break MORE here, not less — that inversion is the thread). And the find that explains how this survived a merge: Suite.test() ignored its return value, so `return "SKIPPED — ..."` was recorded as a PASS. The integrator's storm_02 skip was a fake pass; so was mine for storm_01; `skip: 0` in every report we ever printed was the tell and nobody read it, me included — I misread one of those fakes as my win reproducing. test() now honours the string, and rejects async fns outright (they hand back a Promise that cannot throw synchronously, passing forever while proving nothing). That guard immediately caught its own author: BOTH my wind-router tripwire tests were async and have asserted nothing for two sprints — including the sprint where I reported the tripwire "caught its first real omission". It had, in a hand-run probe, never in the suite. Awaits hoisted into run(); they asserted for the first time today, and pass. Selftest 261 pass / 0 fail / 2 honest skips. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
c3967d98e8 |
Verdicts read the actual failure mode (SPRINT6 gate 1)
The aftermath was lying. Any run under 50 hp read "the rain found what you skimped on" — including Lane B's twisted quad that holds 4/4 corners and skimps on nothing. The verdict is the game's entire feedback channel: DESIGN.md wants every disaster to replay as "…the shackle, I knew about the shackle", and blaming the wrong thing teaches the opposite of the lesson the storm just spent 90 seconds giving. The garden now keeps its damage split by cause, because the caller was pre-summing hail and rain into one number and that sum is exactly the information the verdict needs and can never recover afterwards. A garden that only knows it went 100 → 39 cannot tell a player whether their hardware let go or their perfectly-held sail simply wasn't over the bed — opposite mistakes, opposite fixes. verdictFor() is pure and lifted to module scope so it can be asserted directly. It picks from modes the run can PROVE: cascade (naming the weakest link as going first, not whichever was listed first), single corner, uncovered, rain, broomed, ponded, clean. The integrator's exact case now reads "THE GARDEN IS GONE — and every corner held. The hail fell where your sail wasn't." Aftermath also gains decision 13's headline: "51 HP to hail, 13 to rain". Verified live on the reported scenario, not just in asserts. Selftest 244/0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
eb6986d1a0 |
Tripwire the wind router; take Lane C's gardenExposure helper
The router is a hand-maintained delegation list and it has already cost us once: when Lane C added rainMmPerHour/rainDepthMm, it didn't forward them, and nothing went red — every suite holds a real wind, only the GAME holds the router. Lane B's ponding would have passed every assert it had and done nothing in the yard; the integrator caught it by hand at merge. The class matters more than the instance. Sprint 5's headline system is Lane C's hailAt(), and decision 13 hangs the whole garden score off it — the same silent swallow would make rigging look irrelevant to the garden all over again, which is the exact thing the sprint exists to fix. The assert diffs the router against a real wind and names what's missing. Verified it fires rather than just passing: rebuilt Sprint 4's pre-fix router and it reports exactly rainMmPerHour, rainDepthMm. Garden drain now goes through sky.gardenExposure() — same arithmetic, but it's Lane C's term to own, and decision 13 extends precisely this shape, so hail lands as one added term rather than a rewrite. Numbers unmoved: good rig 53%. Also re-measured Sprint 4's headline finding against a TRUE control (fresh boot, nothing ever rigged, shadow over bed 0.000) because my original control left the previous round's sail in the shadow grid: no-sail is 48.5% vs a good rig's 53%. The finding stands, and decision 13's premise with it. Selftest 209/0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |