Lane A S18: 👁️ played the callout — five bugs 535 green asserts could not see, all fixed and pinned; gate 5's A half
Eyes on the cards found what the suite structurally cannot:
· the board advertised $57 while the dispatch quoted $114 one click apart — the
quote-vs-settle lie walking back in through an unreachable closure inside
boot(). pricedOffers is now the exported pure priceOffer(), rigRecordFor's
precedent, and the bug has its own pin.
· the emergency wore the same grey "callout" tag as the Tuesday alternative
beside it — the tell rule broken on the surface the tell rule is for.
· the invoice argued with itself: the ledger said CB1 "is not on you" and the
verdict blamed CB1. verdictFor now reads `yours`, and the ruling is extracted
as answerableFor() so the RULING is pinnable, not just its consequences.
· the shop lied by $20 — the session charged me for the cowboy's carabiners.
· "the Hendersons IS expecting you", live, on the ruling's headline line.
Gate 5 (A's half): collateralKeyClashes() turns D's second-rust-post landmine
into a tripwire carrying both harnesses' numbers. It reports and does not throw
(D and E are authoring in parallel), and it deliberately does NOT make
exposureOf dedupe — the card is the honest side, the failure route is what needs
per-instance keys, and matching a bug converts a loud disagreement into a quiet
lie.
⚠️ RETRACTED IN THREADS: my $180 balance measurement is contaminated. C found
SailRig.step accepts `t` and never reads it, so setPhase('storm',{at:30}) moves
the sky and not the cloth (4.12× understated load, 93× light rain). B must not
rule the garden bonus against my $38. The TELL survives — CB1 still blows at
t≈33 — the ECONOMY does not.
Selftest 539/0/0 — 522 + 17, the exact sum. 19 mutations, all red with their own
words, controls green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
82a52aa014
commit
397d9435bc
159
THREADS.md
159
THREADS.md
@ -9787,3 +9787,162 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
over an unrigged yard each produce their own accusation). Your yard debuts as an ordinary
|
||||
rig-for-tonight job; DESIGN.md's sentence belongs to a later week. Flag me when it is placeable
|
||||
and gate 5 is next on my list.
|
||||
|
||||
[A] 2026-07-25 — 👁️ **I PLAYED THE EMERGENCY, AND LOOKING AT THE CARDS FOUND FIVE BUGS 535 GREEN
|
||||
ASSERTS COULD NOT SEE.** All five fixed, all five pinned, two of them by EXTRACTING the thing
|
||||
that was wrong so the ruling itself became assertable. Recording them together because they
|
||||
are one lesson: *every one of them lived on a surface, and the suite has no eyes.*
|
||||
|
||||
Real path throughout, on my own tab at :8824: splash → board → click the emergency → DISPATCH
|
||||
→ ROLL → storm at t=30, fast-forwarded with `SHADES.step(1/60)` in a loop (the repo's own
|
||||
path) → invoice. One disclosed harness move: `week.advance()` ×4 to reach night 5's morning,
|
||||
which does not EARN nights 1–4 — the same class of seek as `boot({site, bank})`, and the bank
|
||||
reads the honest $80 of night one throughout.
|
||||
|
||||
**BUG 1 — THE BOARD ADVERTISED $57 AND THE DISPATCH QUOTED $114, one click apart.** The
|
||||
emergency term never reached the offer card: `pricedOffers` was an inline closure inside
|
||||
`boot()`, behind a WebGL context where no assert can reach it, and it simply had not been given
|
||||
the fourth argument. **This is the quote-vs-settle lie that `calloutFee()` was extracted to
|
||||
make impossible, and it walked straight back in through the one surface no test could see.** So
|
||||
`priceOffer(night, {def, site, rep, remembers})` is now exported and pure — the `rigRecordFor`
|
||||
precedent, verbatim, for exactly the reason that function's own note gives. Pinned both ways
|
||||
(an ordinary night prices as it always did; the emergency prices at ×2; and what the card
|
||||
prints equals what `week.quote()` quotes).
|
||||
|
||||
**BUG 2 — THE EMERGENCY WORE THE SAME GREY "callout" TAG AS THE TUESDAY ALTERNATIVE BESIDE
|
||||
IT.** Double rates, no prep, no quote, somebody else's rig — the biggest new thing in the game
|
||||
— and the board said nothing until one click later. That is DESIGN.md's tell rule broken on the
|
||||
exact surface the tell rule exists for. It now has its own tag (`right now · emergency`), its
|
||||
own red left border against the constraint's amber (different KINDS of fact: amber is a client
|
||||
asking something of you, red is weather already happening to somebody), one line naming what is
|
||||
different (`storm already running · no prep · no quote · you arrive 30s in`), and the doubled
|
||||
number carries `· 2× emergency rate` beside it, because money with no stated cause is what
|
||||
every ledger comment in this repo is about.
|
||||
|
||||
**BUG 3 — THE INVOICE ARGUED WITH ITSELF ABOUT THE SAME CORNER.** The ledger said *"CB1 —
|
||||
carabiner, already on the ground when you arrived · not billed · That corner is what the
|
||||
callout was for. It is not on you"* and four lines below it the verdict said *"the carabiner at
|
||||
CB1 went first and took 1 more with it."* `verdictFor` was being handed `lost` instead of
|
||||
`yours`. Fixed — and this is what made me extract **`answerableFor(corners, emergency)`**, so
|
||||
the ruling is a pure exported function with its own pin rather than four call sites each
|
||||
remembering it. The corner COUNT still reports every corner down; the verdict speaks only of
|
||||
work you are answerable for. Two different facts, both honest.
|
||||
|
||||
**BUG 4 — THE SHOP LIED BY $20.** The session charged me for the cowboy's four carabiners:
|
||||
budget read $60 against a bank of $80. The ledger was fine (`spentThisNight` is forced to 0)
|
||||
but `run.budgetLeft` and every refusal B prices off `budget` read that number. Now pre-banked
|
||||
by `week.bank + theirCost + SPARE_COST`, computed FROM THE RIG DATA so B can author rated
|
||||
shackles into a cowboy's rig without the line silently going wrong, and the budget lands back
|
||||
on exactly `week.bank`. You arrive having spent nothing, because you did.
|
||||
|
||||
**BUG 5 — "the Hendersons IS expecting you"**, live, on the one line John's ruling says must be
|
||||
readable before the choice. I had agreed the verb with the NUMBER OF CLIENTS STOOD UP, which is
|
||||
the wrong thing to agree with: every client in this game is a collective noun whose number no
|
||||
code here can infer, and the repo's own voice already treats them as plural (night 7's brief:
|
||||
*"The Vasilaros place again — THEY asked for you by name"*). No agreement logic at all now;
|
||||
"are", always, which is also the spec's own example wording.
|
||||
|
||||
── **THE NIGHT AS PLAYED, and the number B and John need** ─────────────────────────────
|
||||
|
||||
Night 5's board, live: **the wildnight, booked — $73, $115 at risk, golf-ball hail 11 s, 80
|
||||
mm/hr.** Beside it: **the emergency — $114 at 2× rate, $205 at risk (the carport), "storm
|
||||
already running · no prep · no quote · you arrive 30s in", and "the Hendersons are expecting
|
||||
you · −½★ standing".** That is a genuinely hard morning and the dodge finally has a printed
|
||||
price. The DISPATCH then lists the rig you didn't build corner by corner — CB1 carabiner **ON
|
||||
THE GROUND**, TR1/TR1B/Q3 carabiners — names the rate, and refuses to total anything.
|
||||
|
||||
The storm entered at `phaseT` 30 with `windTime` 30, the right storm, the right yard, four
|
||||
corners up and one spare in the ute. **CB1 blew at t≈33, through the REAL overload path** — a
|
||||
$5 carabiner on the 0.22 carport beam, ~264 N effective against a southerly — which is the
|
||||
night's premise arriving on its own without anybody poking a flag. Q3 followed later. Garden
|
||||
84%, 2/4 corners standing, verdict "THE GARDEN MADE IT. The sail didn't."
|
||||
|
||||
⚠️ **THE BALANCE MEASUREMENT, AND I AM REPORTING IT RATHER THAN QUIETLY RETUNING CANON.**
|
||||
That night banked **+$180 and took the wallet $80 → $260**, from four lines:
|
||||
|
||||
callout fee (2× rate) +$114 garden bonus — 84% of the bed +$38
|
||||
clean site +$20 gear recovered (the spare only) +$8
|
||||
spent on the rig $0 standing −1.0★
|
||||
|
||||
For scale, D's cold play measured the wildnight at **+$24** and the week's best ordinary night
|
||||
at **+$104**. So the emergency is the biggest single night in the game by ~75%, it required no
|
||||
prep skill, and its only price is half a star. **A fee-maximizer takes it every time.** Three
|
||||
things are true about that and they belong to three different people:
|
||||
· **the ×2 is CANON** (DESIGN.md line 29, ROADMAP arc 2, in those words) and I am not re-ruling
|
||||
a number the document has carried since line one to fix my own feature's balance. Flagged
|
||||
UNMEASURED-BY-PLAY, cut-first, and per-job `pay.emergencyRate` is the next move if it must
|
||||
move — not a trim of `feeFor`, which carries measured evidence.
|
||||
· **the $38 GARDEN BONUS IS B's RULING, and here is its number.** SPRINT18 gate 1.2 gives B
|
||||
*"whether the garden bonus applies at all"* — you had no prep phase to protect a garden with.
|
||||
$38 of $180 is 21% of the night, so B: this is the single largest lever on emergency balance
|
||||
and you now have the measurement to rule it with.
|
||||
· **the $20 CLEAN BONUS is a question I am RAISING AND NOT RULING.** It paid because the
|
||||
client's property survived, which is what the bonus is defined to mean. But "you left their
|
||||
place the way you found it" over a yard you arrived at mid-collapse is at least arguable, and
|
||||
`noCollateralBonus` lives in PAY with measured neighbours — cutting it unilaterally to pay
|
||||
for my own feature's balance is precisely the move this repo keeps catching. For gate 6.
|
||||
|
||||
**AND THE ONE THING THAT WENT EXACTLY RIGHT, unprompted:** collateral billed **$0**. The
|
||||
carport — $180, the trap the client has been selling since night 3 — was NOT charged, because
|
||||
the corner holding it arrived on the ground. The invoice split the blame in its own words
|
||||
without anybody at the keyboard reminding it to. That is the ruling working, and it is the
|
||||
difference between a job you were called out to and a job you failed.
|
||||
|
||||
[A] 2026-07-25 — ⚠️ **RETRACTION, SAME DAY: MY $180 BALANCE MEASUREMENT ABOVE IS CONTAMINATED AND
|
||||
MUST NOT BE ACTED ON. C FOUND THE VARIABLE AND IT IS NOT IN MY FILE.** `SailRig.step(dt, wind,
|
||||
t)` accepts `t` and never reads it — sail.js passes its OWN fixed-step clock, which `attach()`
|
||||
zeroes — so **`setPhase('storm', {at: 30})` moves the sky, the garden, the debris and the
|
||||
player, and does not move the CLOTH.** C measured it on my own shipped emergency: at the moment
|
||||
you get out of the ute the sky blows **9.074 m/s** and the fabric feels **4.468** (a **4.12×
|
||||
understatement** of load, which goes with v²), and the sky rains **5.000 mm/hr** while the pond
|
||||
fills at **0.054** — 93× light. The one object in the yard that can FAIL is the one object that
|
||||
never heard about `arriveAt`. The fix is one line and it is B's; C has relayed it.
|
||||
|
||||
**So every number in my play-through entry above is soft in the same direction: the callout
|
||||
read EASY for a false reason.** Garden 84%, two corners standing, garden bonus $38, +$180 —
|
||||
all of that was earned against a sail feeling a quarter of the load and none of the rain.
|
||||
Concretely, and please read this before touching a constant:
|
||||
· **B — do NOT rule the garden bonus against my $38.** That was my one deliverable to your
|
||||
gate-1.2 ruling and it is void. Re-measure after your clock fix; the shape of the question is
|
||||
unchanged (no prep phase, no garden to have protected) but the size of it is unknown.
|
||||
· **Nobody tune `emergencyRate`, triage pay, or the callout's difficulty against the current
|
||||
feel.** ×2.0 stays where DESIGN.md put it. My "biggest night in the game by ~75%" claim is
|
||||
withdrawn as a measurement; it survives only as a QUESTION for gate 6.
|
||||
· **Integrator — re-check the callout's feel after B's clock fix merges**, and treat the
|
||||
economy paragraph in my play-through as un-run. The mutation receipts and the bug fixes in
|
||||
that entry all stand; only the balance numbers are contaminated.
|
||||
|
||||
**WHAT SURVIVES THE CONTAMINATION, and it is worth saying because it is the night's premise:**
|
||||
**CB1 still blew at t≈33.** A $5 carabiner on the 0.22 carport beam is so far under the night
|
||||
that it fails even at 0.24× load — so the emergency's opening beat (you walk up the drive and
|
||||
the carport corner is going) is real, not an artefact, and D's knife has a genuine target
|
||||
waiting for it. The TELL holds; the ECONOMY does not. Those are separable and only one of them
|
||||
is broken.
|
||||
|
||||
**MY §4 STANDS, and C pinned the property it rests on rather than taking my word for it:** the
|
||||
wind field is closed-form and ORDER-INDEPENDENT — they read t=30 cold, after walking 0→30, and
|
||||
after scrambling 90/0/45/12/88/30/1/−5, byte-identical on every storm across five observables.
|
||||
So `phaseT = at` really is sufficient for everything that takes `t` as an argument, which was
|
||||
the load-bearing claim in the published shape. The gap was never the phase machine; it was one
|
||||
function accepting a parameter it ignored. **SPRINT13's bug arriving through a new door**, in
|
||||
C's words, and the reason a seam gets published early is so somebody else can measure it.
|
||||
|
||||
**C's DEBRIS FINDINGS, noted as the phase's owner (nothing for me to fix).** `clear()` rewound
|
||||
the leaf accumulators but not the PRNG, so night 2 of the shipped week has never flown night
|
||||
1's crates; and an entry at t=45 had every later crate wearing the skipped crate's
|
||||
height/spin/phase. Both closed structurally in C's tree, and **my `arriveAt: 30` was green only
|
||||
by COINCIDENCE — the earliest debris event is t=38.** What it changes for the callout, and it
|
||||
is a design fact rather than a bug: a t=0 run has 6 leaves crossing the grass at t=30, and a
|
||||
cold arrival has **none for ~7 s**. That moving grass is the gust front's tell, so a cold
|
||||
arrival is quieter than the same instant reached by living through it — worth a look, once the
|
||||
cloth clock is honest, at whether the dispatch's "what you can see from the drive" should be
|
||||
doing that work instead.
|
||||
|
||||
**AND C's HALF OF MY §8 LANDED ON THE EXACT PATH I PUBLISHED**, which is the seam pattern
|
||||
paying for itself twice in one day: `midStormRead(def, t)` called through
|
||||
`weather.midStormRead?.()` under a namespace import — verified by them on that call path, not
|
||||
assumed — and `dispatchHonest(def, t)` extending the honesty gate over emergency storms.
|
||||
Their walk reads `board.EMERGENCIES` through a namespace import so c.test does not hard-fail
|
||||
on a table that only exists in my tree, and **`corner_block_emergency` passes both arms**
|
||||
(dispatchHonest OK, pairing legal). Nothing of mine has to move for their half to go live; the
|
||||
dispatch card already maps the ordered list.
|
||||
|
||||
@ -653,6 +653,69 @@ export function exposureOf(site) {
|
||||
return { total, items };
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ **SPRINT18 gate 5 (A's half) — THE SECOND-RUST-POST LANDMINE, DISARMED AS A
|
||||
* TRIPWIRE.** D's filing, S17 gate 3.2, with a measured receipt from the other
|
||||
* Lane-D session: two structures in one yard that share a baked `collateralKey`
|
||||
* make the two exposure harnesses DISAGREE, and one of them bills wrong.
|
||||
*
|
||||
* THE MEASUREMENT, D's, on a real two-corroded-post yard:
|
||||
* · `exposureOf()` (this file, the OFFER CARD) read **$115** — per STRUCTURE.
|
||||
* · `collateralExposure` (B/C, the FAILURE route) read **$45** — per KEY, deduped,
|
||||
* because `collateralFor`/`wreckStructure` match the FIRST structure carrying a
|
||||
* key and C's failure envelope dedupes keys.
|
||||
* So the second post's failure would bill once and wreck the wrong post: **a free
|
||||
* failure in a rust costume**, which is the exact bug class that hid the carport's
|
||||
* $180 for five sprints.
|
||||
*
|
||||
* ⚖️ **WHY THIS REPORTS AND DOES NOT THROW, and why it does not "fix" `exposureOf`.**
|
||||
* Two deliberate refusals:
|
||||
* 1. **No throw.** D and E are authoring yards in parallel right now. A hard
|
||||
* failure in a shared path would soft-lock somebody else's tree mid-sprint over
|
||||
* a bug that is ARMED, not firing (the pool yard ships exactly ONE post on
|
||||
* purpose and pins it). It accuses instead — C's `pairingRefusals`
|
||||
* construction, and their reasoning applies unchanged.
|
||||
* 2. **I did not make `exposureOf` dedupe by key.** That would make the two
|
||||
* harnesses agree, which is tempting and wrong: a yard with two corroded posts
|
||||
* genuinely has $90 of corroded post in it, so the CARD is the honest one and
|
||||
* the FAILURE path is the side that needs per-instance keys. Making the card
|
||||
* under-report to match a bug is how you convert a loud disagreement into a
|
||||
* quiet lie, and it would have deleted D's receipt in the process.
|
||||
*
|
||||
* **THE REAL FIX, for the joint E+A pass:** per-instance collateral keys — the
|
||||
* anchor naming the STRUCTURE it will wreck, not the KIND of thing it is. That
|
||||
* needs E, because the key is baked into the GLB extras and the manifest. This
|
||||
* function is the landing pad: it goes red the moment a yard author ships the
|
||||
* second post, which is the whole of "disarm it before somebody trips it".
|
||||
*
|
||||
* @param {object} site a site JSON
|
||||
* @returns {string[]} one accusation per clashing key, with its measurement
|
||||
*/
|
||||
export function collateralKeyClashes(site) {
|
||||
/** @type {Map<string, object[]>} */
|
||||
const byKey = new Map();
|
||||
for (const s of site?.structures ?? []) {
|
||||
// No key ⇒ world.js's collateralFor resolves it by `id`, which is unique by
|
||||
// construction (the editor generates s1, s2, …) — so an unkeyed structure
|
||||
// cannot clash and must not be reported as if it could.
|
||||
if (!s.collateralKey) continue;
|
||||
if (!byKey.has(s.collateralKey)) byKey.set(s.collateralKey, []);
|
||||
byKey.get(s.collateralKey).push(s);
|
||||
}
|
||||
const out = [];
|
||||
for (const [key, group] of byKey) {
|
||||
if (group.length < 2) continue;
|
||||
const perStructure = group.reduce((n, s) => n + (s.collateralValue ?? 0), 0);
|
||||
const perKey = group[0].collateralValue ?? 0;
|
||||
out.push(`${site?.name ?? site?.id ?? 'this yard'}: ${group.length} structures share the `
|
||||
+ `collateral key '${key}' (${group.map((s) => s.id).join(', ')}). The offer card would read `
|
||||
+ `$${perStructure} (per structure) and the failure route $${perKey} (per key, deduped) — `
|
||||
+ `so ${group.length - 1} of them would fail FREE and wreck '${group[0].id}' instead. `
|
||||
+ 'Per-instance collateral keys are needed first (SPRINT18 gate 5, E+A).');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* BUILD THE MORNING'S BOARD — two offers, tonight's scripted night first.
|
||||
*
|
||||
|
||||
@ -19,7 +19,7 @@ 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 { NIGHTS, REP } from './week.js';
|
||||
import { NIGHTS, PAY, 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,
|
||||
@ -370,6 +370,11 @@ const CSS = `
|
||||
margin-top:7px; font-size:11px; font-style:italic; color:#8a7f74;
|
||||
}
|
||||
#hud-card .board-note.free { color:#8ba0ad; }
|
||||
/* 🚨 SPRINT18 gate 1 — an emergency on the board. Red-side left border against the
|
||||
constraint's amber, because they are different KINDS of fact: amber is a client
|
||||
asking something of you, red is weather already happening to somebody. */
|
||||
#hud-card .offer.emergency { border-left:3px solid #ff8f86; }
|
||||
#hud-card .offer .line.siren span { color:#ff8f86; letter-spacing:.04em; }
|
||||
|
||||
/* --- 🚨 SPRINT18 gate 1: THE DISPATCH ------------------------------------
|
||||
The paper chain's fourth document, and the design brief for it is one line:
|
||||
@ -889,13 +894,29 @@ export function createHud(d) {
|
||||
// The .hail line is lit brighter because it is the one a player must
|
||||
// not skim past — the soaker's whole trap is a gentle-reading wind.
|
||||
return `
|
||||
<button class="offer${cons.length ? ' constrained' : ''}" data-offer="${i}">
|
||||
<span class="tag">${o.kind === 'scripted' ? 'tonight, as booked' : 'callout'}</span>
|
||||
<button class="offer${cons.length ? ' constrained' : ''}${n.emergency ? ' emergency' : ''}" data-offer="${i}">
|
||||
${/* 🚨 SPRINT18 gate 1 — AN EMERGENCY MUST NOT LOOK LIKE AN ORDINARY
|
||||
CALLOUT, and my first cut let it. On the live board it wore the same
|
||||
grey "callout" tag as the Tuesday alternative beside it: double
|
||||
rates, no prep, no quote, somebody else's rig — the biggest new
|
||||
thing in the game — and the card said nothing. A player would find
|
||||
out one click later, which is the tell rule broken on the exact
|
||||
surface the tell rule exists for.
|
||||
So: its own tag, its own left border, and one line naming what is
|
||||
different about it, above the money. */''}
|
||||
<span class="tag">${
|
||||
n.emergency ? 'right now · emergency' : o.kind === 'scripted' ? 'tonight, as booked' : 'callout'
|
||||
}</span>
|
||||
<div class="who">${n.client ?? 'no client'}</div>
|
||||
<div class="addr">${n.addr ?? ''}</div>
|
||||
${n.emergency ? `<div class="line siren"><span>storm already running · no prep · no quote · you arrive ${
|
||||
n.emergency.arriveAt}s in</span></div>` : ''}
|
||||
${(o.band ?? []).map((l) => `<div class="line wx ${l.key}"><span>${l.text}</span></div>`).join('')}
|
||||
${o.fee != null ? `<div class="line fee"><span>the job pays${
|
||||
o.premium ? ` · +${Math.round(o.premium * 100)}% on their terms` : ''
|
||||
}${/* the doubled number gets its reason printed beside it — money with no
|
||||
stated cause is the thing every ledger comment in this repo is about */''}${
|
||||
n.emergency ? ` · ${PAY.emergencyRate}× emergency rate` : ''
|
||||
}</span><b>$${o.fee}</b></div>` : ''}
|
||||
${o.exposure != null ? `<div class="line risk"><span>their property at risk${
|
||||
o.exposureItems?.length ? ` — ${o.exposureItems.map((x) => x.what).join(', ')}` : ''
|
||||
@ -912,9 +933,19 @@ export function createHud(d) {
|
||||
morning) `standsUp` is empty and this renders NOTHING — the free
|
||||
case reads as free because there is no line, not because a line
|
||||
says "$0". */''}
|
||||
${/* ⚠️ "ARE", ALWAYS, and it is not a typo I am defending. My first cut
|
||||
agreed the verb with the NUMBER OF CLIENTS STOOD UP and rendered
|
||||
"the Hendersons IS expecting you" on the live board — bad English
|
||||
on the one line John's ruling says must be readable before the
|
||||
choice. The count is the wrong thing to agree with: every client in
|
||||
this game is a collective noun (a household, a place), whose number
|
||||
no code here can infer, and the repo's own voice already treats them
|
||||
as plural — night 7's brief says "The Vasilaros place again — THEY
|
||||
asked for you by name". The spec's own example wording is "the
|
||||
Hendersons are expecting you". So: no agreement logic at all. */''}
|
||||
${o.standsUp?.length ? `<div class="line cost"><span>${
|
||||
o.standsUp.map((s) => s.client ?? 'a client').join(' and ')
|
||||
} ${o.standsUp.length > 1 ? 'are' : 'is'} expecting you</span><b>${
|
||||
} are expecting you</span><b>${
|
||||
starDelta(REP.STOOD_UP * o.standsUp.length)} standing</b></div>` : ''}
|
||||
${cons.length ? `<div class="cons">⚖ ${cons.map((c) => c.label).join(' · ')}</div>` : ''}
|
||||
${cons.map((c) => `<div class="says">“${c.says}”</div>`).join('')}
|
||||
|
||||
@ -357,6 +357,94 @@ export function rigRecordFor(corners, tally = new Map(), theirs = new Set()) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* WHAT AN OFFER CARD PRINTS — pure, exported, and it did not use to be either.
|
||||
*
|
||||
* ⚠️ **EXTRACTED BECAUSE IT SHIPPED A LIVE BUG THE SUITE COULD NOT SEE.** This was
|
||||
* an inline closure inside `boot()`, unreachable by any assert, and on night 5's
|
||||
* real board it advertised the emergency callout at **$57 while the dispatch one
|
||||
* click later quoted $114** — it had simply never been given the emergency term.
|
||||
* That is exactly the quote-vs-settle lie this file's own comment says the board
|
||||
* must not be able to tell ("the board can never be more confident than the
|
||||
* sheet"), and it was invisible to 535 asserts because the money lived behind a
|
||||
* WebGL context. Eyes on the card found it; being a function is what stops the
|
||||
* next one.
|
||||
*
|
||||
* The rule it now keeps by construction: **every number on an offer card comes
|
||||
* from the same function the job sheet and the invoice use.** `calloutFee` for the
|
||||
* money, C's `forecastLines`/`offerBand` for the weather, `exposureOf` for the
|
||||
* risk — nothing retyped here.
|
||||
*
|
||||
* @param {object} night a normalised night (week.js normaliseNight)
|
||||
* @param {object} ctx
|
||||
* @param {object} [ctx.def] the storm def; absent ⇒ the weather lines degrade away
|
||||
* @param {object} [ctx.site] the site JSON, for the exposure walk
|
||||
* @param {number} ctx.rep standing as of this morning — the fee is priced on it
|
||||
* @param {object[]} [ctx.remembers] this client's grudges (week.remembers)
|
||||
*/
|
||||
export function priceOffer(night, { def = null, site = null, rep = 3, remembers = [] } = {}) {
|
||||
const exp = exposureOf(site);
|
||||
return {
|
||||
// C's honest lines at lead 0 — tonight, exact. The same call the job sheet
|
||||
// makes, so the board can never be more confident than the sheet.
|
||||
forecast: def ? forecastLines(def, 0) : null,
|
||||
// SPRINT17 [C], applied by the integrator: the ORDERED band the offer card
|
||||
// maps, so a night's hail can never be dropped by the card picking two
|
||||
// fields. Night 7 read softer than the buster on wind+stones alone (55 vs 76
|
||||
// km/h) while carrying 6x the hail — the board recommending its own trap.
|
||||
band: def ? offerBand(def, 0) : null,
|
||||
// 🚨 SPRINT18 gate 1 — the fourth argument IS the bug above, fixed: an
|
||||
// emergency pays double ON THE CARD, because the card is where the player
|
||||
// decides whether double is worth it.
|
||||
fee: def ? calloutFee(def, rep, night.constraints, !!night.emergency) : null,
|
||||
exposure: exp.total,
|
||||
exposureItems: exp.items,
|
||||
remembers,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚨 **SPRINT18 gate 1 — ⚖️ WHO IS ANSWERABLE FOR WHICH CORNER.** The ruling that
|
||||
* makes an emergency invoice honest, as a pure function so that the RULING is
|
||||
* pinnable and not merely its consequences.
|
||||
*
|
||||
* Exported for the `rigRecordFor` reason, stated in that function's own note:
|
||||
* scoreRun lives inside `boot()` behind a WebGL context where no assert can reach
|
||||
* it, and this is exactly the kind of rule that would otherwise only ever be
|
||||
* "tested" by a green suite that never ran it. It was worth extracting twice over,
|
||||
* because rendering the first real emergency invoice found TWO bugs downstream of
|
||||
* this split — the verdict blaming an inherited corner, and the gnome billable off
|
||||
* corners the player never chose.
|
||||
*
|
||||
* **A corner that was already down when you arrived is the JOB, not your failure.**
|
||||
* It bills nothing, wrecks nothing, books no warranty and costs no standing — it is
|
||||
* the REASON there is a callout. Without the split, an emergency night charges you
|
||||
* $180 for the carport the cowboy was already losing, bills his $5 carabiner, docks
|
||||
* your standing for his corner, and books you a free warranty callout tomorrow to
|
||||
* go and fix his work: four surfaces, all agreeing, all wrong.
|
||||
*
|
||||
* Two lists out, deliberately, because they answer two different questions:
|
||||
* · `lost` — every corner down. REPORTING (the HUD's corners-intact count). It
|
||||
* must stay honest: reading 4/4 over a sail with three corners on the ground
|
||||
* would be its own lie.
|
||||
* · `yours` — the ones you are ANSWERABLE for. Billing, collateral, the subtitle
|
||||
* and the verdict read this.
|
||||
*
|
||||
* @param {Array} corners rig.corners at dawn
|
||||
* @param {object|null} emergency the night's `emergency` block, if it is one
|
||||
*/
|
||||
export function answerableFor(corners, emergency = null) {
|
||||
const theirs = new Set((emergency?.rig ?? []).filter((c) => c.broken).map((c) => c.anchorId));
|
||||
const lost = (corners ?? []).filter((c) => c.broken);
|
||||
return {
|
||||
theirs,
|
||||
lost,
|
||||
yours: lost.filter((c) => !theirs.has(c.anchorId)),
|
||||
inherited: lost.filter((c) => theirs.has(c.anchorId))
|
||||
.map((c) => ({ anchorId: c.anchorId, hw: c.hw?.name ?? '?' })),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase machine
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -908,12 +996,7 @@ export async function boot(opts = {}) {
|
||||
* subtitle read this.
|
||||
*/
|
||||
const em = week.job.emergency;
|
||||
const theirs = new Set((em?.rig ?? []).filter((c) => c.broken).map((c) => c.anchorId));
|
||||
const lost = rig.corners.filter((c) => c.broken);
|
||||
const yours = lost.filter((c) => !theirs.has(c.anchorId));
|
||||
/** Their corners still on the ground at dawn — the invoice names them, unbilled. */
|
||||
const inherited = lost.filter((c) => theirs.has(c.anchorId))
|
||||
.map((c) => ({ anchorId: c.anchorId, hw: c.hw.name }));
|
||||
const { theirs, lost, yours, inherited } = answerableFor(rig.corners, em);
|
||||
const bill = yours.reduce((s, c) => s + c.hw.cost, 0);
|
||||
const collateral = [];
|
||||
// The gnome is collateral if the sail came down over it. DESIGN.md: the
|
||||
@ -970,7 +1053,19 @@ export async function boot(opts = {}) {
|
||||
*/
|
||||
const win = hp >= 50;
|
||||
const dmg = garden.damage;
|
||||
const { verdict, mode } = verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped,
|
||||
/**
|
||||
* ⚠️ **`yours`, NOT `lost` — AND THIS WAS A LIVE BUG ON THE FIRST EMERGENCY
|
||||
* INVOICE I RENDERED.** With `lost` the verdict narrated the corner the player
|
||||
* INHERITED as their failure: the ledger said *"CB1 — already on the ground when
|
||||
* you arrived · not billed · That corner is what the callout was for. It is not
|
||||
* on you"* and four lines below it the verdict said *"the carabiner at CB1 went
|
||||
* first and took 1 more with it."* One card, two contradictory accounts of the
|
||||
* same corner — the invoice arguing with itself, which is the defect class this
|
||||
* repo has spent five sprints hunting. The verdict speaks about work you are
|
||||
* answerable for; the corner count above it still reports every corner down,
|
||||
* because those are two different facts and both papers say so honestly.
|
||||
*/
|
||||
const { verdict, mode } = verdictFor({ hp, lost: yours, win, dmg, pondPeak, pondDumped,
|
||||
beyondSaving: week.job.gardenBeyondSaving });
|
||||
|
||||
// SPRINT16 gate 2.2 [Lane B] — the fabric bet on the record. The F key
|
||||
@ -1118,32 +1213,18 @@ export async function boot(opts = {}) {
|
||||
*/
|
||||
async function pricedOffers() {
|
||||
for (const o of week.offers()) await siteJsonFor(o.night.site);
|
||||
return week.offers((night) => {
|
||||
const def = defs[night.storm];
|
||||
const exp = exposureOf(siteJson.get(night.site));
|
||||
return {
|
||||
// C's honest lines at lead 0 — tonight, exact. The same call the job
|
||||
// sheet makes, so the board can never be more confident than the sheet.
|
||||
forecast: def ? forecastLines(def, 0) : null,
|
||||
// SPRINT17 [C], applied by the integrator: the ORDERED band the offer
|
||||
// card maps, so a night's hail can never be dropped by the card picking
|
||||
// two fields. Night 7 read softer than the buster on wind+stones alone
|
||||
// (55 vs 76 km/h) while carrying 6× the hail — the board recommending
|
||||
// its own trap. offerBand carries every fact the storm has.
|
||||
band: def ? offerBand(def, 0) : null,
|
||||
fee: def ? calloutFee(def, week.rep, night.constraints) : null,
|
||||
exposure: exp.total,
|
||||
exposureItems: exp.items,
|
||||
// ⚖️ SPRINT18 gate 0, clause 3 — THE STOOD-UP CLIENT REMEMBERS. Injected
|
||||
// here with the rest of the per-offer facts because the memory is week
|
||||
// state and board.js is pure: same seam as the fee and the exposure, and
|
||||
// the card renders nothing when the list is empty (the degradation rule
|
||||
// every other injected field follows). A client you left waiting on
|
||||
// Tuesday and who is on the board again on Thursday is the whole of
|
||||
// "your work follows you", and it costs one line.
|
||||
remembers: week.remembers(night.client),
|
||||
};
|
||||
});
|
||||
return week.offers((night) => priceOffer(night, {
|
||||
def: defs[night.storm],
|
||||
site: siteJson.get(night.site),
|
||||
rep: week.rep,
|
||||
// ⚖️ SPRINT18 gate 0, clause 3 — THE STOOD-UP CLIENT REMEMBERS. Injected
|
||||
// with the rest of the per-offer facts because the memory is week state and
|
||||
// board.js is pure: same seam as the fee and the exposure, and the card
|
||||
// renders nothing when the list is empty (the degradation rule every other
|
||||
// injected field follows). A client you left waiting on Tuesday who is on
|
||||
// the board again on Thursday is the whole of "your work follows you".
|
||||
remembers: week.remembers(night.client),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1196,7 +1277,23 @@ export async function boot(opts = {}) {
|
||||
async function adoptEmergencyRig(em) {
|
||||
const s = rigging.session;
|
||||
resetRig();
|
||||
s.setBudget(week.bank + SPARE_COST);
|
||||
/**
|
||||
* ⚠️ **THE ARITHMETIC, AND THE BUG IT FIXES — CAUGHT LIVE, NOT BY A TEST.** The
|
||||
* first cut banked the session at `week.bank + SPARE_COST` and the real game came
|
||||
* up reading **budget $60 against a bank of $80**: the session had charged me $20
|
||||
* for the cowboy's four carabiners. The LEDGER was fine (`spentThisNight` is
|
||||
* forced to 0 on the ROLL) but the SHOP was lying by $20 — and `run.budgetLeft`,
|
||||
* plus every refusal B prices off `budget`, reads that number.
|
||||
*
|
||||
* You arrive having spent NOTHING: their steel is not yours, you did not buy it,
|
||||
* and the ute's spare is free. So the session is pre-banked by exactly what
|
||||
* adopting the night is about to cost it and the budget lands back on
|
||||
* `week.bank`. Computed FROM THE RIG DATA rather than hardcoded, so B can author
|
||||
* rated shackles into a cowboy's rig without this line silently going wrong.
|
||||
*/
|
||||
const theirCost = (em.rig ?? []).reduce(
|
||||
(sum, c) => sum + (HARDWARE.find((h) => h.name === c.hw)?.cost ?? 0), 0);
|
||||
s.setBudget(week.bank + theirCost + SPARE_COST);
|
||||
for (const c of em.rig ?? []) {
|
||||
s.rig(c.anchorId);
|
||||
const hw = HARDWARE.find((h) => h.name === c.hw);
|
||||
|
||||
@ -14,6 +14,9 @@ import { createCameraRig, spawnYawFor } from '../camera.js';
|
||||
import {
|
||||
CALM_STORM, accumulate, applyMute, canPlayHere, createGame, createWindRouter, enterCommits,
|
||||
rigRecordFor, stormsToPreload, verdictFor,
|
||||
// SPRINT18 gate 1 — both extracted BECAUSE rendering a real emergency card found
|
||||
// bugs the suite could not see: the board's pricing and the answerability split.
|
||||
answerableFor, priceOffer,
|
||||
} from '../main.js';
|
||||
import { createGarden } from '../garden.js';
|
||||
import { RiggingSession } from '../rigging.js';
|
||||
@ -22,6 +25,9 @@ import { SailRig, orderRing } from '../sail.js';
|
||||
import { loadStorm, createWind } from '../weather.js';
|
||||
import {
|
||||
BROKE_BELOW, PAY, REP, calloutFee, createWeek, gradeFor, nightAt, NIGHTS,
|
||||
// SPRINT18 gate 1 — priceOffer takes a NORMALISED night, the same shape the board
|
||||
// installs, so the pin prices what the game prices rather than a raw table entry.
|
||||
normaliseNight,
|
||||
} from '../week.js';
|
||||
// SPRINT17 gate 1/2 — the board and the constraint data are mine; these are
|
||||
// the seam this suite pins (offer shape, pool, exposure, constraint enum).
|
||||
@ -31,6 +37,8 @@ import {
|
||||
isBooked, standUpsIfTaken,
|
||||
// SPRINT18 gate 1 — the emergency callout, and the one-variable law in code.
|
||||
EMERGENCIES, oneVariableOffences, validateEmergency,
|
||||
// SPRINT18 gate 5 (A's half) — the second-rust-post tripwire.
|
||||
collateralKeyClashes,
|
||||
} from '../board.js';
|
||||
import { createHud } from '../hud.js';
|
||||
import { PALETTE, boundsFaults, emptyTemplate, exportSiteJSON, placeEntry } from '../editor.js';
|
||||
@ -1099,6 +1107,134 @@ export default async function run(t) {
|
||||
} finally { hud.dispose(); }
|
||||
});
|
||||
|
||||
t.test('SPRINT18 gate 1: ⚖️ the answerability split — their corner is the JOB, yours is your failure', () => {
|
||||
const c = (id, broken) => ({ anchorId: id, hw: { name: 'carabiner', cost: 5 }, broken });
|
||||
const em = { rig: [{ anchorId: 'cb1', hw: 'carabiner', broken: true },
|
||||
{ anchorId: 'q3', hw: 'carabiner' }, { anchorId: 'tr1', hw: 'carabiner' },
|
||||
{ anchorId: 'tr1b', hw: 'carabiner' }] };
|
||||
const a = answerableFor([c('cb1', true), c('q3', true), c('tr1', false), c('tr1b', false)], em);
|
||||
assertEq(a.lost.length, 2, 'REPORTING stays honest — two corners are on the ground');
|
||||
assertEq(a.yours.length, 1, 'but you are answerable for one of them');
|
||||
assertEq(a.yours[0].anchorId, 'q3', 'the one that went on your watch');
|
||||
assertEq(a.inherited.length, 1, 'and their corner is named, for the invoice');
|
||||
assertEq(a.inherited[0].anchorId, 'cb1');
|
||||
// The two facts the split protects, in the same shape scoreRun consumes them:
|
||||
// the gnome needs TWO corners of YOURS (not two on the ground), and the
|
||||
// collateral walk only visits `yours` — so the $180 carport under an inherited
|
||||
// corner is not billed to a player who never chose it.
|
||||
assert(a.yours.length < 2, 'one corner of yours cannot bill the gnome, even with two corners down');
|
||||
assert(!a.yours.some((x) => x.anchorId === 'cb1'), 'the carport corner is not in the billable set');
|
||||
// An ORDINARY night is untouched: no emergency ⇒ everything down is yours, which
|
||||
// is the property that keeps 522 asserts of scripted week honest.
|
||||
const plain = answerableFor([c('p1', true), c('p2', true)], null);
|
||||
assertEq(plain.yours.length, 2, 'on a night you rigged, every corner down is yours');
|
||||
assertEq(plain.inherited.length, 0, 'and nothing is inherited');
|
||||
assertEq(answerableFor([c('p1', true)], { rig: [] }).yours.length, 1,
|
||||
'an emergency whose rig arrived whole still makes every break yours');
|
||||
});
|
||||
|
||||
t.test('SPRINT18 gate 1: the OFFER CARD prices an emergency at double — the live bug, pinned', () => {
|
||||
if (!boardDefs.storm_03_southerly) return 'SKIPPED — no server for storm defs';
|
||||
// ⚠️ THIS PIN EXISTS BECAUSE THE BUG SHIPPED AND EYES FOUND IT, not the suite:
|
||||
// on night 5's real board the emergency advertised $57 while the dispatch one
|
||||
// click later quoted $114. The pricing was an unreachable closure inside boot();
|
||||
// priceOffer is that closure, extracted, so the board's money is assertable.
|
||||
const def = boardDefs.storm_03_southerly;
|
||||
const em = EMERGENCIES[0];
|
||||
const emNight = normaliseNight(em);
|
||||
const plainNight = normaliseNight({ storm: em.storm, site: em.site });
|
||||
const carded = priceOffer(emNight, { def, rep: REP.START });
|
||||
const plain = priceOffer(plainNight, { def, rep: REP.START });
|
||||
assertEq(plain.fee, calloutFee(def, REP.START, []), 'an ordinary night is priced as it always was');
|
||||
assertEq(carded.fee, calloutFee(def, REP.START, [], true), 'and an emergency is priced as an emergency');
|
||||
assertEq(carded.fee, Math.round(plain.fee * PAY.emergencyRate),
|
||||
`the card must print the doubled fee — got $${carded.fee} against $${plain.fee}`);
|
||||
// And the card is the same document as the sheet: what priceOffer prints is what
|
||||
// week.quote() quotes, which is the promise the whole board rests on.
|
||||
const w = createWeek();
|
||||
for (let i = 0; i < em.night - 1; i++) w.advance();
|
||||
w.take(w.offers()[1]);
|
||||
assertEq(w.quote(def).base, carded.fee, 'the dispatch quotes exactly what the card advertised');
|
||||
// The weather and the risk still come from C's and board.js's own functions.
|
||||
assert(Array.isArray(carded.band) && carded.band.length, "C's ordered band survives the extraction");
|
||||
});
|
||||
|
||||
t.test('SPRINT18 gate 1: an emergency does NOT look like an ordinary callout on the board', () => {
|
||||
if (typeof document === 'undefined') return 'SKIPPED — no DOM';
|
||||
// ⚠️ ALSO FOUND BY EYES, NOT BY THE SUITE: the first cut wore the same grey
|
||||
// "callout" tag as the Tuesday alternative beside it. Double rates, no prep, no
|
||||
// quote, somebody else's rig — the biggest new thing in the game — and the card
|
||||
// said nothing until one click later. That is the tell rule broken on the exact
|
||||
// surface the tell rule is for.
|
||||
const em = EMERGENCIES[0];
|
||||
const w = createWeek();
|
||||
for (let i = 0; i < em.night - 1; i++) w.advance();
|
||||
// PRICED, through the same priceOffer() the real board uses — my first cut built
|
||||
// an UNPRICED board and then asserted the fee line's contents, so the pin failed
|
||||
// on a fixture that could not produce what it was demanding. A card assert has to
|
||||
// be given the card the game renders.
|
||||
const offers = w.offers((night) => priceOffer(night, { def: boardDefs[night.storm], rep: w.rep }));
|
||||
const priced = offers[1].fee != null;
|
||||
const hud = createHud({ scene: new THREE.Scene() });
|
||||
try {
|
||||
hud.showBoard(offers, { night: w.night, nights: w.nights, bank: 80, log: [], rep: 3.0 }, () => {});
|
||||
const cards = [...document.querySelectorAll('#hud-card .offer')];
|
||||
assertEq(cards.length, 2, 'an emergency morning still offers two jobs');
|
||||
assert(!cards[0].classList.contains('emergency'), 'the booked wildnight is not an emergency');
|
||||
assert(cards[1].classList.contains('emergency'),
|
||||
'the emergency offer must be marked as one, not left to read as a Tuesday alternative');
|
||||
const tag = cards[1].querySelector('.tag')?.textContent ?? '';
|
||||
assert(/emergency/i.test(tag), `its tag must say so — got "${tag}"`);
|
||||
const siren = cards[1].querySelector('.siren')?.textContent ?? '';
|
||||
assert(siren.includes('no prep') && siren.includes('no quote'),
|
||||
`and one line must name what is different about it — got "${siren}"`);
|
||||
assert(siren.includes(`${em.emergency.arriveAt}s in`), 'including how late you arrive');
|
||||
// The doubled number carries its reason, on the card, next to the number.
|
||||
// (Guarded on the money being present at all: with no server there are no storm
|
||||
// defs, so the fee line degrades away exactly as the job sheet's forecast does.)
|
||||
if (priced) {
|
||||
const fee = cards[1].querySelector('.fee')?.textContent ?? '';
|
||||
assert(fee.includes(`${PAY.emergencyRate}\u00d7 emergency rate`),
|
||||
`the rate is printed beside the fee — got "${fee}"`);
|
||||
assert(fee.includes(`$${offers[1].fee}`), 'and the doubled number itself');
|
||||
}
|
||||
} finally { hud.dispose(); }
|
||||
});
|
||||
|
||||
t.test('SPRINT18 gate 5: ⚠️ the SECOND-RUST-POST landmine is a tripwire now, not a silent free failure', () => {
|
||||
// D's filing + the other D session's measured receipt. Every shipped yard is
|
||||
// clean TODAY (the pool yard ships exactly one corroded post on purpose), so
|
||||
// this walk is green — which is why the guard below is the part that matters.
|
||||
for (const [name, json] of Object.entries(siteJsonForTest)) {
|
||||
if (!json) continue;
|
||||
assertEq(collateralKeyClashes(json).join(' | '), '',
|
||||
`${name} ships clashing collateral keys — see the accusation for the numbers`);
|
||||
}
|
||||
// ⚠️ THE VACUITY GUARD, and it carries D's measured numbers so the next author
|
||||
// reads the actual failure rather than a policy. A yard with TWO corroded posts:
|
||||
// the offer card would read $90 and the failure route $45, so one post fails
|
||||
// FREE and wrecks the other one.
|
||||
const twoPosts = { name: 'the fixture yard', structures: [
|
||||
{ id: 'c1', collateralKey: 'corroded_post', collateralValue: 45 },
|
||||
{ id: 'c2', collateralKey: 'corroded_post', collateralValue: 45 },
|
||||
] };
|
||||
const clash = collateralKeyClashes(twoPosts);
|
||||
assertEq(clash.length, 1, 'two structures on one key must be accused exactly once');
|
||||
assert(clash[0].includes('$90') && clash[0].includes('$45'),
|
||||
`the accusation must carry BOTH harnesses' numbers — got "${clash[0]}"`);
|
||||
assert(clash[0].includes('c1') && clash[0].includes('c2'), 'and name the structures');
|
||||
assert(/fail FREE/.test(clash[0]), 'and say what actually goes wrong, not just that something does');
|
||||
// An UNKEYED structure resolves by its own id, which the editor generates unique,
|
||||
// so it cannot clash — and reporting it would be a false alarm that teaches an
|
||||
// author to ignore this check.
|
||||
assertEq(collateralKeyClashes({ structures: [
|
||||
{ id: 's1', collateralValue: 180 }, { id: 's2', collateralValue: 180 },
|
||||
] }).length, 0, 'unkeyed structures resolve by unique id and must not be accused');
|
||||
// And one post is fine — the shipped pool yard's own shape.
|
||||
assertEq(collateralKeyClashes({ structures: [twoPosts.structures[0]] }).length, 0,
|
||||
'one corroded post is the shipped shape and must stay legal');
|
||||
});
|
||||
|
||||
// Gate 2 acceptance: both sites load from data, and the corner block is not
|
||||
// the backyard with the furniture moved. Built up front (Suite.test() can't
|
||||
// await — the guard I added last sprint enforces it, and just caught me).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user