Lane D S17 gate 0.1: SEVEN NIGHTS — the wildnight restored at night 5 (John's ruling)

Ladder: gentle, southerly, earlybuster×site_02, southerly×swing_lawn,
WILDNIGHT×backyard, icenight×backyard (beyondSaving rides the night),
soaker×site_02. Icenight brief reverts to 'less wind than last night'
(true again); new Hendersons brief sells the pyrrhic stake. Law pins
retitled + one new ruling pin (wildnight in, known yard, before the
icenight); a.test re-pinned to the seven-shape; gradeFor clean bar
derived (held >= savable) per the S16 same-commit demand. Selftest
474/0/0; verified live on :8827.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-21 00:36:03 +10:00
parent a1b7d468a4
commit 75d5f9a4bf
4 changed files with 218 additions and 73 deletions

View File

@ -8205,3 +8205,47 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
the S16 law pins move WITH the ruling, which is them working), hud already reads NIGHTS.length the S16 law pins move WITH the ruling, which is them working), hud already reads NIGHTS.length
so the papers tell the truth by construction. SPRINT17 gate 0 lands it; arc 2 (THE BOOK OF so the papers tell the truth by construction. SPRINT17 gate 0 lands it; arc 2 (THE BOOK OF
CLIENTS) begins in the same sprint. The play half of gate 6 stands as ever. CLIENTS) begins in the same sprint. The play half of gate 6 stands as ever.
[D] 2026-07-21 — ⚖️→🔧 **GATE 0.1 LANDED: THE SEVEN-NIGHT WEEK IS WIRED. The wildnight flies
again, night 5, over the backyard.** John's ruling, in code, fast — A, rebase away. The
ladder as wired:
1 gentle × backyard — the tutorial
2 southerly × backyard — hail arrives
3 earlybuster × site_02 — the corner block's debut (new yard)
4 southerly × swing_lawn — the swing lawn's debut (new yard, survived storm)
5 WILDNIGHT × backyard — RESTORED. the marquee storm, the pyrrhic-win canon
6 icenight × backyard — the designed loss (gardenBeyondSaving rode the night to slot 6)
7 soaker × site_02 — the fabric bet closes the week
**The slot, and why it's the law and not taste:** the wildnight flies over a yard the player
knows — it always did. Backyard at night 5 is a yard rigged three times by then, so the storm
is the only new variable (one-variable law at the marquee slot); the pinned separation lives
in backyard_01's own site data, so the pin flies unchanged; and 30.0 directly before 28.5
restores the icenight's pre-S16 opening line "less wind than last night" as a TRUE sentence —
that brief moved with the facts twice now (S16 out, S17 back), and a new d.test pin
(wildIdx === iceIdx1) makes any future re-order re-word it in the same commit or go red.
**The restored night's brief** (the Hendersons' voice, selling the pyrrhic stake — "the garden
made it; the sail didn't" promised as a WIN before a dollar is spent): *"The Hendersons rang
from Cairns — the forecast made the news up there. This is the big one, the night the whole
week was pointing at. Their words: they'd rather fly home to a wrecked sail over a living bed
than a tidy yard and a dead one. Spend the sail if you have to. Save the garden."*
**The pins moved with the ruling — that's them working:** d.test's four S16 law pins are
retitled "week law (S16, holds at seven)" and every assert body survived UNTOUCHED (they were
slot-free rules, which was the point); one NEW pin carries the ruling itself (wildnight IN the
week, over backyard_01, directly before the icenight — re-evicting it is a John question, not
a re-order). a.test's week suite re-pinned to the seven-shape: NIGHTS.length 7, wildnight at
slot 5, icenight's flag at slot 6 (neither new nor final — its dawn breaks book warranty on
the soaker's morning), broke-on-final-night now walks NIGHTS.length so it TRACKS the final
night by construction, the flagged-night rep test finds the flag instead of hardcoding slot 4.
**A, your files, my edits, flagged (revert-and-tell-me standard, same as S16):** a.test week
asserts as above, and — the one real ruling I made in your territory — **gradeFor's clean bar
is now DERIVED** (held >= savable nights, i.e. NIGHTS minus beyondSaving = 6). My own S16
filing said a seven-night week needs gradeFor re-ruled in the same commit as the ladder; this
is that commit, and held>=5 'clean' would have printed THE WEEK HELD over a savable garden
that died. The scraped/solvent boundary (>=3) is untouched — moving it is a wording-taste
call and it's yours; there's no lie in leaving it.
**Verified:** selftest **474/0/0** (473 baseline + the new ruling pin — the count moved
exactly by what was added, nothing else). Verified live on :8827, my own eyes: splash "Seven
nights, seven jobs, one wallet", job sheet NIGHT 1 OF 7 with seven pips, cold-rigged night 1
on carabiners, invoice MORNING · NIGHT 1 OF 7, night 2 sheet NIGHT 2 OF 7 / ●◆····· /
southerly brief — hud read NIGHTS.length throughout, zero hud edits needed, as designed.
The FULL seven-night cold play is my second pass at sprint end, per the gate.

