- site_03 re-scored on the post-ruling tree, S15 loop, FUNNEL ON: every number
reproduces the S14 card to the digit (81 quads, 11 clean/16 marginal, cheapest
honest $65 h3,t2,t2b,p1 85.3 FULL, bare 83.7, best clean 90.0, sep +6.3,
best-reading 91.8 MARGINAL). Nothing moved; nothing to fix; B's PIN REFUSED
(measured) card renders the finding on this yard.
- NIGHTS is six: gentle, southerly, earlybuster(s02), SOUTHERLY over the SWING
LAWN (new site, survived storm), icenight (flag intact, no longer final),
SOAKER over the CORNER BLOCK (C's measured pairing). Two new briefs: Ruby's
swing set sells the crossbar trap; the Vasilaros return sells the cloth
default the forecast argues against.
- THE STRUCTURAL CONSEQUENCE, filed loudly: six slots cannot hold five old
storms + a survived-storm debut + the soaker. The WILD NIGHT leaves the
rotation (every other candidate is load-bearing); storm file, tests and the
pinned backyard separation all stand. If the eviction is wrong the fix is a
seven-night week — one entry, a ruling.
- week asserts updated (a.test): six nights, final-night=6 both directions on
the broke rule, three clients with night 6 a RETURN booking.
- four six-night-law pins in d.test: one new site / one new storm / never same
night; survived-storm debut; soaker pairing (C's receipt in the message);
beyond-saving night neither new nor final.
- hud surfaces read NIGHTS.length (splash, morning header, end card) — flagged
for A; hardcoded 'five' would lie on every paper.
- selftest 473/0/0 (469 + 4); mutations M1 soaker→backyard, M2 debut→wildnight,
M3 flag→final each RED with the law's own words, restored, green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fabricNoteFor() (rigging.js, pure, headless-tested): one verdict sentence
when the bet provably mattered — cloth on a night whose stones pass the
2mm-weave ruling with real hail damage names the leak; membrane past the
verdict's own 80 kg pond threshold names the trade; silent everywhere a
sentence would be a guess (big stones, dry nights, unknown fabric).
scoreRun carries {fabric, fabricNote}; the invoice grows an unconditional
'fabric flown' row and the verdict prints the note under itself (existing
verdict prose byte-untouched — a.test's verdictMode pins unaffected).
Prep-summary half already shipped in the fresh-eyes review (panel line 2).
Mutations red-then-green: leak gate inverted; pond threshold dropped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
week.js: REP (stars, half-steps, x1+0.04(rep-3) fee multiplier), warrantyItems()
derived from the previous settlement's rig record so quote==settle holds by
construction; rep moves at dawn of the night that earned it and never reads the
garden. main.js: rigRecordFor (exported value — the tally remembers what
repairCorner's cleared flag forgets), night break/repair tally, rep on the
forecast call. hud.js: rep under the ABN on job sheet AND invoice, warranty
lines on both papers worded per spec, fee-row multiplier the night it applies,
rep line under the verdict, end card speaks rep + shows warranty receipts,
C's gate-3.3 forecast print seam (inert until they land). a.test: 8 new tests
incl. the clean-week negative control. Selftest 426/0/0 in the browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FABRIC and setFabric() shipped in SPRINT6 and the sim honoured porosity
all along, but no key, click or HUD line ever reached them — every rig
ever played was DEFAULT_FABRIC. F toggles the two entries, the panel
shows the fabric, the splash teaches it, the blurb announces the switch.
Session-level assert pins the cycle, the $0 price, and the refusal of
unknown ids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
COLLATERAL ON THE CARD. 'Winnable at $80' is an incomplete sentence on a yard
where letting a corner go also bills $180 of carport, so the card now prices
it — through world.collateralFor(), the one resolver (A's structKey fix), never
a second copy of that lookup in a panel. Deduped per STRUCTURE the way main.js
bills ('two beams letting go is one carport gone'): site_02's four carport
anchors read $180 total, not $720. The bill also rides on the margin-flag line
itself, because 'knife-edge' and 'knife-edge AND $180' are different authoring
decisions. An unpriced label renders UNPRICED — not scored, in red, never
'free' — which is the gutter bug's shape and the lie the card exists to prevent.
Verified A's structKey fix rather than assuming it: an editor-shaped carport
(id s1 + collateralKey) prices to $180, a second one (s2) also $180, and the
negative control (id s1, no collateralKey) reads null = not scored. My first
test reported a false alarm because I renamed a SHIPPED spec's id without the
palette's collateralKey — A's editor writes that field explicitly, so the
editor path was never broken.
THE NIGHT-5 INVOICE LINE (pool item, offered S13, accepted). Night 5's garden
is beyond saving by design, and the job sheet was the last surface still
dangling 'up to $45 / paid on what's left of the bed at dawn' — a maximum
nobody can earn, phrased as a rigging problem the player could solve.
NO ECONOMY CHANGE, deliberately. week.js's own comment forbids quote and settle
diverging ('the job sheet promises a number the invoice doesn't pay'), and
zeroing the quote would break that in the opposite direction — settle really
does pay the bonus proportionally. So the NUMBER stays and its CONDITION tells
the truth, which is the job sheet's own idiom:
nights 1-4 'paid on what's left of the bed at dawn' (unchanged)
night 5 'scales with the bed at dawn — and tonight the ice takes it
whatever you rig'
and the ledger names it too, because an unexplained small number next to
'night lost' reads as the player's failure:
night 4 'garden bonus — 18% of the bed +$8'
night 5 'garden bonus — 18% of the bed, beyond saving from the start +$8'
Verified in the running game on all five nights: flag fires on 5 only, every
quoted and paid figure identical (garden $45, total $136, bonus $8).
Selftest 374 passed / 0 failed / 0 skipped.
Four of the QA/D "smalls", one of which was not small.
rig.t never reset between nights: main.js builds one SailRig at boot and
re-attach()es it each night while step() runs in every phase, so the clock ran
on and night 2 sampled its storm from wherever night 1's aftermath left off —
30-50% weaker, past the end of the curve, and offset by however long the invoice
was on screen. A determinism break. Fixed in attach() (zeroes t and _acc);
asserted as "two identical nights, one rig -> byte-equal traces", mutation-checked.
standing-kN preview (D's "$60 blind"): the prep panel shows each corner's
still-air load vs its effective rating, and a summary line, so the tension dial's
static cost is visible before you pay. At 1.4, p4 reads 1.62/1.2 kN !! RIPS AT
REST. session.standingLoads() measures on unbreakable hardware (measuring load,
not rehearsing failure). Cross-checks D's in-play 46m2 number (1.41 kN) exactly.
Panel honesty (D): prep list prints effective rating with the hint shown as its
discount (cb1 0.12/0.3 kN×0.22) — the weak-link arrow now explains itself.
setHardware() throws on non-shop hardware (incl. lookalikes — the shop compares
by identity) instead of silently no-opping; player-reachable failures still
return {ok:false}. balance.test documents the old silent-fail bite.
BLOWN label collision (hud.js — A's file, flagged in THREADS): screen-space
de-collision stacks overlapping corner bars. Verified by projecting cb1/cb2's
real positions through a swept camera — 6/72 viewpoints collide (all ~12m, where
the QA played), thresholds catch exactly those.
Gate 1.2 and the fuse-line stretch stay held on C's geometry fix + A's garden
export. Withdrew my night-1 flag (the card says it's the tutorial night by
design); the ice-night win-line question stands.
selftest 339/0/0 (335 + 4 new).
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).
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.
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.
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).
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).
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>
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>
The model landed in Sprint 6 and nothing ever called it, which is why this kept
getting carried: `forecastFor` existed while the card went on estimating inline.
So this is the wiring, not more API.
`forecastLines(def, lead)` turns a storm into the card's two stat lines, already
worded in A's voice. lead 0 = tonight = exact numbers (reads exactly as the card
always has); further out it hedges into ranges and prints a confidence line. The
bands always contain the truth — a forecast may be vague, never wrong, or a
player who rigs for the top of the stated range gets ambushed.
Two things the card gains beyond the bands:
- The numbers are now MEASURED. The inline estimate (`baseCurve peak + powBase
+ powRamp`) read 30 m/s for storm_02; it really gusts to 32.3, because gust
power is drawn per gust and rides a ramp. The wild night now says 116 km/h
because that is what hits you.
- The card finally MENTIONS HAIL. Hail has been the garden score since decision
13 and the forecast never said so — you were being scored on something the
card didn't tell you was coming. Now: "hail likely" on the wild night, silent
on the gentle one, "possible" when it's too far out to promise.
hud.js is Lane A's file and A is meant to build the week uninterrupted, so this
is deliberately a 3-line swap that keeps their markup, classes and wording — the
week's card rewrite can carry it or drop `forecastLines` and call `forecastFor`
directly. `lead` defaults to 0, so nothing changes until the week passes one.
Verified live at both ends: tonight renders exact; five nights receding into the
week render 0%-confidence hedges ("16–28 m/s · gusts ~72–144 km/h · hail
possible"). Selftest 265/0/0 + gate 0's 2 skips.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>