diff --git a/web/world/js/board.js b/web/world/js/board.js
index 78f16a7..e707511 100644
--- a/web/world/js/board.js
+++ b/web/world/js/board.js
@@ -444,13 +444,47 @@ export const EMERGENCIES = [
* over the one piece of collateral in this game that a player has already
* been taught to fear.
*/
+ /**
+ * ⚖️ **B's AUTHORED SET, ADOPTED VERBATIM — landed by D, and it should have landed at
+ * integration.** B published it in THREADS with the measurements ([B] gate 1 §5) and A
+ * pre-authorised the swap in the note above ("revert-and-tell-me applies… yours is canon the
+ * moment it lands"). It never landed, because it is DATA IN A's FILE while B rules the
+ * semantics. **I found it by playing the night:** the dispatch card read *"CB1 carabiner — ON
+ * THE GROUND"*, which is a different night from the one this sprint is about.
+ *
+ * Why the difference IS the gate and not a detail: in the placeholder the CARPORT CORNER
+ * ARRIVES ALREADY DOWN, so the $180 carport is `theirs` — unbillable, unwreckable — and there
+ * is nothing left for the knife to save. DESIGN.md line 165's signature decision had no
+ * object. In B's set `cb1` is a SHACKLE on the 0.22 beam (704 N effective, holding their roof)
+ * which the southerly puts over its ceiling from t=35 to t=42, and `tr1b` — the 0.88 branch on
+ * the cheapest steel — is the corner that arrived gone. That is B's measured triage window:
+ * ride it out → cb1 blows @35.9 s and takes q1 → the carport WRECKED, $180
+ * cut at t=33 → nothing breaks, the carport saved
+ * 5.9 seconds. Measured by B on the rig; re-flown by D through the shipped game.
+ */
rig: [
- { anchorId: 'cb1', hw: 'carabiner', broken: true },
- { anchorId: 'tr1', hw: 'carabiner' },
- { anchorId: 'tr1b', hw: 'carabiner' },
- { anchorId: 'q3', hw: 'carabiner' },
+ { anchorId: 'q3', hw: 'shackle' }, // sound post, still holding
+ { anchorId: 'q1', hw: 'carabiner' }, // under-specced
+ { anchorId: 'tr1b', hw: 'carabiner', broken: true }, // 0.88 branch + cheapest steel: already gone
+ { anchorId: 'cb1', hw: 'shackle' }, // ⚠️ the CARPORT BEAM, hint 0.22
],
},
+ /**
+ * ⚖️ **B's RULING ON THE ECONOMY OF TRIAGE, landed by D — same story as the rig above: a ruling
+ * B made and could not apply, because it is per-job DATA in A's file.** ([B] gate 1 §2.)
+ *
+ * **The garden BONUS does not apply on a callout.** It is a per-HP payment for a bed you chose
+ * hardware to protect; you arrive at t=30 with thirty seconds of somebody else's storm already
+ * in that bed, under steel you did not buy. Paying per-HP prices the PREVIOUS contractor's
+ * geometry and docks you for their mistakes. Data, not a branch — so A's acceptance criterion
+ * (§10: `settle()` runs unchanged on an emergency) holds exactly.
+ *
+ * **The garden's WIN still applies, and that is the knife's price.** `win = hp >= 50` gates the
+ * FEE (`PAY.lostNightShare`), so cutting spends garden HP — and can spend 65% of the callout —
+ * to buy certainty on their property. Ride it out for the full double rate and gamble their
+ * carport; cut and take the smaller cheque. The garden is the STAKE, not the wage.
+ */
+ pay: { garden: 0 },
_why: 'THE PAIRING IS THE POOL\'S OWN AUDITED corner_block_southerly (A, S17 gate 1, '
+ 'audit.html): 19 holding lines / 7 marginal / 38 unholdable, cheapest hold $40 '
+ '(tr1,tr1b,q1,q3 → garden 87.6 FULL). ⚠️ THAT AUDIT ANSWERS A DIFFERENT QUESTION and '
diff --git a/web/world/js/hud.js b/web/world/js/hud.js
index 73b8d38..30c03c7 100644
--- a/web/world/js/hud.js
+++ b/web/world/js/hud.js
@@ -81,6 +81,28 @@ const starDelta = (d) => {
return `${d < 0 ? '−' : '+'}${whole || !half ? whole : ''}${half ? '½' : ''}★`;
};
+/**
+ * ⚠️ **SPRINT18 gate 1.3 [Lane D] — WHAT STANDING A CLIENT UP WILL ACTUALLY COST, WHICH IS NOT
+ * ALWAYS `REP.STOOD_UP`.** Found by playing the week to ★0.0 and taking the emergency anyway.
+ *
+ * `settle()` clamps rep at `REP.MIN`, so at ★0.0 a −½★ debit moves NOTHING — and both papers went on
+ * promising it. On the offer card that is a price the game will not charge; on the invoice it is
+ * worse, because the same card carries the row *"stood up the Hendersons · −½★ standing"* four lines
+ * above *"reputation ★ 0.0 → ★ 0.0"*. **The invoice arguing with itself**, which is the defect class
+ * this repo has spent five sprints hunting, on John's own ruling.
+ *
+ * This function is the PAPER half only, and the split is deliberate: whether the ruling should have
+ * teeth at the floor is a DESIGN question and it is John's (filed in THREADS with the measurement —
+ * at ★0.0 a fee-maximiser can dodge every booked night for free again, which is precisely the player
+ * the ruling was written for). Until he rules, the documents tell the truth about the number.
+ *
+ * @param {number} n how many clients would be stood up
+ * @param {number|null} rep the standing the charge lands against (`wk.rep` / `w.repBefore`)
+ */
+const standUpCost = (n, rep) => (rep == null
+ ? REP.STOOD_UP * n
+ : Math.max(REP.MIN, rep + REP.STOOD_UP * n) - rep);
+
// Load bar colours: green → amber → red. The amber band is deliberately wide;
// a corner at 60% in a lull is one gust from 100% and the player should feel
// that before the bar turns red on them.
@@ -943,10 +965,15 @@ export function createHud(d) {
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. */''}
+ ${/* SPRINT18 gate 1.3 [D] — the cost AS IT WILL BE CHARGED (standUpCost): at ★0.0 the
+ clamp eats it, and a card promising a price the invoice will not take is the same lie
+ as a card hiding one. The words change with it, because "−0★" would be gibberish. */''}
${o.standsUp?.length ? `
${
o.standsUp.map((s) => s.client ?? 'a client').join(' and ')
} are expecting you${
- starDelta(REP.STOOD_UP * o.standsUp.length)} standing
` : ''}
+ standUpCost(o.standsUp.length, wk.rep) === 0
+ ? 'nothing left to dock'
+ : `${starDelta(standUpCost(o.standsUp.length, wk.rep))} standing`}` : ''}
${cons.length ? `⚖ ${cons.map((c) => c.label).join(' · ')}
` : ''}
${cons.map((c) => `“${c.says}”
`).join('')}
${/* gate 0, clause 3 — and if you have done this to THEM before, it is
@@ -1496,9 +1523,14 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
you took this callout${w.client ?? 'no client'}
${w.stoodUp?.length
? w.stoodUp.map((s) => `
- stood up ${s.client ?? 'a client'} — they were expecting you${
- starDelta(REP.STOOD_UP)} standing
- No invoice for that one. They will remember the phone call.
`).join('')
+ stood up ${
+ s.client ?? 'a client'} — they were expecting you${
+ standUpCost(1, w.repBefore) === 0
+ ? 'nothing left to dock'
+ : `${starDelta(standUpCost(1, w.repBefore))} standing`}
+ ${standUpCost(1, w.repBefore) === 0
+ ? 'Your standing is already on the floor, so this one was free. They will remember the phone call anyway.'
+ : 'No invoice for that one. They will remember the phone call.'}
`).join('')
: 'nobody was expecting you — nothing to stand up—
'}
` : ''}
${/* 🚨 SPRINT18 gate 1 — THE EMERGENCY'S PAPERWORK. Three things the invoice
@@ -1521,9 +1553,20 @@ showForecast({ key, def, site, tomorrowDef }, wk, onGo) {
${(w.inherited ?? []).map((c) => `
${c.anchorId.toUpperCase()} — ${c.hw}, already on the ground when you arrivednot billed
That corner is what the callout was for. It is not on you.
`).join('')}
- ${(w.cutAways ?? []).map((c) => `
+ ` : ''}
+ ${/* 🚨 SPRINT18 gate 1.3 [Lane D] — **THE KNIFE'S LINE, ON ANY NIGHT.** It shipped inside the
+ EMERGENCY ATTENDANCE block, so a sail cut loose on an ordinary Tuesday printed NOTHING —
+ the verb is available every night the sail is up, and principle 2 does not have a "only
+ on callouts" clause. Its own section for the same reason the emergency has one: this is
+ not a charge and not a bonus, it is a decision, and the ledger below has no row shape for
+ that. `reason` is printed because B put it in the payload for the paperwork and D fills
+ it with what the cut was FOR — the sentence a player would say out loud. */''}
+ ${w?.cutAways?.length ? `
+ THE KNIFE
+
+ ${w.cutAways.map((c) => `
sail cut loose${c.corners?.length ? ` at ${c.corners.map((x) => String(x).toUpperCase()).join(', ')}` : ''} — ${
- Math.round(c.t)}s in, deliberateyour call
+ Math.round(c.t)}s in, deliberate${c.reason ? ` · for ${c.reason}` : ''}
your call
You spent the sail to keep the steel. That is the trade the knife is for.
`).join('')}
` : ''}
${w?.constraints?.length ? `
diff --git a/web/world/js/main.js b/web/world/js/main.js
index c66fc3a..cf18f37 100644
--- a/web/world/js/main.js
+++ b/web/world/js/main.js
@@ -223,7 +223,7 @@ export function applyMute(sky, on) {
*
* @returns {{verdict: string, mode: string}}
*/
-export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped, beyondSaving = false }) {
+export function verdictFor({ hp, lost, cut = [], win, dmg, pondPeak, pondDumped, beyondSaving = false }) {
// SPRINT12 — B's flag, actioned: "weakest link that went first" reduces on
// the EFFECTIVE rating (rating × ratingHint), matching sail.js's failure
// line and B's summary.weakest tie-break fix. On the bare rating a rated
@@ -237,6 +237,33 @@ export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped, beyondSav
const named = worst ? `${worst.hw.name} at ${worst.anchorId.toUpperCase()}` : '';
const hailKilled = dmg.hail > dmg.rain;
+ /**
+ * 🚨 SPRINT18 gate 1.3 [Lane D] — **THE KNIFE'S OWN VERDICT, and without it this function calls a
+ * deliberate cut "the hail fell where your sail wasn't".**
+ *
+ * That is mode `uncovered`, and it is the single worst thing the card could say to a player who
+ * just made the game's signature decision: it blames placement on a night where the sail came down
+ * because they cut it down. The verdict's whole charter is that it reads what ACTUALLY happened.
+ *
+ * It fires only when nothing of YOURS failed, and that composition rule is deliberate rather than
+ * tidy: if a corner blew as well, that failure is the louder fact and the modes below say it
+ * truthfully — the cut is still named on the invoice's own line. So the knife speaks when the knife
+ * is the story, and shuts up when it isn't.
+ *
+ * "Nothing of theirs" is true BY CONSTRUCTION here, not by hope: `collateralWalk` prices `yours`,
+ * and with `yours` empty the gnome's two-corner rule and every structure key are empty too.
+ */
+ if (cut.length && !lost.length) {
+ const where = cut.map((c) => c.anchorId.toUpperCase()).join(', ');
+ return win
+ ? { mode: 'cut-held',
+ verdict: `YOU CUT IT LOOSE at ${where} — and the bed came through anyway. `
+ + 'Nothing of theirs is on the ground, and nothing of yours broke.' }
+ : { mode: 'cut',
+ verdict: `YOU CUT IT LOOSE at ${where}. The bed went with it — and their property didn't. `
+ + 'That was the trade.' };
+ }
+
// SPRINT9 decision 1. This case had to come FIRST because it did not used to
// exist: `lost >= 2` was unconditionally a cascade-and-a-loss, and under the
// new rule it can be the best night the game has. A sail that tore itself
@@ -353,6 +380,17 @@ export function rigRecordFor(corners, tally = new Map(), theirs = new Set()) {
repaired: broke && !c.broken,
brokenAtDawn: !!c.broken,
theirs: theirs.has(c.anchorId),
+ /**
+ * SPRINT18 gate 1.3 [D] — you let this one go on purpose. week.js reads it exactly where it
+ * already reads `theirs`, and for the same reason: warranty and standing are about work that
+ * FAILED. A corner still down at dawn because you cut it booked a free callout to go and
+ * re-hang your own decision, and docked half a star for taking it.
+ *
+ * NOTE it does not suppress `broke`/`brokenAtDawn`. Those two are the RECORD — the sail really
+ * is on the ground and the client can see it — and a record that edits itself to be flattering
+ * is the one thing this ledger has never done.
+ */
+ cut: !!c.cut,
};
});
}
@@ -436,15 +474,83 @@ export function priceOffer(night, { def = null, site = null, rep = 3, remembers
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);
+ /**
+ * 🚨 **SPRINT18 gate 1.3 [Lane D] — A CORNER YOU CUT IS NOT A CORNER THAT FAILED, AND WITHOUT
+ * THIS LINE THE KNIFE IS A TRAP.**
+ *
+ * B's `cutAway` deliberately emits no `break` ("warranty must never chase a corner the player
+ * chose to let go") — but every ledger surface downstream reads `c.broken`, and `_releaseCorner`
+ * sets it for a cut exactly as it does for an overload. So on the tree as merged, taking DESIGN.md
+ * line 165's signature decision billed you for it on five surfaces at once: the cut corner's
+ * hardware went on `bill`, the collateral walk BILLED AND WRECKED the $180 carport you cut the
+ * sail to save, a four-corner cut tripped the gnome's two-corner rule, `clean` was voided by the
+ * collateral it caused, and week.js booked warranty and docked standing for every corner you let
+ * go. The invoice would have printed *"You spent the sail to keep the steel. That is the trade the
+ * knife is for"* directly above a $180 charge for the thing you kept.
+ *
+ * Found by playing it, not by a test — the same way every card bug in this repo has been found.
+ * A is the author of both halves and neither half is wrong; nobody joined them, because the join
+ * is a flag on an object and there was no verb setting the flag until now.
+ *
+ * The ruling, and it is B's extended to every surface that reads a corner: **a cut is a DECISION.
+ * It bills nothing, wrecks nothing, books no warranty and costs no standing. Its price is the
+ * garden — and through the garden, the night's fee — and that is enough.** `lost` still counts it
+ * (the HUD's corners-intact read must stay honest: the sail IS on the ground), and `cut` is named
+ * so the paper can say who put it there.
+ */
+ const cut = lost.filter((c) => c.cut);
return {
theirs,
lost,
- yours: lost.filter((c) => !theirs.has(c.anchorId)),
+ cut,
+ yours: lost.filter((c) => !theirs.has(c.anchorId) && !c.cut),
inherited: lost.filter((c) => theirs.has(c.anchorId))
.map((c) => ({ anchorId: c.anchorId, hw: c.hw?.name ?? '?' })),
};
}
+/**
+ * 🚨 **SPRINT18 gate 1.3 [Lane D] — THE COLLATERAL WALK, EXTRACTED.** What a set of failed corners
+ * costs you in the client's property. Lifted OUT of scoreRun unchanged, for the two reasons
+ * `answerableFor` and `rigRecordFor` were extracted before it, and one new one:
+ *
+ * · scoreRun lives inside `boot()` behind a WebGL context where no assert can reach it.
+ * · **the knife's HUD read has to promise exactly what the invoice will honour.** The panel says
+ * "saves the carport $180" before the cut and the invoice charges $0 after it; if those two ever
+ * came off different arithmetic, the game would be advertising a saving. One walk, two readers —
+ * the same discipline that made `calloutFee` one function for the board, the dispatch and the
+ * invoice.
+ *
+ * PURE, and that is load-bearing: `wreckStructure()` stays at the call site. A HUD asking what a cut
+ * would save must not swap a structure for its wreck as a side effect of the question.
+ *
+ * @param {Array} corners the corners to price — `yours` at dawn, or the LIVE ones for the knife
+ * @param {object} world contracts World (anchor(), collateralFor(), gnome)
+ * @returns {{items: {what:string,cost:number}[], keys: string[], total: number}}
+ */
+export function collateralWalk(corners, world) {
+ const items = [];
+ const keys = [];
+ // The gnome is collateral if the sail came down over it (DESIGN.md: the worst debris in any storm
+ // is your own failed work). Two corners, per the original rule.
+ if ((corners ?? []).length >= 2 && world?.gnome) {
+ items.push({ what: 'garden gnome', cost: world.gnome.collateralValue });
+ }
+ /**
+ * Priced per STRUCTURE, not per corner: two beams letting go is one carport gone, not $360.
+ */
+ const taken = new Set();
+ for (const c of corners ?? []) {
+ const key = world?.anchor ? world.anchor(c.anchorId)?.collateral : null;
+ const priced = key && world.collateralFor ? world.collateralFor(key) : null;
+ if (!priced || taken.has(key)) continue;
+ taken.add(key);
+ keys.push(key);
+ items.push({ what: priced.label, cost: priced.cost });
+ }
+ return { items, keys, total: items.reduce((s, i) => s + i.cost, 0) };
+}
+
// ---------------------------------------------------------------------------
// Phase machine
// ---------------------------------------------------------------------------
@@ -723,7 +829,18 @@ export async function boot(opts = {}) {
// world into its ground clamp, solid collider, ladder and broom, none of
// which have a setter (Lane D's modules). A site change only happens at a
// forecast, never mid-storm, so a clean rebuild is honest and cheap.
- player = await createPlayer(scene, world, cameraRig, { wind, interact });
+ // SPRINT18 gate 1.3 — D's knife rides in on the opts it already had. Every one of these is a
+ // GETTER because `game` and `garden` are declared below the first call to this function (the
+ // TDZ trap documented twice above); the knife reads them at frame time, when they exist.
+ player = await createPlayer(scene, world, cameraRig, {
+ wind,
+ interact,
+ getPhase: () => game.phase,
+ getGarden: () => garden,
+ /** One walk, two readers: what the panel promises is what the invoice honours. */
+ collateralAtRisk: collateralWalk,
+ secsLeft: () => STORM_LEN - game.phaseT,
+ });
// SPRINT13 gate 2.5 — the opening frame, per YARD rather than per game.
// Done here because the spawn is here: the player and the yard are rebuilt
@@ -882,10 +999,15 @@ export async function boot(opts = {}) {
* see (this wire lives inside `boot()` behind a WebGL context). The fallback is
* one `??` and it agrees with the argument on every wired path.
*/
- async function rigSail(anchorIds, hwChoices, tension = 1.0, fabric) {
+ /**
+ * @param {object} [opts] `{at, wind}` — B's `attach` door (sail.js). Absent/`at:0` is
+ * byte-for-byte the old behaviour, so every ordinary night and every bench is untouched.
+ * See the emergency's note on `onCommit` for the one caller that passes it.
+ */
+ async function rigSail(anchorIds, hwChoices, tension = 1.0, fabric, opts) {
const cloth = fabric ?? rigging?.session?.fabric;
if (cloth) rig.setFabric(cloth);
- rig.attach(anchorIds, hwChoices, tension);
+ rig.attach(anchorIds, hwChoices, tension, opts);
disposeSailView();
sailView = await createSailView(rig);
scene.add(sailView);
@@ -996,7 +1118,27 @@ export async function boot(opts = {}) {
// verbatim; `fabric` is undefined on bare lane/a (their commit() passes it,
// and that lands at integration) and rigSail falls back to session.fabric so
// the sim flies the right cloth on both branches. See rigSail's note.
- onCommit: (ids, hw, tension, fabric) => { void rigSail(ids, hw, tension, fabric); },
+ /**
+ * 🚨 SPRINT18 gate 1.3 [D] — **`{at, wind}`: THE CLOTH FINALLY ARRIVES IN THE STORM IT IS
+ * STANDING IN.** C measured it and B fixed it and nobody wired it, so on the merged tree the one
+ * night that starts mid-storm attached its sail at t=0: measured live by D on the shipped
+ * emergency, **`rig.clockSkew` read 30.02** while the sky, garden, debris and player were all
+ * thirty seconds in. B's own number for what that hides: the same rig entering the same storm
+ * pulls **90 N at t=0 and 655 N at t=40**. The triage that this whole gate is about was being
+ * judged against a sail feeling a seventh of the load, and the knife's own invoice line said
+ * *"cut loose 9s in"* on a night you arrived at 30 — because the log stamps the SAIL's clock.
+ *
+ * The night is asked, not a flag: `week.job.emergency` is a fact the night already owns, so this
+ * needs no new state and no ordering change (`adoptEmergencyRig` runs before `setPhase`, so
+ * reading `game.phaseT` here would read the forecast's clock — which is how it would go wrong).
+ * With a `wind` B's `attach` genuinely PRE-FLIES the thirty seconds rather than teleporting the
+ * cloth into a gale, so the drape and the belly's water are real history.
+ */
+ onCommit: (ids, hw, tension, fabric) => {
+ const em = week.job?.emergency;
+ void rigSail(ids, hw, tension, fabric,
+ em ? { at: em.arriveAt, wind: winds[week.stormKey] } : undefined);
+ },
onMessage: pushEvent,
panel: true,
});
@@ -1047,13 +1189,8 @@ export async function boot(opts = {}) {
* subtitle read this.
*/
const em = week.job.emergency;
- const { theirs, lost, yours, inherited } = answerableFor(rig.corners, em);
+ const { theirs, lost, yours, cut, 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
- // worst debris in any storm is your own failed work. `yours`, not `lost`:
- // two corners on the ground when you ARRIVED is not the sail you dropped.
- if (yours.length >= 2) collateral.push({ what: 'garden gnome', cost: world.gnome.collateralValue });
/**
* SPRINT11 — the carport trap finally BILLS (§gate 1).
@@ -1072,16 +1209,13 @@ export async function boot(opts = {}) {
*
* Priced per STRUCTURE, not per corner: two beams letting go is one carport
* gone, not $360. It bills once and the wreck swaps once.
+ *
+ * SPRINT18 gate 1.3 [D] — the ARITHMETIC moved out to `collateralWalk` (pure, exported, above)
+ * so the knife's HUD read prices what it saves through this exact walk. The WRECK stays here:
+ * a HUD asking "what would a cut save" must not swap a structure for its wreck.
*/
- const taken = new Set();
- for (const c of yours) {
- const key = world.anchor(c.anchorId)?.collateral;
- const priced = key ? world.collateralFor(key) : null;
- if (!priced || taken.has(key)) continue;
- taken.add(key);
- collateral.push({ what: priced.label, cost: priced.cost });
- world.wreckStructure(key); // false offline/graybox — the bill still lands
- }
+ const { items: collateral, keys: wrecked } = collateralWalk(yours, world);
+ for (const key of wrecked) world.wreckStructure(key); // false offline/graybox — the bill still lands
const s = rigging.summary;
const hp = garden.hp;
/**
@@ -1116,7 +1250,7 @@ export async function boot(opts = {}) {
* 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,
+ const { verdict, mode } = verdictFor({ hp, lost: yours, cut, win, dmg, pondPeak, pondDumped,
beyondSaving: week.job.gardenBeyondSaving });
// SPRINT16 gate 2.2 [Lane B] — the fabric bet on the record. The F key
@@ -1144,7 +1278,13 @@ export async function boot(opts = {}) {
win,
subtitle: yours.length
? `${yours.map((c) => `${c.hw.name} at ${c.anchorId.toUpperCase()}`).join(', ')} let go.`
- : inherited.length
+ // SPRINT18 gate 1.3 [D] — a cut sail must not be headlined "Every corner held." The card
+ // would then carry that line, a CUT LOOSE row in the ledger and a cut verdict underneath:
+ // three accounts of one night, one of them false. `cut` before `inherited` because on the
+ // shipped emergency both are true and the cut is the thing the PLAYER did.
+ : cut.length
+ ? `You cut it loose at ${cut.map((c) => c.anchorId.toUpperCase()).join(', ')}.`
+ : inherited.length
// An emergency you held: nothing of yours went, and the corner that was
// already down stays named rather than quietly counted as a hold.
? `Nothing of yours let go. ${inherited.map((c) => c.anchorId.toUpperCase()).join(', ')} was on the ground when you got there.`
@@ -1169,9 +1309,18 @@ export async function boot(opts = {}) {
* somebody else's four rated shackles at dawn, and the biggest earner in the
* game would be turning up to other people's disasters and salvaging them.
*/
+ /**
+ * SPRINT18 gate 1.3 [D] — **"LOSE THE SAIL, SAVE THE ANCHORS" IS A LEDGER LINE, NOT A
+ * SLOGAN.** `!c.broken` alone excluded a CUT corner from the refund, so the knife also quietly
+ * took your steel: you cut the cloth, and the shackle still bolted to the post — undamaged,
+ * unclippable in thirty seconds at dawn — paid you nothing. DESIGN.md line 165 promises the
+ * opposite in the same breath as the verb, and `bill` already agrees (a cut corner is not
+ * `yours`, so its hardware is not written off). This is the other half of that sentence.
+ * On an EMERGENCY it changes nothing — none of that steel is yours to unclip either way.
+ */
intactHardwareValue: em
? (rigging.session.spares ?? 0) * SPARE_COST
- : rig.corners.filter((c) => !c.broken).reduce((sum, c) => sum + c.hw.cost, 0)
+ : rig.corners.filter((c) => !c.broken || c.cut).reduce((sum, c) => sum + c.hw.cost, 0)
+ (rigging.session.spares ?? 0) * SPARE_COST,
pondPeak,
pondDumped,
@@ -1358,9 +1507,26 @@ export async function boot(opts = {}) {
throw new Error(`main: the emergency rig would not commit — ${em.rig?.length ?? 0} corners `
+ `on ${week.site}. Check every anchorId exists in that yard's site JSON.`);
}
- // Corners that ARRIVED gone. Feature-detected; paper-only until B's door lands.
+ /**
+ * Corners that ARRIVED gone.
+ *
+ * ⚠️ **SPRINT18 gate 1.3 [D] — THE DOOR IS CALLED `failCorner`, AND FOR THIS WHOLE SPRINT THIS
+ * LINE CALLED A METHOD THAT DOES NOT EXIST.** B opened it ([B] gate 1 §4) under the name
+ * `failCorner`; this asked for `breakCorner?.()`, and an optional call to a renamed method is a
+ * silent no-op that reads exactly like a call — **the `rigging.setWorld` lesson this file quotes
+ * elsewhere, arriving through the door built to prevent it.** Measured live:
+ * `typeof rig.failCorner === 'function'`, `typeof rig.breakCorner === 'undefined'`, and the
+ * corner every card called "ON THE GROUND" was flying.
+ *
+ * A's fallback reasoning ("the authored corner is a $5 carabiner on the carport beam, so it fails
+ * within seconds through the real overload path") was doing the work and hid the break — but it
+ * only holds for A's placeholder rig. Under B's canon set the arrived-broken corner is `tr1b`, a
+ * carabiner on a 0.88 branch that does NOT self-destruct, so the dispatch would have promised a
+ * corner on the ground and the sim would have flown it all night. Still feature-detected, because
+ * the pattern is right; only the name was wrong.
+ */
for (const c of em.rig ?? []) {
- if (c.broken) rig.breakCorner?.(c.anchorId, { reason: 'arrived' });
+ if (c.broken) rig.failCorner?.(c.anchorId);
}
}
@@ -1577,7 +1743,11 @@ export async function boot(opts = {}) {
// that doesn't wait), and M only once C's bus has a tap — hud owns that
// question, because a help line that lists a dead key is a help line that
// teaches a stranger the game is broken.
- hud.setHelp(`WASD move · shift run · E repair/pickup · C brace · RMB orbit · ${hud.comfortKeysHint()}`);
+ // SPRINT18 gate 1.3 — X is on the help line because a key nobody advertises is a key nobody
+ // presses, and the knife is the one verb the game has to offer BEFORE the moment it is needed.
+ // "hold X cut sail" rather than "X cut sail": it is a hold, and the difference matters when the
+ // window is six seconds long.
+ hud.setHelp(`WASD move · shift run · E repair/pickup · hold X cut sail · C brace · RMB orbit · ${hud.comfortKeysHint()}`);
}
if (to === 'aftermath') {
// The dawn comes up BEFORE the scoreboard, on E's 2.2 s ease. Their note
diff --git a/web/world/js/week.js b/web/world/js/week.js
index 57a9cfc..ff509fd 100644
--- a/web/world/js/week.js
+++ b/web/world/js/week.js
@@ -646,7 +646,13 @@ export function createWeek(opts = {}) {
// for a failure that predates you, which is the exact opposite of what the
// warranty ledger was built to say. main.js's rigRecordFor sets the flag; the
// rep block below skips it for the same reason.
- return (prev.rig ?? []).filter((c) => c.brokenAtDawn && !c.theirs).map((c) => ({
+ // 🚨 SPRINT18 gate 1.3 [Lane D] — `!c.cut`: THE KNIFE. B's ruling on `cutAway` in sail.js is
+ // that a cut is a DECISION and emits no `break`, precisely so "warranty must never chase a
+ // corner the player chose to let go" — but warranty is booked off `brokenAtDawn`, which a cut
+ // sets, so without this line the ledger sent you back unpaid tomorrow to re-hang a sail you
+ // deliberately spent to save the client's carport. Same shape as the `!c.theirs` line above it,
+ // one verb along.
+ return (prev.rig ?? []).filter((c) => c.brokenAtDawn && !c.theirs && !c.cut).map((c) => ({
anchorId: c.anchorId, hw: c.hw, cost: c.hwCost,
night: prev.night, site: prev.site, addr: prev.addr, client: prev.client,
}));
@@ -940,8 +946,12 @@ export function createWeek(opts = {}) {
// SPRINT18 gate 1 — their corners are not your record. A rig you inherited
// mid-storm cannot cost you a clean night for breaking before you got there,
// and cannot cost you standing for still being down at dawn.
- const brokeCount = rigRec.filter((c) => c.broke && !c.theirs).length;
- const dawnBroken = rigRec.filter((c) => c.brokenAtDawn && !c.theirs).length;
+ // SPRINT18 gate 1.3 — and a corner you CUT is not your record either. Standing is what the
+ // client thinks of your work; the knife is the work. Its price is the garden (and the fee the
+ // garden carries), ruled by B and measured by D — not a second, silent bite at the star the
+ // same night. `!c.cut` twice, for the same reason `!c.theirs` is there twice.
+ const brokeCount = rigRec.filter((c) => c.broke && !c.theirs && !c.cut).length;
+ const dawnBroken = rigRec.filter((c) => c.brokenAtDawn && !c.theirs && !c.cut).length;
const collateralEvents = (run.collateral ?? []).length;
const repBefore = rep;
let repDelta = 0;