Compare commits

..

2 Commits

Author SHA1 Message Date
type-two
f53a0d5ee9 Lane D S16: [D] THREADS — gate 4.1/4.2 wiring + eviction filing, gate 4.3 cold-play verdicts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:23:11 +10:00
type-two
d8e7087c2c Lane D S16 gate 4.1+4.2: swing lawn re-scored (zero deltas), THE SIX-NIGHT WEEK wired
- 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>
2026-07-20 21:49:58 +10:00
5 changed files with 336 additions and 52 deletions

View File

@ -8021,3 +8021,150 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
before the sep-judged assert landed (its green is in the 445 merged run; its red was proven
by hiding site_03's finding — "site_03_swing_lawn has neither a separation block nor a
_separation_finding", restored, green). Pushed lane/b.
[D] 2026-07-20 — 🗓 **GATE 4.1 + 4.2 — RE-SCORE ZERO-DELTA, THE SIX-NIGHT WEEK IS WIRED, AND THE
ARITHMETIC EVICTED THE WILD NIGHT. John, A: the eviction is the entry's headline and it is
yours to overturn — one line puts it back as a seventh night.** Selftest 473/0/0 (469 + my 4
law pins), three mutations red-then-green, everything below verified in the browser on :8827.
**4.1 — the swing lawn re-score, S15 loop, post-ruling tree, FUNNEL ON:** every number on the
S14 card reproduced TO THE DIGIT — winnable at $80 · 81 quads · 11 clean / 16 marginal ·
cheapest honest h3,t2,t2b,p1 $65 (holds $55) · 22 m² · 85.3 FULL · bare 83.7 → best clean
h3,t1c,t2,p1 90.0, sep +6.3 · best-reading t1c,t2,t2b,s1_f2 still MARGINAL at 91.8 (the trap
line, still flagged as such). B predicted zero movement from their gate-2.3 probe and the
prediction held — nothing to fix, nothing retuned. B's PIN REFUSED (measured) card renders the
site's _separation_finding correctly on this yard; the finding block is carried, not touched.
**4.2 — the ladder as wired (week.js, D's edits by the sprint's explicit grant):**
· N1 gentle × backyard (Hendersons) — unchanged
· N2 southerly × backyard (Hendersons) — unchanged
· N3 earlybuster × site_02 (the Vasilaros place) — unchanged
· **N4 southerly × site_03_swing_lawn (the Delaneys)** — THE NEW SITE, under Tuesday's storm:
at its debut the yard is the only new variable, C's "southerly or buster" taken at southerly
because every measured number on the yard (S14 card, my 4.1 re-score, B's probe) is a
southerly number.
· N5 icenight × backyard (Hendersons, gardenBeyondSaving intact) — NO LONGER FINAL, which
quietly closes the gap A filed at gate-1 landing: a night-5 dawn break now has a morning to
book its warranty on. Brief reworded ("The most wind of the week and golf-ball ice") because
"less wind than last night" compared against the wild night that no longer precedes it.
· **N6 storm_06_soaker × site_02 (the Vasilaros place, a RETURN booking)** — THE NEW STORM,
over the yard night 3 taught, C's measured pairing honoured. Final night, so
going-broke-never-on-the-final-night now guards the fabric-bet night.
**The two new briefs** (client flavour is data): Ruby Delaney's dad sells the crossbar trap in
his own voice ("bolted right through, tie to that and save yourself a post" — the crossbar is
NOT an anchor and the apexes are 0.45 steel billing $140), and the bed has its reason ("her
school project, so it comes home alive"). The Vasilaros return sells the WRONG DEFAULT the way
"plenty to tie off to" sold the carport: "the kind of night you could rig in your sleep.
Whatever you flew last time will do, they said." — last time was cloth; the forecast's own
lines (C's stones/rainRate) are what argue back.
**⚠️ THE EVICTION, said before anyone finds it in a diff:** six slots cannot hold seven
nights. Five shipped storms + the soaker = six storms; the swing lawn's debut must REPEAT an
already-survived storm (gate 4.2's own words, and C read it the same way); so exactly one
shipped storm leaves the week. Every other candidate is load-bearing — gentle (tutorial +
calm-day preload), southerly (hail intro + the buster's "same as Tuesday" referent + the swing
lawn's debut partner), earlybuster (site_02's debut, which the soaker pairing needs), icenight
(the flag, canon). **The WILD NIGHT is out of the rotation.** Its storm file, its tests, C's
forecast word map, B's membrane table and the pinned backyard separation all stand — it is
un-flown by NIGHTS, not deleted. What the week loses: its 30-gust apex and the one shipped
night where a cloth rig buys real garden (+28.1 HP, B's table). What it gains: the soaker as
the new teeth night (bare 17.5 DEAD → membrane 96.5 FULL — a Δ79 the wild night never
offered). If the trade is wrong, the fix is a SEVEN-night week (arc 2's own range is
"five-to-seven") — one NIGHTS entry re-added, a ruling, not a refactor. **Gate 6 material.**
· The week asserts moved exactly as the sprint predicted (a.test: six nights, final-night
walk, three clients with N6 pinned as a RETURN of N3's client, broke-on-5 = gameover both
directions). Four new SIX-NIGHT LAW pins live in d.test next to their owner: one new site /
one new storm / never the same night · new-site debut only under an earlier night's storm ·
soaker only over site_02 with C's receipt in the failure message · the beyond-saving night
neither new nor final. Mutations, each red with the law's own words, restored: soaker →
backyard (2 red), debut → wildnight (2 red), flag → final night (2 red).
· **A, your files, my edits, flagged:** hud.js now reads NIGHTS.length for the splash
("Six nights, six jobs"), the morning header (NIGHT n OF 6) and the end card (n/6 rows,
count-worded copy) — hardcoded "five" would have been the paper lying on every surface;
word map falls back to digits for an unmapped length. a.test's week suite updated as above.
Revert-and-tell-me standard applies.
· **A, one observation, not a change:** gradeFor(5)='clean' now lands on the six-night week's
BEST POSSIBLE outcome (5 won + the designed loss), so THE WEEK HELD is finally reachable —
played it tonight, it reads earned. That is thresholds coinciding, not design: a seven-night
week would need gradeFor re-ruled in the same commit.
[D] 2026-07-20 — 🎮 **GATE 4.3 — THE COLD-PLAY: SIX NIGHTS, ONE WALLET, REAL UI, AND A VERDICT
FOR EVERY GATE. Short version: TEETH works — the ledger reads FAIR at every surface it
touches, the fabric bet is legible from the paper, and the week tells a story it could not
tell five nights ago.** Wallet trace: $80 → 174 → 242 → 311 → 366 → 361 → **$419**, end card
THE WEEK HELD, 6/6 nights, 5/6 gardens, ★3.0, 2 warranty jobs booked. Storms fast-forwarded
with the game's own fixed-dt step (the disclosed gait); every card read at the moment it was
live; night 6's broom run driven through the real interact chain (real key events, real
holds).
**GATE 1 (A) — the ledger: FAIR, and twice it was fair about ME.**
· N1: my $5 carabiner on P5 let go ON THE SEA BREEZE (peak wind ~11 m/s beats 1200 N steel on
the windward corner — the tutorial's "nothing can hurt you" is true of the GARDEN, not of a
$5 corner; that reads as the shop teaching, not a lie, but noting it for the record). I rode
it out broken: ★3.0 → ★2.5 at dawn with the reason printed, and next morning's sheet read
"warranty — P5, carabiner, no charge to the client · $5 · your corner, down at dawn — 14
Kurrajong St". I watched that corner hang all night; the line felt like a receipt, not a
fine. **Warranty callout on your sheet: FAIR.**
· N3→N4 is the ruling working cross-client: Q2's shackle (corner block, pond corner, let go
late — C called that corner in their gate-3.1 entry) booked on the DELANEYS' sheet with the
Vasilaros address on the line. The van detouring on the way to the job reads exactly as the
ruling intended. quote==settle held every morning it was tested, deduction on both papers.
· **Rep moving pay: EARNED.** ×0.98 on the fee row the mornings after my two dawn breaks,
×1.02 on night 6 after the icenight's clean +0.5 — the multiplier only ever appeared WITH
its reason line, and the swing (a few dollars) is felt as reputation, not as balance. The
night-5 pin is the one that matters and it held IN PLAY: garden 38%, night lost, rep UP
(★3.0 → ★3.5, "clean night") with the verdict crediting the steel — "Yours held anyway.
That was the whole job tonight, and you did it." A designed loss that pays respect instead
of docking it. **Gate 1: PASS, no retune asked.**
**GATE 2 (B) — the heal, the paper trail, the pin refusals.**
· fabricNote: fired FIRST on night 4 of all places — cloth on the southerly with 9 HP of fine
hail through: "stones this fine rattle through the weave... The membrane stops every stone."
Honest per its thresholds (B's table says membrane was RIGHT on the southerly by +1.7), and
pedagogically it is the whole reason night 6 was legible — see gate 3. N6's membrane note
("held 391 kg of what it stopped. That is the membrane's price") lands the trade. Silent on
the nights it would have been a guess. **The line fires when the bet mattered: PASS.**
· The heal: six honest nights produced ZERO divergences — that datum first. I then poisoned a
mid-cloth node on a scratch night (disclosed poke, B's own gait): the NaN cascade broke all
four corners inside a step, the sail vanished clean from the render — reads as "the sail's
gone", a break, NOT a glitch; garden HUD flipped to bare-bed honestly. **One edge for B's
file, observed not alarmed:** with all four corners already divergence-broken, a SECOND
poison has no unbroken corner left to arm the heal (_checkFailure skips broken corners), so
a fully-lost sail that diverges again stays NaN and renders as nothing. Player-facing
surface is acceptable (a lost sail LOOKING lost); noting it because your "NaN pond second
wire" comment lives one door down and the day a free sheet can re-diverge, the heal has no
trigger there. Your in-suite poison assert (green in my 473) covers the partial case that
actually matters.
· The pin refusals: B's PIN REFUSED (measured) card on both my yards prints the finding where
the author will look. Correct surface, nothing to add.
**GATE 3 (C) — storm_06 and the forecast: THE BET IS LEGIBLE, and here is the receipt as a
played sequence.** N6's sheet: "sustained to 9 m/s · gusts ~55 · rain heavy · FINE PEA STONES
· rain to 44 mm/hr" over a brief telling me to fly what I flew last time. Cold read at the
sheet: mildest wind since night 1 + fine stones + heavy rain, and night 4's invoice had
already taught what fine stones do to a weave → pressed F before spending a dollar. Membrane
+ C's $75 rated-q1 ring: garden 96% where every measured cloth line on the night reads DEAD.
**The forecast sells the bet — BUT the chain matters: "fine pea stones" carries the answer
because B's fabricnote taught "fine = through the weave" two nights earlier. Forecast words +
paper trail TOGETHER are the legibility; either alone is a puzzle. In the shipped order the
chain works. PASS.**
· C's q2 call was surgical: I broomed the pond LATE (t≈65 on, real broom, real holds) and q2
let go at ~t80 anyway — the broom's job on that corner starts when the rain does, not when
the belly shows. The corner-block pond punishing the same corner two different nights (N3
cloth, N6 membrane) is the yard having a personality; keep it.
· Final-night dawn breaks: q2 down at dawn on N6 cost the rep (★3.5 → ★3.0, printed) and
booked NOTHING — no morning left. A filed this at gate-1 landing as arc-2's warranty-callout
seed; the six-night order moved the beyond-saving night off the final slot, so the only
breaks that can now vanish from the ledger are night-SIX breaks. Same filing, smaller hole,
arc 2 closes it.
**THE ONE SURFACE QUESTION TO FILE (C predicted it, and it is real): SCORE IT on the soaker.**
Flew it over site_02 post-play: the card says **"✓ WINNABLE — at $80"** and then shows a
garden table where EVERY line is DEAD (cheapest honest 19.1, best clean 32, bare 17.5) — and
NOTHING on the card says the sweep is cloth-fixed. The numbers are honest and C's charter is
sound; the SILENCE is the problem: on the one night where fabric is the whole answer, a
reader without the charter in their head concludes "winnable rig, unwinnable garden — the
night is broken." **Ask for A/B (one line, not a re-architecture): the score header should
name its fabric the way it names its funnel state — "fabric · shade cloth (sweep charter)" —
and on a storm whose stones pass the weave ruling, one footnote line: "membrane not swept —
the F key is not in this card." Who owns the line is yours to settle (B's card, A's page).**
Filed as a surface question, not a bug, exactly per C's coda.
Pushed lane/d: d8e7087 (the wiring), this entry and its sibling above. Port 8827, tab kept
fronted throughout; B's iterative score yielded correctly on every card this sprint.

View File

@ -19,7 +19,15 @@ import { forecastLines, leadFor } from './weather.js';
// SPRINT16 — the end card compares the final rep against neutral. A constant,
// not state: the HUD still only READS. week.js has no DOM and no THREE, so
// this import costs nothing it didn't already cost main.js.
import { REP } from './week.js';
import { NIGHTS, REP } from './week.js';
// SPRINT16 gate 4 (Lane D, flagged for A): the week is data — NIGHTS.length —
// and these surfaces used to hardcode "five". A six-night week whose splash,
// morning header and end card still said 5 would be the paper lying, so every
// count below reads the ladder. The word map keeps the splash's prose voice;
// an unmapped length falls back to the digit, which is honest if inelegant.
const NIGHT_COUNT = NIGHTS.length;
const NIGHT_WORD = { 5: 'Five', 6: 'Six', 7: 'Seven' }[NIGHT_COUNT] ?? String(NIGHT_COUNT);
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
const kmh = (ms) => ms * 3.6;
@ -533,7 +541,7 @@ export function createHud(d) {
</div>
</div>
<p class="premise">Rig shade sails, then keep the client's garden alive
through the storm. Five nights, five jobs, one wallet.</p>
through the storm. ${NIGHT_WORD} nights, ${NIGHT_WORD.toLowerCase()} jobs, one wallet.</p>
<div class="sect">THE CONTROLS</div>
<div class="keys">
${[
@ -921,9 +929,10 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
// scraped one, and a third for the ending nobody knew existed until the
// economy was measured: solvent, with the gardens dead behind you.
const COPY = {
clean: ['THE WEEK HELD', "Five nights. Everything's still where you put it.",
clean: ['THE WEEK HELD', `${NIGHT_WORD} nights. Everything's still where you put it.`,
"Nobody thanks you for the storm that did nothing. That's the job."],
scraped: ['FIVE NIGHTS, FIVE MORNINGS', 'You made the least wrong call five times running.',
scraped: [`${NIGHT_WORD.toUpperCase()} NIGHTS, ${NIGHT_WORD.toUpperCase()} MORNINGS`,
`You made the least wrong call ${NIGHT_WORD.toLowerCase()} times running.`,
'Some of it held. You know which bits didn\'t.'],
solvent: ['STILL TRADING', 'The books balanced. The garden didn\'t.',
'You can be paid all week and still have nothing to point at.'],
@ -953,8 +962,8 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
<div class="endtext">
<h1>${head}</h1>
<h2>${sub}</h2>
<div class="row"><span>nights survived</span><b>${nights}/5</b></div>
<div class="row"><span>gardens held</span><b>${s.held ?? 0}/5</b></div>
<div class="row"><span>nights survived</span><b>${nights}/${NIGHT_COUNT}</b></div>
<div class="row"><span>gardens held</span><b>${s.held ?? 0}/${NIGHT_COUNT}</b></div>
<div class="row"><span>in the bank</span><b>$${s.bankAfter}</b></div>
${s.rep != null ? `<div class="row"><span>reputation</span><b>${repFmt(s.rep)}</b></div>` : ''}
${s.warrantiesToDate ? `<div class="row"><span>warranty jobs booked</span><b>${s.warrantiesToDate}</b></div>` : ''}
@ -1071,7 +1080,7 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
card.innerHTML = `<div class="card jobcard">
${letterhead}
<h1>MORNING${w ? ` · NIGHT ${w.night} OF 5` : ''}</h1>
<h1>MORNING${w ? ` · NIGHT ${w.night} OF ${w.nights ?? NIGHT_COUNT}` : ''}</h1>
<h2>${r.subtitle}</h2>
${rows}
${money}

View File

@ -215,17 +215,25 @@ export default async function run(t) {
// --- the week (SPRINT8 gate 1) -------------------------------------------
t.test('the week is five escalating nights, each a storm and a site', () => {
assertEq(NIGHTS.length, 5);
t.test('the week is six escalating nights, each a storm and a site', () => {
// SPRINT16 gate 4 (D's wiring): five → six. The swing lawn debuts night 4
// under Tuesday's southerly; the soaker closes the week over the corner
// block. The deeper pins — one-variable law, survived-storm debut, the
// measured soaker pairing — live in d.test.js next to the wiring's owner.
assertEq(NIGHTS.length, 6);
// SPRINT10: a night is {storm, site} now. nightAt() normalises either shape.
assertEq(nightAt(0).storm, 'storm_01_gentle', 'night one must be the one that cannot hurt you');
assertEq(nightAt(4).storm, 'storm_02b_icenight', 'night five is the ice night');
assertEq(nightAt(3).storm, 'storm_03_southerly', 'night four repeats the southerly — the yard is the only new thing');
assertEq(nightAt(3).site, 'site_03_swing_lawn', 'night four is the swing lawn');
assertEq(nightAt(4).storm, 'storm_02b_icenight', 'night five is still the ice night');
assertEq(nightAt(5).storm, 'storm_06_soaker', 'night six is the fabric bet');
assertEq(nightAt(5).site, 'site_02_corner_block', "and it flies over the corner block — C's measured pairing");
assertEq(nightAt(2).site, 'site_02_corner_block', 'night three moves to the corner block');
assertEq(nightAt(0).site, 'backyard_01', 'the rest are the backyard');
assertEq(nightAt(0).site, 'backyard_01', 'the retainer nights are the backyard');
// SPRINT13 gate 1.4: the unsavable-garden flag is night 5's alone, and a
// night without it is presumed savable — new nights can't inherit the excuse.
assertEq(nightAt(4).gardenBeyondSaving, true, 'night 5 is beyond saving BY DESIGN — canon, ruled');
for (let i = 0; i < 4; i++) assertEq(nightAt(i).gardenBeyondSaving, false, `night ${i + 1} is savable`);
for (const i of [0, 1, 2, 3, 5]) assertEq(nightAt(i).gardenBeyondSaving, false, `night ${i + 1} is savable`);
const w = createWeek();
assertEq(w.night, 1); assertEq(w.bank, 80);
assertEq(w.site, 'backyard_01', 'week.site tracks the ladder');
@ -234,9 +242,12 @@ export default async function run(t) {
assertEq(w.site, 'site_02_corner_block', 'and reaches the corner block on night three');
for (let i = 0; i < 2; i++) w.advance();
assertEq(w.night, 5);
assert(w.isFinalNight, 'night five is the last');
assert(!w.isFinalNight, 'the ice night is no longer the last — its dawn breaks have a morning to book on now');
w.advance();
assertEq(w.night, 5, 'the ladder does not walk off its own end');
assertEq(w.night, 6);
assert(w.isFinalNight, 'night six is the last');
w.advance();
assertEq(w.night, 6, 'the ladder does not walk off its own end');
});
t.test('money persists across nights, and the bank is next night\'s shop', () => {
@ -292,14 +303,15 @@ export default async function run(t) {
assert(j.brief && j.brief.length > 20, `night ${i + 1} has a brief worth reading`);
assert(j.site, `night ${i + 1} has a yard`);
}
// The week's shape, which is DATA and not decoration: four nights on
// retainer at one yard while they're away, and ONE short-notice callout to
// a stranger's corner block. Night 3 landing differently is the whole point
// of the ladder — a different site MEANS a different client.
// The week's shape, which is DATA and not decoration: the Hendersons'
// retainer, one short-notice callout to a stranger's corner block, the
// Delaneys' swing lawn — and the corner block calling BACK, which is
// SPRINT16's whole thesis (your work follows you) written into the ladder.
const clients = NIGHTS.map((_, i) => nightAt(i).client);
assertEq(new Set(clients).size, 2, 'two clients: the retainer and the one-off');
assertEq(new Set(clients).size, 3, 'three clients: the retainer, the one-off, and the swing lawn');
assert(clients[2] !== clients[0], 'night 3 is somebody else');
assertEq(nightAt(2).site, 'site_02_corner_block', "and it's their corner block");
assertEq(clients[5], clients[2], 'night six is the corner block client again — a return booking, not a stranger');
});
t.test('an unbriefed night is a job with no letterhead, not a crash', () => {
@ -386,10 +398,17 @@ export default async function run(t) {
assertEq(s.outcome, 'gameover', 'you cannot rig four corners, so the week is over');
// On the last night there is no next shop to be unable to afford, so being
// broke is just being broke — the week still finished.
// broke is just being broke — the week still finished. SPRINT16: the last
// night is SIX now, and night five going broke is a real game over again —
// both directions pinned so the re-ordering can't quietly move the rule.
const w2 = createWeek();
for (let i = 0; i < 4; i++) w2.advance();
assertEq(w2.settle(ruin, def, 80).outcome, 'win', 'night five always ends the week, not the run');
for (let i = 0; i < NIGHTS.length - 1; i++) w2.advance();
assert(w2.isFinalNight, 'precondition: the walk reached the final night');
assertEq(w2.settle(ruin, def, 80).outcome, 'win', 'the final night always ends the week, not the run');
const w3 = createWeek();
for (let i = 0; i < 4; i++) w3.advance();
assertEq(w3.settle(ruin, def, 80).outcome, 'gameover',
'night five is not the final night any more — going broke there ends the run');
});
// Gate 2 acceptance: both sites load from data, and the corner block is not
@ -711,15 +730,15 @@ export default async function run(t) {
t.test('SPRINT16 negative control: a clean week books ZERO warranty items and ends rep-positive', () => {
const w = createWeek();
let items = 0;
for (let n = 0; n < 5; n++) {
for (let n = 0; n < NIGHTS.length; n++) {
items += w.quote(s16def).warranty.length; // counted off the sheet…
items += w.settle(cleanRun(), s16def, 0).warranty.length; // …and the invoice
w.advance();
}
assertEq(items, 0, 'no sheet and no invoice ever carried a warranty line');
assert(w.rep > REP.START, `the week ends rep-positive (★${w.rep})`);
assertEq(w.rep, REP.MAX, 'five clean nights ride the clamp to the top of the scale');
assertEq(w.log[4].warrantiesToDate, 0, 'and the end card has no receipts to show');
assertEq(w.rep, REP.MAX, 'six clean nights ride the clamp to the top of the scale');
assertEq(w.log[NIGHTS.length - 1].warrantiesToDate, 0, 'and the end card has no receipts to show');
w.reset();
assertEq(w.rep, REP.START, 'reset() is a new outfit — rep goes back to neutral');
});

View File

@ -20,6 +20,9 @@ import { needsLadder as realNeedsLadder, createLadder } from '../ladder.js';
import { assert, assertEq, assertClose, assertLess, fixedLoop } from '../testkit.js';
import { FIXED_DT } from '../contracts.js';
import { loadStorm, createWind } from '../weather.js';
// SPRINT16 gate 4: the six-night law pins at the bottom of this file read the
// ladder. week.js is contracts-only underneath, so this stays headless-safe.
import { NIGHTS, nightAt } from '../week.js';
const DT = FIXED_DT;
@ -1135,4 +1138,61 @@ export default async function run(t) {
assertEq(repaired, 0, 're-rig fires with a spare');
assertEq(p.carrying, null, 'and consumes it');
});
// ================================================================ SPRINT16 gate 4
// THE SIX-NIGHT LAW, pinned next to its owner. a.test pins the ladder's
// shape (which storm, which site, which slot); these pin the RULES the
// ordering must obey, so a future re-order that keeps six nights but breaks
// the law goes red with the law's own words. Every threshold here is
// ROADMAP principle 5 or a measured THREADS receipt, not taste.
t.test('SPRINT16 six-night law: exactly one new site, exactly one new storm, never the same night', () => {
const nights = NIGHTS.map((_, i) => nightAt(i));
const PRE_S16_SITES = ['backyard_01', 'site_02_corner_block'];
const PRE_S16_STORMS = ['storm_01_gentle', 'storm_02_wildnight', 'storm_02b_icenight',
'storm_03_southerly', 'storm_03b_earlybuster'];
const newSiteNights = nights.filter((n) => !PRE_S16_SITES.includes(n.site));
const newStormNights = nights.filter((n) => !PRE_S16_STORMS.includes(n.storm));
assertEq(newSiteNights.length, 1, 'exactly ONE night is the new site');
assertEq(newSiteNights[0].site, 'site_03_swing_lawn');
assertEq(newStormNights.length, 1, 'exactly ONE night flies the new storm');
assertEq(newStormNights[0].storm, 'storm_06_soaker');
assert(newSiteNights[0] !== newStormNights[0],
'and they are NOT the same night — a player who loses must be able to name what beat them');
});
t.test('SPRINT16 six-night law: the new site debuts under a storm already survived', () => {
const idx = NIGHTS.findIndex((_, i) => nightAt(i).site === 'site_03_swing_lawn');
assert(idx > 0, 'the swing lawn is in the rotation and is not night one');
const debut = nightAt(idx);
const earlier = NIGHTS.slice(0, idx).map((_, i) => nightAt(i).storm);
assert(earlier.includes(debut.storm),
`night ${idx + 1} flies ${debut.storm}, which must have flown on an earlier night — `
+ 'at the swing lawn\'s debut the yard is the ONLY new variable');
});
t.test("SPRINT16 six-night law: the soaker flies the corner block — C's measured pairing", () => {
// THREADS [C] 2026-07-20: the backyard's buyable geometry caps hail cover
// over the bed at ~31% — best membrane line there reads 37.5, a bet with
// no win in it. The soaker over anything but site_02 needs a re-measure
// FIRST; this pin is where a re-pairing finds that out.
const soaker = NIGHTS.map((_, i) => nightAt(i)).find((n) => n.storm === 'storm_06_soaker');
assert(soaker, 'the soaker is in the week');
assertEq(soaker.site, 'site_02_corner_block',
'storm_06 over any other yard is unmeasured — fly gardenfly over the new pairing before moving this');
const debutIdx = NIGHTS.findIndex((_, i) => nightAt(i).site === soaker.site);
const soakerIdx = NIGHTS.findIndex((_, i) => nightAt(i).storm === 'storm_06_soaker');
assert(debutIdx < soakerIdx, 'and the yard was taught on an earlier night — the storm is the only new variable');
});
t.test('SPRINT16 six-night law: the beyond-saving night is neither new nor final', () => {
const flagged = NIGHTS.map((_, i) => nightAt(i)).filter((n) => n.gardenBeyondSaving);
assertEq(flagged.length, 1, 'exactly one designed loss in the week');
assertEq(flagged[0].storm, 'storm_02b_icenight', 'and it is the ice night — the flag is a measured fact about');
assertEq(flagged[0].site, 'backyard_01', 'THIS yard under THIS storm (bare 0.0, best buyable 27.7, win at 50)');
const idx = NIGHTS.findIndex((_, i) => nightAt(i).gardenBeyondSaving);
assert(idx !== NIGHTS.length - 1,
'the designed loss must not be the final night: its dawn breaks need a morning to book warranty on '
+ '(A filed the night-5-final gap in THREADS — the six-night order is what closes it)');
});
}

View File

@ -1,7 +1,9 @@
/**
* SHADES the week. Lane A owns this file.
* (SPRINT16 gate 4: the NIGHTS ladder and the two new briefs are Lane D's
* edits, by the sprint's explicit grant A owns everything else here.)
*
* Five nights, one yard, one wallet. This is deliberately a thin wrapper around
* Six nights, one wallet. This is deliberately a thin wrapper around
* the phase machine rather than a new system: main.js already knows how to run
* forecast prep storm aftermath, and the week's whole job is to answer
* three questions between rounds which storm is tonight, how much money do you
@ -20,30 +22,51 @@ import { START_BUDGET, HARDWARE } from './contracts.js';
* 1 Sea Breeze gust 10.5, no hail the tutorial: nothing can hurt you
* 2 Southerly Buster gust 20.0, hail the change, and hail, arrive
* 3 Early Buster gust 20.0, hail same teeth, change comes EARLY
* 4 Wild Night gust 30.0, hail the one the whole repo tuned for
* 5 Ice Night gust 28.5, hail less wind, more ice
* 4 Southerly again gust 20.0, hail the storm you know, the yard you don't
* 5 Ice Night gust 28.5, hail the most wind of the week, and worse ice
* 6 Pea-Hail Soaker gust 15.2, fine hail the night the default fabric is wrong
* Night 3 is the trap: it looks like night 2 on the forecast card and isn't.
*
* SPRINT10: a night is now a STORM and a SITE. Nights 1/2/4/5 are the backyard;
* night 3 moves to the corner block the same "looks like night 2 and isn't"
* beat, sharpened: it's a new yard you've never rigged, its carport is a trap,
* and its storm carries the southerly change that makes the venturi scream. The
* plain-string form is still accepted (defaults to backyard_01) so nothing that
* reads NIGHTS[i] as a storm key breaks.
* SPRINT10: a night is now a STORM and a SITE. Night 3 moves to the corner block
* the same "looks like night 2 and isn't" beat, sharpened: it's a new yard
* you've never rigged, its carport is a trap, and its storm carries the
* southerly change that makes the venturi scream. The plain-string form is
* still accepted (defaults to backyard_01) so nothing that reads NIGHTS[i] as
* a storm key breaks.
*
* SPRINT11: a night is now a JOB. Same ladder, same yards what's new is the
* frame DESIGN.md has had from line one: *"work arrives as callouts each one a
* site with existing conditions, a client brief, a budget, and a forecast
* window."* You are not surviving five nights; you are working five jobs.
* SPRINT11: a night is now a JOB. What's new is the frame DESIGN.md has had
* from line one: *"work arrives as callouts each one a site with existing
* conditions, a client brief, a budget, and a forecast window."* You are not
* surviving the week; you are working its jobs.
*
* The week that fell out of the data, once it was asked to name a client: the
* Hendersons are away (SPRINT11's own line they leave for Cairns), so four of
* the five nights are the same yard, on retainer, through a bad week. Night 3 is
* a different site, so it is necessarily a different client a short-notice
* callout to a yard you have never rigged, on the night the change comes early.
* That is not flavour bolted on: it is what `site: site_02_corner_block` already
* MEANT, finally said out loud. The corner block lands harder because it's a
* stranger's place and you're there at short notice.
* Hendersons are away (SPRINT11's own line they leave for Cairns), so the
* backyard nights are the same yard, on retainer, through a bad week. A
* different site is necessarily a different client.
*
* SPRINT16 gate 4 (Lane D, by the sprint's explicit grant): SIX nights. The
* swing lawn enters the rotation at night 4 under the southerly the ONE
* variable rule (ROADMAP principle 5), applied literally: at its debut the only
* new thing is the yard, because the storm is Tuesday's, already survived. The
* soaker flies night 6 over the corner block a yard night 3 taught because
* C MEASURED the pairing (THREADS [C] 2026-07-20): the backyard's buyable
* geometry caps hail cover over the bed at ~31%, so over there the membrane
* bet has no win in it. Exactly one night is the new site (4), exactly one
* flies the new storm (6), and they are not the same night.
*
* THE STRUCTURAL CONSEQUENCE, SAID OUT LOUD (D, filed in THREADS for John's
* gate 6): six slots cannot hold seven nights. Five shipped storms + the
* soaker is six distinct storms; the swing lawn's debut must REPEAT a storm
* the player already survived; so exactly one shipped storm leaves the week
* and every other candidate is load-bearing (gentle = the tutorial and the
* calm-day preload; southerly = the hail intro and the early buster's
* referent; early buster = site_02's debut, which the soaker's pairing needs;
* icenight = the beyond-saving flag, canon). THE WILD NIGHT (storm_02) is out
* of the rotation. Its storm file, its tests, and the pinned backyard
* separation all stand it is un-flown by the week, not deleted. If that
* eviction is wrong, the honest fix is a SEVEN-night week (arc 2's own range
* is "five-to-seven"), which is one entry re-added here a ruling, not a
* refactor.
*
* Copy lives here as data, per SPRINT11's rule: client flavour is data, not code.
*/
@ -72,17 +95,29 @@ export const NIGHTS = [
+ 'to tie off to.',
},
{
storm: 'storm_02_wildnight', site: 'backyard_01',
client: 'the Hendersons', addr: '14 Kurrajong St — the backyard',
brief: 'Back at the Hendersons\', and this is the night the whole week was pointing at. Rig it like '
+ 'they\'re watching, because Friday they will be.',
// SPRINT16 gate 4 — THE NEW SITE, and nothing else new: Tuesday's southerly
// over D's swing lawn. The brief does what "plenty to tie off to" did for
// the carport — the client's own voice sells the trap (the crossbar between
// the swing apexes is a dead-level rail and NOT an anchor; the apexes are
// 0.45 steel that bills $140) — and the kid gives the bed its reason.
storm: 'storm_03_southerly', site: 'site_03_swing_lawn',
client: 'the Delaneys', addr: '31 Ferndale Ave — the swing lawn',
brief: 'Same southerly as Tuesday, over a lawn you\'ve never rigged. The swing set is Ruby\'s — '
+ 'her dad reckons the crossbar\'s bolted right through, tie to that and save yourself a '
+ 'post. The veggie bed is her school project, so it comes home alive.',
},
{
storm: 'storm_02b_icenight', site: 'backyard_01',
client: 'the Hendersons', addr: '14 Kurrajong St — the backyard',
brief: 'Less wind than last night and more ice, which is worse for the bed and easier on the rig. '
+ 'Last night before they land. Nobody\'s pretending the seedlings ride this one out — keep '
+ 'the sail on its feet and the ice off the windows.',
// SPRINT16 gate 4 wording note (D): this brief used to open "Less wind than
// last night" — true when the wild night (gust 30) sat at night 4. The wild
// night left the six-night rotation (see the ladder comment), so the ice
// night is now the most wind the week throws; the sentence moved with the
// facts. The teaching half — level with the player that the bed is already
// lost — is untouched.
brief: 'The most wind of the week and golf-ball ice with it. Last night before they land. '
+ 'Nobody\'s pretending the seedlings ride this one out — keep the sail on its feet and '
+ 'the ice off the windows.',
// SPRINT13 gate 1.4 — A's ruling, DESIGN CANON: night 5's garden is BEYOND
// SAVING, by design, at any price. Measured game-true (B's pay table and
// A's probe, agreeing): bare bed 0.0 DEAD; best buyable garden 27.7; best
@ -99,6 +134,20 @@ export const NIGHTS = [
// flag in the same commit or the verdict lies in the other direction.
gardenBeyondSaving: true,
},
{
// SPRINT16 gate 4 — THE NEW STORM, over a yard night 3 taught. C's pairing
// receipt (THREADS [C] 2026-07-20): the soaker MUST fly the corner block —
// the backyard caps hail cover at ~31% and the bet has no win there. The
// brief sells the WRONG default the way the carport got sold: fine hail
// through a 2 mm weave is exactly the night cloth loses, and the client is
// the one telling you it's easy. The forecast (C's stones/rain lines) is
// where the truth argues back — the brief stays in character.
storm: 'storm_06_soaker', site: 'site_02_corner_block',
client: 'the Vasilaros place', addr: '2 Bight Rd — the corner block',
brief: 'The Vasilaros place again — they asked for you by name this time. Hardly any wind in it, '
+ 'just rain and a fine rattle of hail; the kind of night you could rig in your sleep. '
+ 'Whatever you flew last time will do, they said.',
},
];
/**