View File

@ -215,39 +215,45 @@ export default async function run(t) {
// --- the week (SPRINT8 gate 1) ------------------------------------------- // --- the week (SPRINT8 gate 1) -------------------------------------------
t.test('the week is six escalating nights, each a storm and a site', () => { t.test('the week is seven escalating nights, each a storm and a site', () => {
// SPRINT16 gate 4 (D's wiring): five → six. The swing lawn debuts night 4 // SPRINT16 gate 4 (D's wiring): five → six. SPRINT17 gate 0.1 (D again,
// under Tuesday's southerly; the soaker closes the week over the corner // wiring JOHN'S RULING 2026-07-21): six → SEVEN — the wildnight restored
// block. The deeper pins — one-variable law, survived-storm debut, the // at night 5, between the swing lawn and the icenight. The swing lawn
// measured soaker pairing — live in d.test.js next to the wiring's owner. // debuts night 4 under Tuesday's southerly; the soaker still closes the
assertEq(NIGHTS.length, 6); // week over the corner block. The deeper pins — one-variable law,
// survived-storm debut, the measured soaker pairing, the wildnight's
// known-yard slot — live in d.test.js next to the wiring's owner.
assertEq(NIGHTS.length, 7);
// SPRINT10: a night is {storm, site} now. nightAt() normalises either shape. // 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(0).storm, 'storm_01_gentle', 'night one must be the one that cannot hurt you');
assertEq(nightAt(3).storm, 'storm_03_southerly', 'night four repeats the southerly — the yard is the only new thing'); 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(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(4).storm, 'storm_02_wildnight', "night five is the wildnight — restored, John's seven-night ruling");
assertEq(nightAt(5).storm, 'storm_06_soaker', 'night six is the fabric bet'); assertEq(nightAt(4).site, 'backyard_01', 'and it flies the backyard — the yard its separation is pinned to');
assertEq(nightAt(5).site, 'site_02_corner_block', "and it flies over the corner block — C's measured pairing"); assertEq(nightAt(5).storm, 'storm_02b_icenight', 'night six is the ice night');
assertEq(nightAt(6).storm, 'storm_06_soaker', 'night seven is the fabric bet');
assertEq(nightAt(6).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(2).site, 'site_02_corner_block', 'night three moves to the corner block');
assertEq(nightAt(0).site, 'backyard_01', 'the retainer nights 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 // SPRINT13 gate 1.4: the unsavable-garden flag is the icenight's alone, and
// night without it is presumed savable — new nights can't inherit the excuse. // a night without it is presumed savable — new nights can't inherit the
assertEq(nightAt(4).gardenBeyondSaving, true, 'night 5 is beyond saving BY DESIGN — canon, ruled'); // excuse. The flag rides the NIGHT, not the slot: it moved 5 → 6 with the ladder.
for (const i of [0, 1, 2, 3, 5]) assertEq(nightAt(i).gardenBeyondSaving, false, `night ${i + 1} is savable`); assertEq(nightAt(5).gardenBeyondSaving, true, 'night 6 is beyond saving BY DESIGN — canon, ruled');
for (const i of [0, 1, 2, 3, 4, 6]) assertEq(nightAt(i).gardenBeyondSaving, false, `night ${i + 1} is savable`);
const w = createWeek(); const w = createWeek();
assertEq(w.night, 1); assertEq(w.bank, 80); assertEq(w.night, 1); assertEq(w.bank, 80);
assertEq(w.site, 'backyard_01', 'week.site tracks the ladder'); assertEq(w.site, 'backyard_01', 'week.site tracks the ladder');
assert(!w.isFinalNight); assert(!w.isFinalNight);
for (let i = 0; i < 2; i++) w.advance(); for (let i = 0; i < 2; i++) w.advance();
assertEq(w.site, 'site_02_corner_block', 'and reaches the corner block on night three'); assertEq(w.site, 'site_02_corner_block', 'and reaches the corner block on night three');
for (let i = 0; i < 2; i++) w.advance(); for (let i = 0; i < 3; i++) w.advance();
assertEq(w.night, 5);
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, 6); assertEq(w.night, 6);
assert(w.isFinalNight, 'night six is the last'); assert(!w.isFinalNight, 'the ice night is still not the last — its dawn breaks keep their morning to book on');
w.advance(); w.advance();
assertEq(w.night, 6, 'the ladder does not walk off its own end'); assertEq(w.night, 7);
assert(w.isFinalNight, 'night seven is the last');
w.advance();
assertEq(w.night, 7, 'the ladder does not walk off its own end');
}); });
t.test('money persists across nights, and the bank is next night\'s shop', () => { t.test('money persists across nights, and the bank is next night\'s shop', () => {
@ -311,7 +317,7 @@ export default async function run(t) {
assertEq(new Set(clients).size, 3, 'three clients: the retainer, the one-off, and the swing lawn'); 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'); 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(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'); assertEq(clients[NIGHTS.length - 1], clients[2], 'the closer 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', () => { t.test('an unbriefed night is a job with no letterhead, not a crash', () => {
@ -381,10 +387,15 @@ export default async function run(t) {
// WEEK HELD — everything's still where you put it" over four dead gardens. // WEEK HELD — everything's still where you put it" over four dead gardens.
// Outlasting the week is not surviving it, and the end card is the last // Outlasting the week is not surviving it, and the end card is the last
// thing this game says to anyone. // thing this game says to anyone.
assertEq(gradeFor(5), 'clean', 'five held is the only clean week'); // SPRINT17 gate 0.1 (D, the same-commit demand from the S16 filing): the
// clean bar is derived — every SAVABLE garden held. Seven nights minus the
// icenight's designed loss is 6; held>=5 'clean' would have printed THE
// WEEK HELD over a savable garden that died.
assertEq(gradeFor(6), 'clean', 'six held — every savable garden — is the only clean week');
assertEq(gradeFor(5), 'scraped', 'five of six savable is no longer clean — the bar moved with the ladder');
assertEq(gradeFor(4), 'scraped'); assertEq(gradeFor(4), 'scraped');
assertEq(gradeFor(3), 'scraped'); assertEq(gradeFor(3), 'scraped');
assertEq(gradeFor(1), 'solvent', 'one garden out of five is not a triumph'); assertEq(gradeFor(1), 'solvent', 'one garden out of six is not a triumph');
assertEq(gradeFor(0), 'solvent'); assertEq(gradeFor(0), 'solvent');
}); });
@ -398,17 +409,19 @@ export default async function run(t) {
assertEq(s.outcome, 'gameover', 'you cannot rig four corners, so the week is over'); 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 // 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. SPRINT16: the last // broke is just being broke — the week still finished. SPRINT16 moved the
// night is SIX now, and night five going broke is a real game over again — // last night to six; SPRINT17's seven-night ruling moves it again — the
// both directions pinned so the re-ordering can't quietly move the rule. // walk below reads NIGHTS.length, so this pin TRACKS the final night by
// construction, and the second-to-last-night control stays a real game
// over — both directions pinned so a re-ordering can't quietly move the rule.
const w2 = createWeek(); const w2 = createWeek();
for (let i = 0; i < NIGHTS.length - 1; i++) w2.advance(); for (let i = 0; i < NIGHTS.length - 1; i++) w2.advance();
assert(w2.isFinalNight, 'precondition: the walk reached the final night'); 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'); assertEq(w2.settle(ruin, def, 80).outcome, 'win', 'the final night always ends the week, not the run');
const w3 = createWeek(); const w3 = createWeek();
for (let i = 0; i < 4; i++) w3.advance(); for (let i = 0; i < NIGHTS.length - 2; i++) w3.advance();
assertEq(w3.settle(ruin, def, 80).outcome, 'gameover', assertEq(w3.settle(ruin, def, 80).outcome, 'gameover',
'night five is not the final night any more — going broke there ends the run'); 'the second-to-last night is not the final night — going broke there ends the run');
}); });
// Gate 2 acceptance: both sites load from data, and the corner block is not // Gate 2 acceptance: both sites load from data, and the corner block is not
@ -678,16 +691,20 @@ export default async function run(t) {
// The pin that matters most: same rig, opposite garden — identical movement. // The pin that matters most: same rig, opposite garden — identical movement.
// hp and win are absent from the movement BY CONSTRUCTION, which is what // hp and win are absent from the movement BY CONSTRUCTION, which is what
// makes night 5's designed loss free without an exemption to go stale. // makes the icenight's designed loss free without an exemption to go stale.
const a1 = createWeek().settle(cleanRun({ hp: 100, win: true }), s16def, 0); const a1 = createWeek().settle(cleanRun({ hp: 100, win: true }), s16def, 0);
const b1 = createWeek().settle(cleanRun({ hp: 0, win: false }), s16def, 0); const b1 = createWeek().settle(cleanRun({ hp: 0, win: false }), s16def, 0);
assertEq(a1.repDelta, b1.repDelta, 'rep never reads the garden'); assertEq(a1.repDelta, b1.repDelta, 'rep never reads the garden');
}); });
t.test("SPRINT16: night 5's designed loss costs NO rep — a clean rig still earns it", () => { t.test("SPRINT16: the icenight's designed loss costs NO rep — a clean rig still earns it", () => {
// S17: the walk finds the flagged night instead of hardcoding slot 4 — the
// icenight moved to night 6 with the seven-night ruling and this pin is
// about the FLAG, not the slot.
const flaggedIdx = NIGHTS.findIndex((_, i) => nightAt(i).gardenBeyondSaving);
const w = createWeek(); const w = createWeek();
for (let i = 0; i < 4; i++) w.advance(); for (let i = 0; i < flaggedIdx; i++) w.advance();
assertEq(nightAt(4).gardenBeyondSaving, true, 'precondition: the flagged night'); assertEq(nightAt(flaggedIdx).gardenBeyondSaving, true, 'precondition: the flagged night');
// Garden lost by design, steel held, nothing of theirs broken: that was // Garden lost by design, steel held, nothing of theirs broken: that was
// the whole job tonight (the verdict's own words), and the number says so. // the whole job tonight (the verdict's own words), and the number says so.
const s = w.settle(cleanRun({ hp: 0, win: false }), s16def, 0); const s = w.settle(cleanRun({ hp: 0, win: false }), s16def, 0);
@ -737,7 +754,7 @@ export default async function run(t) {
} }
assertEq(items, 0, 'no sheet and no invoice ever carried a warranty line'); 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})`); assert(w.rep > REP.START, `the week ends rep-positive (★${w.rep})`);
assertEq(w.rep, REP.MAX, 'six clean nights ride the clamp to the top of the scale'); assertEq(w.rep, REP.MAX, 'seven 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'); assertEq(w.log[NIGHTS.length - 1].warrantiesToDate, 0, 'and the end card has no receipts to show');
w.reset(); w.reset();
assertEq(w.rep, REP.START, 'reset() is a new outfit — rep goes back to neutral'); assertEq(w.rep, REP.START, 'reset() is a new outfit — rep goes back to neutral');
@ -1057,8 +1074,8 @@ export default async function run(t) {
assert(hailed.verdict !== rained.verdict, 'hail and rain deaths must not read identically'); assert(hailed.verdict !== rained.verdict, 'hail and rain deaths must not read identically');
}); });
t.test("night 5's loss is TAUGHT, not blamed: beyondSaving rewrites the garden verdicts only", () => { t.test("the icenight's loss is TAUGHT, not blamed: beyondSaving rewrites the garden verdicts only", () => {
// SPRINT13 gate 1.4, the night-5 ruling. Measured game-true (B's pay table, // SPRINT13 gate 1.4, the icenight ruling (night 5 then, night 6 since S17). Measured game-true (B's pay table,
// my probe agreeing): the icenight garden cannot reach the win line at any // my probe agreeing): the icenight garden cannot reach the win line at any
// price — bare 0.0, best buyable 27.7. So week.js flags the night, and the // price — bare 0.0, best buyable 27.7. So week.js flags the night, and the
// verdict must stop implying a better rig existed. The worst liar was // verdict must stop implying a better rig existed. The worst liar was
@ -1310,7 +1327,7 @@ export default async function run(t) {
// --- SPRINT13 gate 1.4: THE PINNED SEPARATION TARGET ---------------------- // --- SPRINT13 gate 1.4: THE PINNED SEPARATION TARGET ----------------------
// //
// The ruling, RESTATED after B's skew finding (THREADS [A], twice — read the // The ruling, RESTATED after B's skew finding (THREADS [A], twice — read the
// second entry): on the shipped week's wild night — night 4, THIS yard — the // second entry): on the shipped week's wild night — night 5 since S17's ruling, THIS yard — the
// best line START_BUDGET can buy must HOLD and WIN comfortably (>60), a bare // best line START_BUDGET can buy must HOLD and WIN comfortably (>60), a bare
// bed must LOSE (<50), and the spread must read as most of a plant state // bed must LOSE (<50), and the spread must read as most of a plant state
// (≥25 HP), because the win line and the invoice are the game's real stakes. // (≥25 HP), because the win line and the invoice are the game's real stakes.

View File

@ -20,8 +20,9 @@ import { needsLadder as realNeedsLadder, createLadder } from '../ladder.js';
import { assert, assertEq, assertClose, assertLess, fixedLoop } from '../testkit.js'; import { assert, assertEq, assertClose, assertLess, fixedLoop } from '../testkit.js';
import { FIXED_DT } from '../contracts.js'; import { FIXED_DT } from '../contracts.js';
import { loadStorm, createWind } from '../weather.js'; import { loadStorm, createWind } from '../weather.js';
// SPRINT16 gate 4: the six-night law pins at the bottom of this file read the // SPRINT16 gate 4 / SPRINT17 gate 0.1: the week-law pins at the bottom of this
// ladder. week.js is contracts-only underneath, so this stays headless-safe. // file read the ladder. week.js is contracts-only underneath, so this stays
// headless-safe.
import { NIGHTS, nightAt } from '../week.js'; import { NIGHTS, nightAt } from '../week.js';
const DT = FIXED_DT; const DT = FIXED_DT;
@ -1139,14 +1140,19 @@ export default async function run(t) {
assertEq(p.carrying, null, 'and consumes it'); assertEq(p.carrying, null, 'and consumes it');
}); });
// ================================================================ SPRINT16 gate 4 // ==================================== SPRINT16 gate 4 → SPRINT17 gate 0.1
// THE SIX-NIGHT LAW, pinned next to its owner. a.test pins the ladder's // THE WEEK'S LAW, pinned next to its owner. Written as the SIX-night law in
// shape (which storm, which site, which slot); these pin the RULES the // S16; John ruled SEVEN (2026-07-21 — the wildnight restored, my own gate-6
// ordering must obey, so a future re-order that keeps six nights but breaks // filing's alternative), and the pins moved WITH the ruling, which is them
// the law goes red with the law's own words. Every threshold here is // working, not breaking: every rule below is slot-free, so restoring the
// wildnight at night 5 changed no assert body — only this prose, plus one
// NEW pin for the ruling itself (last in the block). 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 seven 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. // 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', () => { t.test('week law (S16, holds at seven): exactly one new site, exactly one new storm, never the same night', () => {
const nights = NIGHTS.map((_, i) => nightAt(i)); const nights = NIGHTS.map((_, i) => nightAt(i));
const PRE_S16_SITES = ['backyard_01', 'site_02_corner_block']; const PRE_S16_SITES = ['backyard_01', 'site_02_corner_block'];
const PRE_S16_STORMS = ['storm_01_gentle', 'storm_02_wildnight', 'storm_02b_icenight', const PRE_S16_STORMS = ['storm_01_gentle', 'storm_02_wildnight', 'storm_02b_icenight',
@ -1161,7 +1167,7 @@ export default async function run(t) {
'and they are NOT the same night — a player who loses must be able to name what beat them'); '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', () => { t.test('week law (S16, holds at seven): the new site debuts under a storm already survived', () => {
const idx = NIGHTS.findIndex((_, i) => nightAt(i).site === 'site_03_swing_lawn'); 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'); assert(idx > 0, 'the swing lawn is in the rotation and is not night one');
const debut = nightAt(idx); const debut = nightAt(idx);
@ -1171,7 +1177,7 @@ export default async function run(t) {
+ 'at the swing lawn\'s debut the yard is the ONLY new variable'); + '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", () => { t.test("week law (S16, holds at seven): the soaker flies the corner block — C's measured pairing", () => {
// THREADS [C] 2026-07-20: the backyard's buyable geometry caps hail cover // 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 // 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 // no win in it. The soaker over anything but site_02 needs a re-measure
@ -1185,7 +1191,7 @@ export default async function run(t) {
assert(debutIdx < soakerIdx, 'and the yard was taught on an earlier night — the storm is the only new variable'); 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', () => { t.test('week law (S16, holds at seven): the beyond-saving night is neither new nor final', () => {
const flagged = NIGHTS.map((_, i) => nightAt(i)).filter((n) => n.gardenBeyondSaving); const flagged = NIGHTS.map((_, i) => nightAt(i)).filter((n) => n.gardenBeyondSaving);
assertEq(flagged.length, 1, 'exactly one designed loss in the week'); 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].storm, 'storm_02b_icenight', 'and it is the ice night — the flag is a measured fact about');
@ -1193,6 +1199,34 @@ export default async function run(t) {
const idx = NIGHTS.findIndex((_, i) => nightAt(i).gardenBeyondSaving); const idx = NIGHTS.findIndex((_, i) => nightAt(i).gardenBeyondSaving);
assert(idx !== NIGHTS.length - 1, assert(idx !== NIGHTS.length - 1,
'the designed loss must not be the final night: its dawn breaks need a morning to book warranty on ' '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)'); + "(A filed the night-final gap in THREADS — the week's ordering closes it, at six AND at seven)");
});
t.test("SPRINT17 seven-night law: the wildnight is IN the week, over a yard the player knows", () => {
// JOHN'S RULING (2026-07-21), pinned so a future re-order can't quietly
// re-evict the marquee storm: gate 6 asked six-without-it or seven-with-it,
// and the answer was seven. Three facts carry the ruling:
const nights = NIGHTS.map((_, i) => nightAt(i));
const wildIdx = nights.findIndex((n) => n.storm === 'storm_02_wildnight');
assert(wildIdx >= 0,
"the wildnight flies — its S16 eviction was RULED wrong (THREADS [I] 2026-07-21); "
+ 'removing it again is a John question, not a re-order');
// The one-variable law at its slot: the wildnight flies over a yard the
// player has already rigged — it always did (the pinned separation lives
// in backyard_01's own site data, so the storm is the only thing new).
const wild = nights[wildIdx];
assertEq(wild.site, 'backyard_01',
"the wildnight flies the backyard — the pinned separation is THAT yard's site data; "
+ 'flying it elsewhere needs the separation re-measured FIRST, like the soaker pin above');
assert(nights.slice(0, wildIdx).some((n) => n.site === wild.site),
'and the yard was rigged on an earlier night — at the marquee storm the yard must not also be new');
// The icenight's brief opens "Less wind than last night" — TRUE only while
// the wildnight (peak 30.0) sits immediately before it (28.5). If this
// ordering changes, that brief's first sentence moves in the same commit
// (it already moved twice: S16 out, S17 back — the sentence follows the facts).
const iceIdx = nights.findIndex((n) => n.storm === 'storm_02b_icenight');
assertEq(wildIdx, iceIdx - 1,
"the wildnight sits directly before the icenight — the icenight's brief says 'less wind than "
+ "last night', and the paper never lies: re-order these and re-word that brief in the same commit");
}); });
} }

View File

@ -3,7 +3,7 @@
* (SPRINT16 gate 4: the NIGHTS ladder and the two new briefs are Lane D's * (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.) * edits, by the sprint's explicit grant A owns everything else here.)
* *
* Six nights, one wallet. This is deliberately a thin wrapper around * Seven 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 * 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 * 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 * three questions between rounds which storm is tonight, how much money do you
@ -23,8 +23,9 @@ import { START_BUDGET, HARDWARE } from './contracts.js';
* 2 Southerly Buster gust 20.0, hail the change, and hail, arrive * 2 Southerly Buster gust 20.0, hail the change, and hail, arrive
* 3 Early Buster gust 20.0, hail same teeth, change comes EARLY * 3 Early Buster gust 20.0, hail same teeth, change comes EARLY
* 4 Southerly again gust 20.0, hail the storm you know, the yard you don't * 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 * 5 Wild Night gust 30.0, hail the marquee storm: the most wind of the week
* 6 Pea-Hail Soaker gust 15.2, fine hail the night the default fabric is wrong * 6 Ice Night gust 28.5, hail less wind, worse ice, and the bed is already lost
* 7 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. * 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. Night 3 moves to the corner block * SPRINT10: a night is now a STORM and a SITE. Night 3 moves to the corner block
@ -48,11 +49,11 @@ import { START_BUDGET, HARDWARE } from './contracts.js';
* swing lawn enters the rotation at night 4 under the southerly the ONE * 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 * 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 * 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 * soaker closes the week over the corner block a yard night 3 taught because
* C MEASURED the pairing (THREADS [C] 2026-07-20): the backyard's buyable * 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 * 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 * 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. * flies the new storm (the closer), and they are not the same night.
* *
* THE STRUCTURAL CONSEQUENCE, SAID OUT LOUD (D, filed in THREADS for John's * 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 * gate 6): six slots cannot hold seven nights. Five shipped storms + the
@ -61,13 +62,28 @@ import { START_BUDGET, HARDWARE } from './contracts.js';
* and every other candidate is load-bearing (gentle = the tutorial and the * and every other candidate is load-bearing (gentle = the tutorial and the
* calm-day preload; southerly = the hail intro and the early buster's * 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; * 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 * icenight = the beyond-saving flag, canon). THE WILD NIGHT (storm_02) was out
* of the rotation. Its storm file, its tests, and the pinned backyard * of the rotation for exactly one integration. The honest fix named in that
* separation all stand it is un-flown by the week, not deleted. If that * filing was a SEVEN-night week one entry re-added here, a ruling, not a
* 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. * refactor.
* *
* SPRINT17 gate 0.1 (D, wiring JOHN'S RULING, 2026-07-21): SEVEN NIGHTS
* the wildnight comes back. The slot is between the swing lawn and the
* icenight, and the one-variable law places it, not taste: the wildnight has
* flown the backyard since Sprint 4 (the pinned separation lives in THAT
* yard's site data), so at night 5 the player is back on a yard they have
* rigged three times and the storm is the only new thing. The slot also keeps
* every S16 law intact by construction the swing lawn still debuts under a
* survived storm (night 4), the icenight is still neither new nor final
* (night 6, its dawn breaks book warranty on the soaker's morning), and the
* week's arc reads full / full / full / new-yard / won-but-tattered /
* the-designed-loss / the-fabric-bet: the pyrrhic win now sits directly
* before the night that cannot be won, which is the escalation the six-night
* week had to fake with wording. The icenight's brief reverts to its pre-S16
* sentence ("less wind than last night") because that fact is TRUE again
* wildnight peaks 30.0, icenight 28.5. gradeFor's clean bar moves with the
* ladder in this same commit (my own S16 filing's demand see gradeFor).
*
* Copy lives here as data, per SPRINT11's rule: client flavour is data, not code. * Copy lives here as data, per SPRINT11's rule: client flavour is data, not code.
*/ */
export const NIGHTS = [ export const NIGHTS = [
@ -106,31 +122,51 @@ export const NIGHTS = [
+ 'her dad reckons the crossbar\'s bolted right through, tie to that and save yourself a ' + '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.', + 'post. The veggie bed is her school project, so it comes home alive.',
}, },
{
// SPRINT17 gate 0.1 — THE WILDNIGHT, RESTORED (John's ruling: seven
// nights). Back at the Hendersons', the yard the player has rigged three
// times, so the storm is the only new thing — the one-variable law at the
// marquee slot. This is the pyrrhic-win canon (the pinned separation:
// best buyable line WINS at ~55-64 hp, tattered; bare bed loses), and the
// brief sells exactly that stake in the clients' own voice: they're
// ringing from Cairns about the BED, and they price the sail as the thing
// that spends itself protecting it. "The garden made it; the sail didn't"
// is the ending this night is FOR — the copy promises it as a win.
storm: 'storm_02_wildnight', site: 'backyard_01',
client: 'the Hendersons', addr: '14 Kurrajong St — the backyard',
brief: 'The Hendersons rang from Cairns — the forecast made the news up there. This is the big '
+ 'one, the night the whole week was pointing at. Their words: they\'d rather fly home to a '
+ 'wrecked sail over a living bed than a tidy yard and a dead one. Spend the sail if you '
+ 'have to. Save the garden.',
},
{ {
storm: 'storm_02b_icenight', site: 'backyard_01', storm: 'storm_02b_icenight', site: 'backyard_01',
client: 'the Hendersons', addr: '14 Kurrajong St — the backyard', client: 'the Hendersons', addr: '14 Kurrajong St — the backyard',
// SPRINT16 gate 4 wording note (D): this brief used to open "Less wind than // SPRINT17 gate 0.1 wording note (D): "Less wind than last night" is the
// last night" — true when the wild night (gust 30) sat at night 4. The wild // pre-S16 sentence, back because the FACT is back — the wildnight (gust
// night left the six-night rotation (see the ladder comment), so the ice // 30.0) sits at night 5 again, immediately ahead of this one (28.5). The
// night is now the most wind the week throws; the sentence moved with the // S16 six-night week had no louder night before this, so the brief said
// facts. The teaching half — level with the player that the bed is already // "the most wind of the week" for one integration; the sentence moves
// lost — is untouched. // with the facts, both directions. The teaching half — level with the
brief: 'The most wind of the week and golf-ball ice with it. Last night before they land. ' // player that the bed is already lost — is untouched, as ever.
+ 'Nobody\'s pretending the seedlings ride this one out — keep the sail on its feet and ' brief: 'Less wind than last night and more ice, which is worse for the bed and easier on the rig. '
+ 'the ice off the windows.', + 'Last night before they land. Nobody\'s pretending the seedlings ride this one out — keep '
// SPRINT13 gate 1.4 — A's ruling, DESIGN CANON: night 5's garden is BEYOND + 'the sail on its feet and the ice off the windows.',
// SAVING, by design, at any price. Measured game-true (B's pay table and // SPRINT13 gate 1.4 — A's ruling, DESIGN CANON: the icenight's garden is
// BEYOND SAVING, by design, at any price (ruled at night 5; the S17
// seven-night ladder flies it night 6 — the flag rides the NIGHT, not the
// slot). Measured game-true (B's pay table and
// A's probe, agreeing): bare bed 0.0 DEAD; best buyable garden 27.7; best // A's probe, agreeing): bare bed 0.0 DEAD; best buyable garden 27.7; best
// clean 15.3 — every line in the game sits under WIN's hp >= 50, wallet or // clean 15.3 — every line in the game sits under WIN's hp >= 50, wallet or
// bank. The week's arc WANTS this: full / full / full, won-but-tattered // bank. The week's arc WANTS this: full / full / full, won-but-tattered
// (night 4, the pinned separation), then the night you cannot save — the // (night 5, the pinned separation, restored S17), then the night you cannot save — the
// icenight is where the job stops being about the bed and becomes about // icenight is where the job stops being about the bed and becomes about
// your steel (the line that would hold it needs $105 the shop caps at // your steel (the line that would hold it needs $105 the shop caps at
// 3.20 kN shackles against 3.22/3.25 peaks — C's few-hundredths whisker, // 3.20 kN shackles against 3.22/3.25 peaks — C's few-hundredths whisker,
// kept on purpose). This flag exists so surfaces TEACH the loss instead of // kept on purpose). This flag exists so surfaces TEACH the loss instead of
// implying a better rig existed: verdictFor reads it (main.js) and stops // implying a better rig existed: verdictFor reads it (main.js) and stops
// blaming placement; the brief above levels with the player before they // blaming placement; the brief above levels with the player before they
// spend a dollar. If a retune ever makes night 5 winnable, delete this // spend a dollar. If a retune ever makes this night winnable, delete this
// flag in the same commit or the verdict lies in the other direction. // flag in the same commit or the verdict lies in the other direction.
gardenBeyondSaving: true, gardenBeyondSaving: true,
}, },
@ -270,7 +306,7 @@ export const PAY = {
* A broke-but-repaired-by-dawn night is NEUTRAL by design: not clean (it * A broke-but-repaired-by-dawn night is NEUTRAL by design: not clean (it
* broke), not a warranty (you fixed it mid-storm on your own ladder that IS * broke), not a warranty (you fixed it mid-storm on your own ladder that IS
* the job). And rep NEVER reads the garden hp/win/gardenBeyondSaving are not * the job). And rep NEVER reads the garden hp/win/gardenBeyondSaving are not
* inputs, so night 5's designed loss cannot cost rep by construction rather * inputs, so the icenight's designed loss cannot cost rep by construction rather
* than by exemption. a.test pins that from both directions. * than by exemption. a.test pins that from both directions.
* *
* Money: `multiplier(rep)` scales the callout fee at QUOTE TIME quote() and * Money: `multiplier(rep)` scales the callout fee at QUOTE TIME quote() and
@ -338,10 +374,24 @@ export const BROKE_BELOW = HARDWARE[0].cost * 4;
* client's garden died four times", and until this was measured nobody knew the * client's garden died four times", and until this was measured nobody knew the
* game could even produce that ending. * game could even produce that ending.
* *
* @param {number} held gardens saved, 0..5 * SPRINT17 gate 0.1 (D, in A's file, flagged revert-and-tell-me standard):
* the clean bar is now DERIVED, not the constant 5. My own S16 filing said a
* seven-night week needs gradeFor re-ruled in the same commit as the ladder,
* and this is that commit. The S16 coincidence gradeFor(5)='clean' landing
* exactly on the six-night week's best outcome (5 won + the designed loss)
* breaks at seven nights: held>=5 would print "THE WEEK HELD everything's
* still where you put it" over a savable garden that died, which is the end
* card lying, the one thing it exists not to do. So 'clean' is the RULE the
* constant was standing in for: every garden the week let you save. The
* scraped/solvent boundary (>=3) is untouched moving it is a taste call on
* A's wording and there's no lie in leaving it (3 of 6 savable is still "the
* least wrong call most nights"); re-rule it if you want, it's yours.
*
* @param {number} held gardens saved, 0..savable
*/ */
export function gradeFor(held) { export function gradeFor(held) {
if (held >= 5) return 'clean'; const savable = NIGHTS.filter((_, i) => !nightAt(i).gardenBeyondSaving).length;
if (held >= savable) return 'clean';
if (held >= 3) return 'scraped'; if (held >= 3) return 'scraped';
return 'solvent'; return 'solvent';
} }