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>
This commit is contained in:
type-two 2026-07-20 21:49:58 +10:00
parent 1073ac97e8
commit d8e7087c2c
4 changed files with 189 additions and 52 deletions

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.',
},
];
/**