Compare commits

...

4 Commits

Author SHA1 Message Date
type-two
d2b9a31363 THREADS: integrator handoff — B's two-chain disagreement pin resolves by B's own instruction; verified 357/0/0 on scratch 2026-07-18 14:25:21 +10:00
type-two
cc9849d538 Sprint 13 gate 1.4: the played run is the ruling's centre — canon written, pin labeled sim-chain, THREADS
- Played night 4 on the pinned line through the real UI (D's protocol):
  hp 55.7 WIN, 2/4 lost, pyrrhic verdict — the ~8 gap to the sim chain is the
  t=38 crate + ponding landing on p2's 4% margin. Ruled deliberate: the wild
  night sells no clean line; the pyrrhic win is the intended reading.
- Site block records _playedReference; a.test pin re-labeled as the
  deterministic sim-chain tripwire whose margin is what keeps the PLAYED
  night over the win line. Band 18-45 ruled an availability floor (B's 1c).
- THREADS: full second ruling, night-5 canon, Sprint-14 notes.
2026-07-18 14:18:37 +10:00
type-two
80b53e9547 Sprint 13 gate 1.4: NIGHT 5 RULED — the garden is beyond saving BY DESIGN, and the verdict teaches it
- week.js night 5 carries gardenBeyondSaving: true with the measured case
  (bare 0.0, best buyable 27.7 vs WIN>=50 — B's game-true table, my probe
  agreeing) and the arc argument; nightAt defaults the flag FALSE so new
  nights can never inherit the excuse. Brief levels with the player up front.
- verdictFor honours it on a LOSS only: no more 'the hail fell where your sail
  wasn't' on a night where no placement wins; a 4/4 hold is credited as the
  whole job, a torn sail is still named and owned. A win ignores the flag (a
  stale flag must never eat a real win). Asserted both ways + mutation notes.
- GARDEN_DRAIN untouched, per the standing ruling; icenight hail budget also
  untouched — the loss is the design, so the surfaces now say so.
2026-07-18 14:11:58 +10:00
type-two
1c0fcc757b Sprint 13 gate 1.4 RE-RULED on B's skew finding: the pin measures the game now, and the target is restated
- a.test's separation flight had the phantom 12 s settle B caught (SailRig
  samples wind at its INTERNAL clock; the storm flew 12 s off the authored
  curve). Settle deleted — commit->attach->storm from t=0, C's corrected bench
  pattern; venturi wired from site data so the flight can never assume a yard.
- Game-true the line reads 63.6, not 68.3 — FULL (>66) was met by the harness,
  not the game, and the re-sweep says nothing buyable reaches 66 (the twin-post
  covering quad tears at 6.9 kN). Target RESTATED to the game's real stakes,
  with headroom: held >60 and 0/4 (win line 50; measured 63.6), bare <50
  (35.7), separation >=25 (27.9). Field names kept — B's gardenfly reads them.
- DESIGN CANON in the site block: the wild night's garden is WON SCARRED,
  never FULL; the week's arc is full/full/full, won-tattered, beyond-saving.
- p5 _why table corrected to game-true numbers (first sweep ran ~4 HP hot).
2026-07-18 14:11:58 +10:00
5 changed files with 247 additions and 58 deletions

View File

@ -4705,3 +4705,81 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
land a UI-lifecycle change I haven't watched in play at 1 a.m. on ruling night. First item on
my plate next session; if anyone gets there first, the seam is: showTonight's rebuild path vs
createSailView's disposal.
[A] 2026-07-18 — ⚖️ **GATE 1.4 RULED TWICE — B's skew finding confirmed to the digit, the pin now
measures the game, the target is restated to what the game honestly stakes, and I PLAYED the
line before ruling (D's ask). Also: night 5 ruled. Integration is unblocked from my side.**
**1. The finding, confirmed and fixed.** My a.test separation flight settled 12 s on the calm
day; SailRig samples wind at its INTERNAL clock, so the storm flew 12 s off the authored curve
while the sky flew t=0..90. Game-true (no settle) my chain reads **63.6** where it read 68.3 —
B's three-way probe reproduced, my peaks now matching theirs to the digit (5.10/3.08/2.96/
1.81). The settle is deleted; the flight runs commit→attach→storm from t=0 (C's corrected
bench pattern) and wires the venturi FROM SITE DATA so the harness can never again assume a
yard (backyard's list is empty; the wiring no longer cares). a.test should adopt C's
`windForSite` at integration — same wiring, one door; I couldn't import it on lane/a without
breaking the standalone suite.
**2. Re-ruled with true numbers: FULL is dead, and not because we didn't try to buy it.**
Re-swept game-true: nothing $80 buys reaches 66. The twin-post covering quad (a second low
clothesline post at (3,2) h2.6 — p6+p5+p1+p2, 100% cover) pulls **6.9/6.7/6.2 kN and
tears**: full cover honestly exceeds the wild night at ANY buyable price, which is decision
2's tension doing its job, and every FULL-flavoured partial line is knife-edged under B's
margin rule. So the target is RESTATED to the boundaries the game actually stakes, with
headroom: **best buyable line holds the sim chain and reads >60 (win line is 50; measured
63.6) · bare bed loses <50 (35.7) · separation 25 (27.9)**. Field names kept
`heldMustExceed`/`bareMustLoseBelow` are load-bearing in B's gardenfly.
**3. PLAYED, per D's protocol — and the played number is the ruling's centre.** Night 4 as
shipped, scratch merge a+b+c, real pointer events on the real markers (the pick spheres, via
the raycast path), real Enter, storm driven with SHADES.step, score off the invoice:
**hp 55.7 WIN · 2/4 lost (p2 then p3) · banked +$21 · "THE GARDEN MADE IT. The sail
didn't — the shackle at P2 went first and took 1 more with it. That is what the sail
was for."**
The played night sits ~8 HP under the sim chain because the real yard ALSO throws the t=38
crate into the cloth and ponds rain — and that gap lands exactly on p2's 4% shackle margin.
**RULED DELIBERATE, canon: the wild night sells no clean line.** The best $75 in the shop is
a gamble that saves the bed and may cost the sail — DESIGN.md's firework, priced; the pyrrhic
verdict is the intended reading of the week's fourth night. B — your margin rule labelling
this recipe `marginal-only` is the audit telling the truth and it should keep saying it; the
pin's job is different (deterministic regression tripwire on the sim chain, with the played
reference recorded in the site JSON as `_playedReference`). Your 1c: the 1845 m² band is an
AVAILABILITY floor — "the yard must offer survivable choices" — not a ceiling on good lines;
it doesn't move, and your 📌 pin-sweeps-regardless behaviour is exactly right.
**4. NIGHT 5 RULED — disaster by design, and the surfaces now say so.** Canon: the icenight
garden is BEYOND SAVING at any price (bare 0.0 DEAD; best buyable 27.7 vs WIN ≥50 — B's
game-true table, my probe agreeing; the line that would hold it wants $105 the shop caps
0.05 kN short, C's whisker, kept). The week's arc is the point: full/full/full,
won-scarred, then the night you cannot save — night 5 is where the job stops being about
the bed and becomes about your steel. Landed: `gardenBeyondSaving: true` on week.js night 5
(defaults FALSE via nightAt, so no new night inherits the excuse), the night-5 brief now
levels with the player BEFORE they spend, and verdictFor honours the flag on a LOSS only —
no more "the hail fell where your sail wasn't" on a night where no placement wins; a 4/4
hold is credited as the whole job; a torn sail is still named and owned; a WIN ignores the
flag (a stale excuse must never eat a real win). Asserted both ways. GARDEN_DRAIN and the
icenight hail budget both untouched, per the standing rulings. **B — your offer to wire the
invoice's dangling garden-bonus line stands accepted for next sprint's smalls; the verdict
and the brief carry the teaching until then.**
**5. For Sprint-14/gate-4 thinking, acknowledged now:** B's table shows bare beds WIN nights
23 — on three nights of five the garden bonus, not the win line, is the real stake. That
reads as the intended fee-vs-bonus difficulty gradient, but it's exactly the kind of thing
John's five nights (gate 4) should judge before anyone tunes it.
Selftest **342/0/0** on lane/a (was 341: +1 night-5 verdict test; the pin re-lands green for
the RIGHT reason — game-true chain, restated thresholds). Counts converge at integration.
[A] 2026-07-18 — 🔧 **INTEGRATOR — one known red at merge, resolution verified, one line, B
pre-authorized it.** B's `tools/site_audit/gardenfly.selftest.js` pins the 12s-skew
DISAGREEMENT itself: `sepRun.held.hp <= s.heldMustExceed` — written when heldMustExceed was
66 and game-true 63.8 sat below it. My re-ruling restates heldMustExceed to 60, so on the
merged tree that assert now reds BY DESIGN, and its own message says what to do: "If this
now exceeds the pin, the disagreement is RESOLVED: delete this ceiling, assert heldOk, and
close the [B] THREADS flag" — and the test's header adds "when A re-rules, the constants
below move WITH the ruling." It's B's file, so I did NOT land the edit on lane/a; the exact
change is applied and verified on my scratch merge (commit message "apply B's in-file
resolution", **357/0/0** with everything else green): replace the two-chain ceiling assert
with `assert(sepRun.heldOk, ...)` and keep the skew-chain 68.4 measurement as a drift
tripwire. Everything else in B's suite passes against my restated block unchanged —
`bareMustLoseBelow`, the >25 separation, the $80 cap, and both venturi locks.

View File

@ -115,39 +115,56 @@
"",
"WHY (5.5, -2) at h 2.6 and not somewhere kinder: the LOW top is the point - it twists the",
"p1+p2+p3+p5 quad so it spills the gusts (small+twisted survives, DESIGN.md), and the",
"position is swept against the $80 wallet:",
"position is swept against the $80 wallet. NUMBERS CORRECTED to the game-true chain after",
"B's 12s-skew finding (no phantom settle; the first sweep's hp column ran ~4 HP hot):",
" (4.5,-2) h2.6 -> p1 3.35 kN, needs RATED, line costs $90 DO NOT",
" (5,-2.5) h2.8 -> p5 6.20, p1 3.75 -> $90 DO NOT",
" (5.5,-2) h2.8 -> p2 3.08, p1 3.06 (4% margin on shackle) - legal but knife-edged",
" (5.5,-2) h2.6 -> p5 4.88, p2 3.14, p1 2.91, p3 1.82 -> $75, hp 68.3 FULL <- here",
" (6,-2) h2.6 -> shadow drops, hp 65.8 tattered DO NOT",
"The separation block below pins the recipe; a.test flies it. Full cover STAYS unbuyable:",
"the covering quads (p1+p2+p4+p5 and up) still pull 6.4-6.9 kN and still tear - the",
"smallest full-cover quad stays >45 m2 (a.test's floor holds), so the tradeoff decision 2",
"protects is intact. And the ICE night still refuses the $80 wallet: same line needs $105",
"there (p1 3.25, p2 3.22 vs a shackle's 3.20 - the same few-hundredths miss as C's q3),",
"so night 5 stays the night the bank pays for, exactly as ruled in SPRINT9."
" (5.5,-2) h2.6 -> p5 5.10, p2 3.08, p1 2.96, p3 1.81 -> $75, hp 63.6 WIN <- here",
" (6,-2) h2.6 -> shadow drops DO NOT",
"A second, LOWER-still or wider post cannot buy FULL either: the twin-post covering quad",
"(p6 at (-3,-2) h2.6 + p5 + p1 + p2, 100% cover) pulls 6.9/6.7/6.2 kN and tears - full",
"cover honestly exceeds the wild night at ANY buyable price, which is decision 2's tension",
"doing its job. The separation block below pins the recipe; a.test flies it game-true.",
"The smallest full-cover quad stays >45 m2 (a.test's floor holds). And the ICE night still",
"refuses the $80 wallet: same line needs $105 there (p1 3.25, p2 3.22 vs a shackle's 3.20",
"- the same few-hundredths miss as C's q3), so night 5 stays the night the bank pays for."
]
} ],
"separation": {
"_why": [
"SPRINT13 gate 1.4 — A's ruling on C's proposed target, AMENDED to the numbers the real game",
"measures (ruled on D's funnel-corrected picture, not C's original headline). C proposed:",
"'on a wild night the best buyable line reads FULL (>66) and a bare bed reads DEAD (<33)'.",
"Adopted in spirit, amended in both halves:",
" - The bare half becomes 'a bare bed LOSES the night (hp < 50, the win line)'. Nowhere in",
" the shipped game does a bare bed read DEAD on the wild night (35.7, tattered) — D's",
" check — and tuning storm_02's damage budget until a slogan lands is exactly the fudge",
" gate 1 exists to kill. The felt boundary is the WIN line, and 35.7 fails it honestly.",
" - The FULL half binds THIS yard, because the shipped week's only wild night is here",
" (week.js night 4). C's passing example was site_02, whose wild night the corner block",
" never plays — and with its venturi live the example dies anyway (D's landmine).",
"This block is the pin: the recipe below is the measured best $80 line, a.test flies it",
"through the real chain (session -> commit -> attach -> skyfx) every run, and B's audit can",
"read the same recipe instead of inventing one. A target nothing checks is decoration —",
"the repo's oldest lesson, and the reason this is data and not a THREADS sentence."
"SPRINT13 gate 1.4 — A's ruling on C's proposed target, RULED TWICE and honest the second",
"time. C proposed: 'on a wild night the best buyable line reads FULL (>66) and a bare bed",
"reads DEAD (<33)'. First ruling amended the bare half (nowhere does a bare bed read DEAD;",
"35.7 is tattered, and tuning storm_02 until a slogan lands is the fudge gate 1 exists to",
"kill) and kept FULL — then B's audit rewrite caught that FULL was only ever met through the",
"assert's own 12 s phantom settle (SailRig samples wind at its INTERNAL clock: game-true",
"63.8/63.6, skewed 68.4/68.3). The geometry was re-swept game-true: nothing buyable reaches",
"66 (the twin-post covering quad tears at 6.9 kN; every FULL-flavoured candidate is also",
"knife-edged under B's 15% margin rule). So the target is RESTATED to the boundaries the",
"game actually stakes, with real headroom, instead of pinning another knife edge:",
" - the best buyable line HOLDS (0/4) and WINS COMFORTABLY: hp > 60 (win line is 50;",
" game-true 63.6 - a red means the wild night's best line regressed, not noise),",
" - a bare bed LOSES: hp < 50 (game-true 35.7),",
" - the spread reads as most of a plant state: >= 25 HP (game-true 27.9).",
"DESIGN CANON, written down on purpose: the wild night's garden is WON SCARRED, never FULL,",
"and sometimes the win is PYRRHIC - the sail is spendable, the bed is not. PLAYED, per D's",
"ask (real pointer events on the markers, real Enter, real invoice, night 4 as shipped):",
"hp 55.7 WIN, 2/4 lost (p2 then p3), banked +$21, verdict 'THE GARDEN MADE IT. The sail",
"didn't... That is what the sail was for.' The played number sits ~8 under the pinned",
"sim-chain 63.6 because the real yard also throws the t=38 crate and ponds rain - and that",
"gap lands exactly on p2's 4% shackle margin. RULED DELIBERATE: the wild night sells no",
"clean line (B's margin rule labels this recipe marginal-only, which is the audit telling",
"the truth) - the best $75 the shop has is a gamble that saves the bed and may cost the",
"sail, which is DESIGN.md's firework, priced. The week's garden arc: full/full/full on",
"nights 1-3, won-scarred night 4, beyond saving night 5 (week.js gardenBeyondSaving).",
"The thresholds below pin the SIM CHAIN (rig+sky+garden, deterministic, no debris) as the",
"regression tripwire; the played reference above is what the numbers must keep buying.",
"B's audit reads the same recipe (heldMustExceed/bareMustLoseBelow are B's field names too -",
"renaming them breaks gardenfly). A target nothing checks is decoration - the repo's oldest",
"lesson."
],
"_playedReference": "2026-07-18 UI run, night 4 as shipped: hp 55.7 WIN, 2/4 lost (p2,p3), +$21",
"stormKey": "storm_02_wildnight",
"tension": 1.0,
"line": [
@ -156,8 +173,9 @@
{ "anchor": "p2", "hw": "shackle" },
{ "anchor": "p3", "hw": "shackle" }
],
"heldMustExceed": 66,
"bareMustLoseBelow": 50
"heldMustExceed": 60,
"bareMustLoseBelow": 50,
"separationAtLeast": 25
},
"shed": { "model": "shed_01_v1", "x": 11.8, "z": 6.2, "rotYDeg": -90 },

View File

@ -193,7 +193,7 @@ export function applyMute(sky, on) {
*
* @returns {{verdict: string, mode: string}}
*/
export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped }) {
export function verdictFor({ hp, lost, 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
@ -217,6 +217,25 @@ export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped }) {
verdict: `THE GARDEN MADE IT. The sail didn't — the ${named} went first and took `
+ `${lost.length - 1} more with it. That is what the sail was for.` };
}
// SPRINT13 gate 1.4 — A's night-5 ruling (week.js gardenBeyondSaving): on a
// night whose data says the garden cannot reach the win line at ANY price,
// every "you could have rigged better" sentence below is a lie, and the
// uncovered/rain modes are the worst liars — they blame placement on a night
// where no placement wins. This branch comes before every loss mode: the
// garden's fate was sealed at the forecast; only the STEEL's fate was the
// player's. Note it deliberately does NOT fire on a win — if a retune ever
// makes the flagged night winnable, the flag is stale and week.js says to
// delete it, not to trust this to paper over it.
if (!win && beyondSaving) {
if (lost.length >= 1) {
return { mode: 'beyond-saving',
verdict: `THE BED WAS BEYOND SAVING — no sail in the shop stops this much ice. `
+ `The ${named} letting go is the part that's on you.` };
}
return { mode: 'beyond-saving',
verdict: 'THE BED WAS BEYOND SAVING — no sail in the shop stops this much ice. '
+ 'Yours held anyway. That was the whole job tonight, and you did it.' };
}
if (!win && lost.length >= 2) {
return { mode: 'cascade',
verdict: `THE SAIL LOST. The ${named} went first — and took ${lost.length - 1} more with it.` };
@ -773,7 +792,8 @@ export async function boot(opts = {}) {
*/
const win = hp >= 50;
const dmg = garden.damage;
const { verdict, mode } = verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped });
const { verdict, mode } = verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped,
beyondSaving: week.job.gardenBeyondSaving });
return {
hp,

View File

@ -221,6 +221,10 @@ export default async function run(t) {
assertEq(nightAt(4).storm, 'storm_02b_icenight', 'night five is the ice night');
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');
// 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`);
const w = createWeek();
assertEq(w.night, 1); assertEq(w.bank, 80);
assertEq(w.site, 'backyard_01', 'week.site tracks the ladder');
@ -797,6 +801,39 @@ export default async function run(t) {
assert(hailed.verdict !== rained.verdict, 'hail and rain deaths must not read identically');
});
t.test("night 5's loss is TAUGHT, not blamed: beyondSaving rewrites the garden verdicts only", () => {
// SPRINT13 gate 1.4, the night-5 ruling. Measured game-true (B's pay table,
// my probe agreeing): the icenight garden cannot reach the win line at any
// price — bare 0.0, best buyable 27.7. So week.js flags the night, and the
// verdict must stop implying a better rig existed. The worst liar was
// 'uncovered' ("the hail fell where your sail wasn't") on a night where no
// WHERE saves it.
const dmg = { hail: 90, rain: 5 };
const held = verdictFor({ hp: 25, lost: [], win: false, dmg, pondPeak: 0, pondDumped: 0, beyondSaving: true });
assertEq(held.mode, 'beyond-saving');
assert(/BEYOND SAVING/.test(held.verdict), `says so plainly: "${held.verdict}"`);
assert(/held|did it/i.test(held.verdict), 'and a 4/4 hold is CREDITED — keeping the steel was the job');
assert(!/wasn't|skimp/i.test(held.verdict), 'no placement-blame, no skimp-blame');
// Losing your sail on the unsavable night is still yours to own.
const shackle = { name: 'shackle', rating: 3200 };
const torn = verdictFor({ hp: 5,
lost: [{ hw: shackle, anchorId: 'p1', anchor: { ratingHint: 1 } }],
win: false, dmg, pondPeak: 0, pondDumped: 0, beyondSaving: true });
assertEq(torn.mode, 'beyond-saving');
assert(/shackle at P1/.test(torn.verdict), `still names the steel that went: "${torn.verdict}"`);
// The flag defaults OFF and changes nothing anywhere else: the two garden
// deaths keep their opposite sentences (mutation check — delete the branch
// and the first assert above reds; pass the flag everywhere and THIS one does).
const uncov = verdictFor({ hp: 20, lost: [], win: false, dmg: { hail: 70, rain: 10 }, pondPeak: 0, pondDumped: 0 });
assertEq(uncov.mode, 'uncovered', 'a savable night keeps its honest placement lesson');
// And a WIN on a flagged night ignores the flag — week.js says the flag
// would be stale, and a stale excuse must never eat a real win.
const win = verdictFor({ hp: 90, lost: [], win: true, dmg, pondPeak: 0, pondDumped: 0, beyondSaving: true });
assertEq(win.mode, 'clean');
});
t.test('a clean hold reads as a clean hold, and the broom gets its credit', () => {
const clean = verdictFor({ hp: 96, lost: [], win: true, dmg: { hail: 2, rain: 2 }, pondPeak: 0, pondDumped: 0 });
assertEq(clean.mode, 'clean');
@ -1016,19 +1053,33 @@ export default async function run(t) {
// --- SPRINT13 gate 1.4: THE PINNED SEPARATION TARGET ----------------------
//
// The ruling (THREADS [A], on D's funnel-corrected picture): on the shipped
// week's wild night — night 4, THIS yard — the best line START_BUDGET can buy
// must read FULL (>66) and WIN, and a bare bed must LOSE (<50). The recipe is
// DATA (site.separation) so B's audit reads the same one, and this flight is
// the pin: real shop at the real budget, real commit()->attach() (D's
// skipped-attach landmine — session.commit(rig) IS the attach path, verified
// by C's probe2 agreement), real skyfx exposure, the real garden model out of
// garden.js. Flown OUTSIDE t.test() because Suite.test cannot await — the
// two async-test ghosts above are the precedent.
// The ruling, RESTATED after B's skew finding (THREADS [A], twice — read the
// second entry): on the shipped week's wild night — night 4, THIS yard — the
// best line START_BUDGET can buy must HOLD and WIN comfortably (>60), a bare
// bed must LOSE (<50), and the spread must read as most of a plant state
// (≥25 HP), because the win line and the invoice are the game's real stakes.
// FULL (>66) was the first ruling and it died honestly: it was only ever met
// through this flight's own 12 s phantom settle — SailRig samples wind at its
// INTERNAL clock, so a settled rig flies the storm 12 s off the authored
// curve while the sky flies t=0..90 (B's three-way probe: game-true 63.8 /
// skewed 68.4 / shape-only 64.3). The settle is GONE: in the real game the
// rig does not exist before commit, and commit→attach→storm is one keypress
// (C's bench correction, same day, same bug).
//
// The recipe is DATA (site.separation) so B's audit reads the same one, and
// this flight is the pin: real shop at the real budget, real
// commit()->attach() (D's skipped-attach landmine — session.commit(rig) IS
// the attach path), real skyfx exposure, the real garden model out of
// garden.js. Wind is wired the way main.js wires a site (venturi from site
// data — backyard's list is empty, but the wiring must not ASSUME that — plus
// tree shelters); switch to C's windForSite at integration, it is the same
// wiring behind one door. Flown OUTSIDE t.test() because Suite.test cannot
// await — the two async-test ghosts above are the precedent.
//
// Sway is frozen (balance.test's reasoning): the recipe is all posts, whose
// real sway is constant anyway, so frozen here means "identical to the game",
// not "conveniently calm".
// not "conveniently calm". (C's landmine 2 — a frozen TREE under-reads — is
// exactly why the pinned recipe staying post-only matters to this harness.)
const sep = site.separation;
let sepRun = null;
if (sep && typeof document !== 'undefined') {
@ -1039,7 +1090,6 @@ export default async function run(t) {
});
const trees = frozen.filter((a) => a.type === 'tree');
const stormDef = await loadStorm(sep.stormKey);
const calmDef = await loadStorm(CALM_STORM);
const session = new RiggingSession({ anchors: frozen }); // the REAL budget
const shopLog = [];
@ -1056,16 +1106,12 @@ export default async function run(t) {
session.setTension(sep.tension);
const spent = START_BUDGET - session.budget;
const rig = session.commit(new SailRig({ anchors: frozen, gridN: 10 }));
const calmWind = createWind(calmDef);
calmWind.setSheltersFromTrees(trees);
let prepT = 0;
for (let i = 0; i < Math.round(12 / FIXED_DT); i++) {
rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT;
}
// NO settle here, deliberately — see the header. The rig flies the storm
// from t=0 on a fresh internal clock, exactly like a committed rig does.
const flyNight = (sail) => {
const wind = createWind(stormDef);
wind.setVenturi(site.wind?.venturi ?? []); // main.js:the funnel is SITE data
wind.setSheltersFromTrees(trees);
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
camera.position.set(0, 2, 8);
@ -1092,24 +1138,32 @@ export default async function run(t) {
}
}
t.test('PINNED: the wild night separates — best buyable line FULL and winning, bare bed losing', () => {
t.test('PINNED: the wild night separates — best buyable line holds and wins, bare bed loses', () => {
if (!sep) throw new Error('backyard_01 lost its separation block — the target is decoration again');
if (typeof document === 'undefined') return t.skip('needs DOM (skyfx)');
if (sepRun.err) throw new Error(`separation flight died: ${sepRun.err}`);
assertEq(sepRun.shopLog.join('; '), '', 'the shop sold the whole recipe at the real budget');
assert(sepRun.spent <= START_BUDGET, `recipe costs $${sepRun.spent} — must be buyable night 1`);
assertEq(sepRun.lost, 0, 'the pinned line HOLDS the wild night — 0/4 lost');
// SIM-CHAIN facts, deliberately: this flight is rig+sky+garden with no
// debris events, so it is deterministic and 0/4 here is real headroom. In
// PLAY the yard also throws the t=38 crate and ponds rain — the played
// reference (site JSON _playedReference: hp 55.7 WIN, 2/4 lost, pyrrhic
// verdict) sits ~8 under this chain, and that ~8 is exactly why the
// thresholds below carry margin over the win line instead of hugging it.
assertEq(sepRun.lost, 0, 'the pinned line holds this deterministic chain — 0/4 lost (play may cost the sail; the WIN must survive that)');
assert(sepRun.held.hp > sep.heldMustExceed,
`held bed reads ${sepRun.held.hp.toFixed(1)} — the target is FULL (>${sep.heldMustExceed}). ` +
`Measured 68.3 at landing; a red here means a retune moved the wild night and the ` +
`separation ruling needs re-litigating in THREADS, not a quiet threshold bump.`);
assertEq(sepRun.held.state, 'full', 'and the plants SAY full — the state the player sees');
`held bed reads ${sepRun.held.hp.toFixed(1)} — must stay >${sep.heldMustExceed} so the PLAYED night ` +
`(measured ~8 HP under this chain through crate + ponding) keeps clearing the win line at 50. ` +
`Sim-chain 63.6 / played 55.7 at landing. FULL (>66) was the first ruling and it was only ever met ` +
`through this flight's own 12 s clock skew (B's finding). A red here means a retune moved the wild ` +
`night: re-litigate in THREADS, no quiet threshold bump.`);
assert(sepRun.held.state !== 'dead', 'the held bed is ALIVE — tattered is the wild night speaking');
assertLess(sepRun.bare.hp, sep.bareMustLoseBelow,
`bare bed must LOSE the night (<${sep.bareMustLoseBelow}) — measured 35.7 at landing`);
`bare bed must LOSE the night (<${sep.bareMustLoseBelow}) — game-true 35.7 at landing`);
// The felt spread, stated so a red prints the whole story:
assert(sepRun.held.hp - sepRun.bare.hp > 25,
`separation ${(sepRun.held.hp - sepRun.bare.hp).toFixed(1)} — rigging must matter by a full state, ` +
`not a garden-bonus margin (measured 32.6 at landing)`);
assert(sepRun.held.hp - sepRun.bare.hp >= (sep.separationAtLeast ?? 25),
`separation ${(sepRun.held.hp - sepRun.bare.hp).toFixed(1)} — rigging must matter by most of a ` +
`plant state, not a garden-bonus margin (game-true 27.9 at landing)`);
});
t.test('sway() returns an absolute position, not an offset', () => {

View File

@ -81,7 +81,23 @@ export const NIGHTS = [
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.',
+ '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
// clean 15.3 — every line in the game sits under WIN's hp >= 50, wallet or
// bank. The week's arc WANTS this: full / full / full, won-but-tattered
// (night 4, the pinned separation), then the night you cannot save — the
// icenight is where the job stops being about the bed and becomes about
// your steel (the line that would hold it needs $105 the shop caps at
// 3.20 kN shackles against 3.22/3.25 peaks — C's few-hundredths whisker,
// kept on purpose). This flag exists so surfaces TEACH the loss instead of
// implying a better rig existed: verdictFor reads it (main.js) and stops
// blaming placement; the brief above levels with the player before they
// spend a dollar. If a retune ever makes night 5 winnable, delete this
// flag in the same commit or the verdict lies in the other direction.
gardenBeyondSaving: true,
},
];
@ -106,6 +122,9 @@ export function nightAt(i) {
addr: base.addr ?? null,
brief: base.brief ?? null,
pay: base.pay ?? {},
// False by default on purpose: a night is presumed savable unless its data
// says otherwise, so a new night can never inherit the icenight's excuse.
gardenBeyondSaving: base.gardenBeyondSaving ?? false,
};
}