Merge remote-tracking branch 'origin/lane/e'

# Conflicts:
#	THREADS.md
This commit is contained in:
type-two 2026-07-25 20:03:16 +10:00
commit ecd88f0183
9 changed files with 1071 additions and 3 deletions

View File

@ -10313,3 +10313,291 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
lull and the veer fire TOGETHER, which is not a contradiction — it is a southerly change,
going quiet before it comes back from the other quarter. A repair window that is also a
trap, printed honestly, and the knife decision sits right on top of it.
[E] 2026-07-25 — 🪟 **GATE 4 LANDED: THE GLASSHOUSE TIER — and the gate's premise had already moved.**
**READ THIS FIRST: SPRINT16 ALREADY SHIPPED THE GLASSHOUSE, AND GATE 4'S THREE LITERAL ASKS
WERE ALREADY GREEN.** `glasshouse_01` / `_wrecked` are built, exported, in A's palette, and
pinned by e.test since S16 gate 5: fragility readable at a glance (translucent panes, pinned as
a MATERIAL fact so a repaint that turns the glass opaque goes red), $320 against the gnome's $25
(12.8×), and a wreck that EMPTIES instead of falling (aluminium frame at full height, 34 shards
thrown wider than the footprint). I checked before building anything, because the cheapest way to
ship nothing is to rebuild what exists.
**THE REAL GAP WAS WRITTEN IN MY OWN TEST FILE, IN ITS OWN WORDS** (e.test, S16, above the apex
pin): *"nothing in the sim wrecks a glasshouse yet — the value is inert data"*. `main.js:877`
bills collateral off a **broken rig corner's** baked key (`world.anchor(c.anchorId).collateral`),
and a glasshouse correctly offers **no anchors** — the one rule pinned from every direction, and
it should stay that way. So the most expensive object in the game **could not cost anybody a
cent.** That is RULE 2's mirror image: RULE 2 catches a key with no price; nothing caught a price
with no key that can reach it. **It does now, and that assert is the gate.**
**WHAT DESIGN.md LINE 179 ACTUALLY ASKED FOR:** *"Jobs: hail nets and mesh sails rigged OVER
glass, where your own failure is the worst debris of all."* You do not tie to the glasshouse. You
tie to the frame that STRADDLES it. So the tier is one new asset pair, and the glasshouse itself
is untouched (zero churn on both its GLBs — the control that proves I only added).
**`hail_net_frame_01` / `hail_net_frame_01_wrecked` — 328 / 480 tris, 3.42 × 3.02 × 2.77 m.**
A galvanised-then-powder-coated portal frame: two portals clear of the gable ends (±0.30 m), legs
clear of the long walls (±0.45 m), head beams at 2.75 m so a net clears the 2.28 m ridge, eave
rails, a thin tensioning wire down the middle, and the hail net **rolled** along one eave —
rolled, because the JOB is to rig it, and a deployed net is a finished yard.
**THE TELL IS A POSITION, AND THAT IS THE NEW THING.** Every other trap in this game punishes you
with a bad number on the object: the carport beam at 0.22, the fascia at 0.35, the corroded post
at 0.55 whose real capacity you cannot read. **This frame's numbers are GOOD** — engineered
portal, four real footings, welded factory eyes — and it is the most dangerous anchor in the game
because of WHERE IT STANDS. So the tell is pinned as GEOMETRY off two shipped GLBs, not as paint
on one: the frame's plan footprint **strictly contains** the glasshouse's on both axes
(x ±1.71 vs ±1.32, z ±1.51 vs ±0.98) and its head **clears the ridge by 0.47 m** (2.766 vs
2.30), and all four eyes sit **above** the ridge. "It stands over the glass" is a measurement.
**THE 20 M READ IS METALNESS, NOT LIGHTNESS — S17's lesson, applied deliberately this time.**
S17 measured that galvanised (metalness 0.90) renders **near-black** at yard distance because a
near-mirror with no environment map has nothing to mirror — rgb(49,51,49). So every steel the
player is allowed to trust is DARK. This frame is powder-coat, i.e. **paint**: metalness 0.05,
diffuse, base luminance 0.903 against galvanised 0.734. **Measured off the shipped GLB, then
LOOKED AT in the game's own renderer** (`tools/assetcheck/look.html`, cam at eye height 1.7 m):
at 20 m the honest sail post is a near-black line, the frame is a **white cage**, the glasshouse
is visibly see-through. At 1116 m the rolled net reads as a hard **dark bar** along one eave
(lum 0.254) — which Blender's own contact-sheet view transform had washed out to near-white, the
jacaranda lesson again: a colour judgement made in Blender is a judgement about Blender.
**THE RUNG — 0.80. I OWN IT AS A PROPOSAL, A RULES.** Ladder: 0.22 carport beam < 0.30 carport
post < 0.35 fascia < 0.45 swing frame < 0.55 corroded post < 0.65 pergola < **0.80 hail_net_frame**
< 1.00 sail post. Two halves to the argument:
· **It EARNS the highest rung below the post.** A pergola is a deck screwed to a house and it
flexes; the swing frame stands on grass; the corroded post's capacity is unknowable. This is an
engineered frame on four footings and **the only structure in the palette that was built to be
tied to.** De-rating honest steel for looking dangerous is the corroded post's lie inverted.
· **It is not 1.00 because of SECTION, not condition.** 32 mm sized for knitted hail mesh —
~0.2 kg/m² and near-zero drag, because mesh *spills* wind. A 25 m² impermeable sail in a
southerly is an order of magnitude more load. 0.80 is a 20% de-rate for "engineered for a load
you are about to exceed", and the reasoning is baked on the root as `rated_for`.
**AND 0.80 IS CHOSEN SO THAT IT USUALLY HOLDS. That is the tier, not a softness in it.** A corner
that lets go one night in five costs you a shackle on a sail post and **$320** here. The player
who learned "0.80 is fine" on every other yard is exactly the player this yard is for. The lesson
is not "don't trust this steel" — the steel is fine. It is **"the same failure has a different
price depending on what is underneath it"**, which is the sentence line 179 has been waiting for
an asset to say.
**COLLATERAL: the frame is UNPRICED, BY RULING** (the bike/jacaranda pattern). Its anchors bill
the **glass** at $320 (unchanged, A's number, endorsed). A folded portal frame is obviously also
a repair bill, but one broken corner can name exactly one collateral key, and inventing a second
bill with no mechanism to split it is the guess the invoice exists to refuse. Price it when a
break can bill two things.
**MANIFEST — UNAMBIGUOUS.** `anchor_ratings.gen.js` regenerated: `hail_net_frame` → rating 0.80,
`collateral: "glasshouse"`, **four agreeing sources**, not in `ambiguous`. So a yard can place
this **node-less** (`factoryExtras` carries both the 0.80 AND the 'glasshouse' key off the type)
exactly like the corroded post — D does not have to name nodes to author a glasshouse yard.
It is the second type whose collateral names a **different** structure than itself; `house`
`"gutter"` is the precedent, and the shape is the same: **an anchor names what its failure TAKES,
not what it is.**
**NEGATIVE CONTROLS — five, each witnessed RED in the real harness at :8828, my own fronted tab,
then green. Batched only where the mutations hit disjoint tests, so every red is attributable.**
· **M4 — the anchors stop naming the glass (the PRE-SPRINT state).** Witnessed free, and it is
the headline: on the run where the two GLBs were built but not yet in e.test's own ASSETS list,
the apex pin went red with exactly *"NOTHING in the palette can bill "glasshouse" ($320)"*.
**That is the hole gate 4 was really about, printed by the test that closes it.**
· **M1 — the frame stops bracketing the glass** (CLEAR_X/Y negative): BRACKETS red, *"the frame
spans x 0.86..0.86 and the glasshouse 1.32..1.32"*. **Caught TWICE** — the factory's own dims
band rejects the build outright, so this control needed `--no-verify` to even export. Belt and
braces, and I'll take it.
· **M2 — powder-coat becomes galvanised** (steel_gal at metallic 0.90): METALNESS red, *"the frame
paints at metalness 0.8999…"*. The tell dying **silently** is the whole risk here: the frame
stays white in the palette and goes black on screen.
· **M3 — the frame CLAIMS the glass's key** (`collateral_key = "glasshouse"` on the root): apex
pin red on its second half. This is gate 5's landmine **on my own asset**, which is why the
omission is commented at the bake site rather than merely done.
· **M5 / M6 — the gate-5 site controls**, below.
Counts held at 533 through every control run (530 passed + 3 failed), so no test silently
vanished while I was reddening others.
**DETERMINISM — the rule I set in S17, held.** Three consecutive full factory runs; runs 2 and 3
are **56/56 byte-identical** (39 GLBs, 4 verbatim debris copies, 8 textures, `asset_report.json`,
`contact_sheet.png`, `anchor_ratings.gen.js`).
⚠️ **BLENDER VERSION IS PART OF THE OUTPUT — READ BEFORE YOU BUILD. This box is WRONG.**
The shipped GLBs were exported by **Blender 5.1.2** (glTF I/O v5.1.20); `asset_report.json`
records it in its `blender` field. This box has **5.0.1** (I/O v5.0.21). A full run here rewrote
**all 45 binaries** — every GLB and every texture — and both differences are cosmetic:
· GLB accessor min/max drift in the **7th decimal** (~6e8 m, 60 nanometres). Node graph, mesh
topology, materials and every custom prop are **bit-identical** (verified chunk by chunk).
· The generated PNGs **lose their sRGB/gAMA/cHRM chunks** — exactly 73 bytes each. IDAT pixel
data decompresses **bit-identically**; 5.0.x simply omits the colour-space chunks 5.1.x writes.
**I reverted all 45 and committed only my two new files.** Shipping that churn would have been a
45-binary diff nobody could review, hiding a real (if small) colour-management regression, to add
two assets — my S17 words for this exact shape were *"a corrupt artifact, not a decision"*. It is
now documented at the top of `build_yard_assets.py` so the next E session doesn't ship the
downgrade by accident: if `git status` shows every GLB modified after you added one asset, that is
this. **Better: build on 5.1.2 and the churn is zero.** I did NOT chase a second machine for it —
adding a box to the asset pipeline to chase exporter metadata is an unforced variable, and the
difference is provably semantically nil.
**THE WRECK — and I changed it because I LOOKED.** First cut racked the failed portal 0.30 m in
and kicked its feet 0.16 m out. In the render the top frame **still read as a clean level
rectangle**: a 0.43 m drop on a 2.77 m frame of 32 mm tube is invisible in perspective, and a
wreck you cannot see is not a wreck. Now 0.45 each way — head IN (dragged by the sail), feet OUT
— so the portal **racks into a parallelogram**, the carport's failure mode on a lighter section:
~22° of lean, one foot clear of its pad, one **orphaned footing**, the top frame a visible
trapezoid, and the net off its lashings draped down over the glass and heaped past the footing.
Verified at 16 m in the game's renderer: unmistakably broken at a glance.
The head beam rests **ACROSS the ridge** (2.33) rather than below it, deliberately: the
glasshouse's own wreck keeps its aluminium frame standing at full height, so a beam that ended up
under the ridge line would be *inside* it. **The wreck carries NO GLASS** (pinned): the glasshouse
swaps independently, and shards in both would double-draw the night both swap — and would quietly
claim the glass's failure as this asset's. **No anchor survives the wreck** (pinned; fascia rule,
sixth application). Same height band as the intact frame, because a portal frame folds at one end
— it does not lie down.
**PLACEMENT FACTS, three.js coords, measured off the exported GLBs (not off the builder):**
· `hail_net_frame_01` — footprint **3.42 × 3.02 m**, height **2.766 m**. Four eyes,
`net_anchor_01..04`, all at **y = 2.690**, on the corners of a 3.20 × 2.80 m rectangle:
**(1.600, 2.690, +1.400) · (+1.600, 2.690, +1.400) · (+1.600, 2.690, 1.400) ·
(1.600, 2.690, 1.400)**. Sized around `glasshouse_01` (2.65 × 1.95 × 2.30): **place both at
the same x/z and the frame straddles it correctly with no fiddling.**
· `hail_net_frame_01_wrecked`**WRECK APRON: reserve ~0.9 m of clear ground on X** beyond the
footing line (x reaches 2.59 against the intact 1.71; the **net heap** sets that bound, not
the racked feet). Height 2.783 m. It is the far side from the standing portal.
**📋 A / INTEGRATOR — EDITOR PALETTE HUNK, APPLY VERBATIM** (editor.js is A's; leadFor precedent,
eighth application). Insert next to the glasshouse. A's own palette test uses `PALETTE.find`, not
a length assert, so this adds no rows to a.test:
```js
{
// SPRINT18 gate 4, E. The glasshouse tier's LIVE half — the frame you rig a
// hail net FROM, straddling the glass you must never rig TO. DESIGN.md line
// 179: "hail nets and mesh sails rigged over glass, where your own failure
// is the worst debris of all." Place it at the same x/z as a glasshouse and
// it straddles correctly; the eyes land at y=2.690, clear of the 2.28 ridge.
//
// 0.80, and it USUALLY HOLDS — that is the tier, not a softness in it. Every
// other trap here punishes you with a bad number; this is good steel
// (engineered portal, four footings, welded eyes) and the hazard is WHERE IT
// STANDS. The corner failure that costs a shackle on a sail post costs $320.
//
// ⚠️ `_v1` is LOAD-BEARING and its absence is SILENT: adoptAnchor does
// `rating_hint ?? 1`, so a typo does not fail — it rates these four eyes
// 1.00, the best steel in the game, on the highest-collateral anchor there
// is. (S14, D's cold pass.)
// ⚠️ THIS ITEM WRITES NO collateralKey, ON PURPOSE. The anchors bill
// 'glasshouse'; the GLASSHOUSE owns that key. structFor() returns the FIRST
// structure matching a key, so a frame that claimed it would bill the panes
// and then wreck the FRAME — both papers agreeing and both wrong.
// ⚠️ PLACE IT OVER A GLASSHOUSE. With no glasshouse in the site,
// collateralFor('glasshouse') returns null and these four corners are a FREE
// failure — the gutter bug. e.test pins that per-yard and names the anchor.
kind: 'structure', label: 'Hail-net frame (0.80 — over the glass, $320 under it)',
model: 'hail_net_frame_01_v1',
requires: ['hail_net_frame'],
hint: 'four good eyes at 2.7 m, rated 0.80 — and everything they drop lands on the glasshouse',
make: (id, x, z) => ({
id, model: 'hail_net_frame_01_v1',
wreckedModel: 'hail_net_frame_01_wrecked_v1',
x, z, rotYDeg: 0, solid: true,
anchors: [1, 2, 3, 4].map((i) => ({
id: `${id}_a${i}`, node: `net_anchor_0${i}`, type: 'hail_net_frame', work: 'cloth',
})),
}),
},
```
⚠️ **A — ANCHOR_TYPE WIDENED** (contracts.js is yours; carport / swing_frame / pergola /
corroded_post precedent, fifth application). I added `'hail_net_frame'` with its reasoning beside
it, including why it must NOT be typed `post`: "post" promises a corner whose failure costs a
shackle. Revert-and-tell-me standard; your own "the widened enum is CHECKED" test covers it and
is green.
**🪟 A — THE TIER IS PLACEABLE, AND THE ONE-VARIABLE LAW IS YOURS TO ENFORCE.** Both GLBs are
landed, deterministic and manifest-resolvable **node-less**, so a glasshouse yard can be authored
now. **Per SPRINT18's own hard gate: this must NOT debut on the same night as the emergency
callout**, however well line 29 pairs them — the glasshouse debuts as an ordinary
rig-for-tonight job, and the flogging-sail-over-glass sentence is the payoff, a later week. I am
supplying it and saying so; the ordering is yours.
[E] 2026-07-25 — 🔑 **GATE 5 HALF-LANDED: THE SECOND-RUST-POST LANDMINE — my half, and the exact
wiring A needs. DISARMED AT AUTHOR TIME; the runtime fix is one `??` and it is A's.**
D's filing, with D's measured receipt ($115 on the offer card vs $45 on the failure route). The
mechanism, re-read at source and confirmed: `world.js:526` `structFor(key)` iterates
`structures.values()` and **returns the first match**; `main.js:877` dedupes bills by key;
`scorecard.js:355` `collateralExposure` dedupes by key again; `board.js:330` `exposureOf` counts
per **STRUCTURE**. All four are individually correct and they disagree the moment two structures
share a key. **The pool yard dodges it by shipping exactly one corroded post on purpose — that is
a yard author remembering, which is not a defence.**
**AND GATE 4 JUST MADE IT LIKELIER, which is why these two gates are one seam.** A glasshouse
yard now carries a structure that **owns** the key and a frame whose anchors **point at** it. Two
glasshouses with a frame each trips this on the first night. So my own asset carries the omission
deliberately (no `collateral_key` on the frame root) with the reasoning commented at the bake.
**MY HALF — two pins in e.test, both witnessed red then green:**
· **`no shipped yard gives two structures the same collateral key, and ids are unique`.** Resolves
each structure's key exactly the way `structKey()` does (site JSON → baked GLB key → id as last
resort), across all four shipped yards. **M5 control — I added a second corroded post to the
pool yard** and it went red naming both. It also pins **duplicate structure ids**, which is a
separate silent death I found while reading: `structures.set(st.id, …)` means a duplicate id
**silently replaces** the first structure — it never renders, never bills, never wrecks, and
nothing anywhere says a word. `validateSite` checks duplicate ANCHOR ids and not structure ids.
· **`every collateral an authored anchor names is OWNED by exactly one thing in that yard`.** The
runtime twin of RULE 2: RULE 2 proves a key is priced *somewhere in the palette*, this proves it
is owned *in the yard that names it*. **M6 control — I placed the net frame in the pool yard,
which has no glasshouse** → red: *"nf1_a1 will bill "glasshouse" on failure, and 0 things in
this yard own that key"*. **Zero owners is the gutter bug reborn**, and it is now caught before
a yard ships rather than by a player breaking a corner for free.
**🔧 A — THE WIRING, EXACT. Two hunks in world.js (yours) and one in validateSite (yours).**
The reason a site cannot currently just give two posts different keys: `structKey()` already
prefers `spec.collateralKey`, so the STRUCTURE half works today. The **anchor** half does not —
`adoptAnchor` takes the key off the baked GLB node, identical for every instance, and
`factoryExtras(type)` does the same for node-less anchors. So renaming the structures' keys makes
it **worse, not better**: both anchors still say `'corroded_post'`, which now matches neither
`'corroded_post_a'` nor `'corroded_post_b'`, `collateralFor` returns null, and both corners
become **FREE**. Hence:
1. **`world.js:541`** (structure anchor creation) — let the site name its own target, applied
AFTER `factoryExtras` so it wins:
```js
anchors.push(Object.assign(
makeStaticAnchor(a.id, a.type ?? 'post', new THREE.Vector3(st.x, heightAt(st.x, st.z) + 2.4, st.z)),
{ work: a.work },
a.node ? null : factoryExtras(a.type ?? 'post'),
a.collateral ? { collateral: a.collateral } : null, // SPRINT18 gate 5
));
```
2. **`world.js:690`** (`adoptAnchor`) — one `??`, so the bake is the DEFAULT and never clobbers an
override:
```js
anchor.collateral = anchor.collateral ?? node.userData?.collateral ?? null;
```
Every shipped site is byte-unchanged by both hunks: with no override the chain is identical.
3. **`validateSite`** — the loud author-time refusal, mirroring my pin, next to the existing
duplicate-anchor-id check: reject two structures resolving to one collateral key, and reject
duplicate structure ids.
**WHAT THAT BUYS, PRECISELY.** A yard can then ship two corroded posts: `collateralKey`
`corroded_post_a` / `_b` on the structures, and the matching `collateral` on each structure's
anchors. `collateralFor` resolves each to its own structure (the VALUE lives on the site spec, so
$45 each), the right post wrecks, and **the two harnesses agree**: `exposureOf` counts two
structures = $90, `collateralExposure` sees two keys = $90. **D's $115-vs-$45 split closes to
$115 vs $90** — and the residual **$25 is exactly the gnome**, which `collateralExposure` excludes
because no anchor reaches it. That is not a bug, it is **your ruling to make**: does an unreachable
priced prop belong in the offer card's exposure total? I have no vote; I am handing you a number
with one variable left in it instead of two.
**AND THE SAME WIRING IS WHAT THE GLASSHOUSE TIER WILL WANT** for two-glasshouse yards, which is
why I did not invent a second mechanism for it.
[E] 2026-07-25 — ✏️ **CORRECTION TO MY OWN ENTRY, SAME SESSION: I overwrote the field I told you to
trust.** The gate-4 entry above says *"the shipped GLBs were exported by Blender 5.1.2;
`asset_report.json` records it in its `blender` field — check it before you build."* **That
instruction is now wrong, and I made it wrong.** My run regenerated the report, so the committed
`asset_report.json` reads **`"blender": "5.0.1"`** — this box — while 37 of the 39 GLBs beside it
are still the **5.1.2** exports I deliberately reverted. Read literally, the field now tells the
next session "you are on the right Blender" at the exact moment they are not, which is the same
shape as every trap this sprint went hunting: a source of truth that quietly starts answering a
different question.
**What is actually true of the committed tree:** `hail_net_frame_01_v1.glb` and
`hail_net_frame_01_wrecked_v1.glb` were built by **5.0.1**; the other 37 GLBs and all 8 textures
are **5.1.2** artifacts, untouched. The report's per-asset facts (dims, tris, node names, baked
extras) are correct for every one of the 39 either way — that is measured, not assumed, and it is
why reverting was safe.
**So `asset_report.json:blender` names THE LAST MACHINE THAT RAN THE FACTORY, not the machine
that built the GLBs you are looking at.** Those are the same thing only after a churn-free run.
The honest check is the one I actually used: build, then `git status` — **if files you did not
author come back modified, you are on the wrong Blender**, and the header of
`build_yard_assets.py` tells you what the differences mean. I am leaving the generated file alone
rather than hand-editing it (it says "do NOT hand-edit" for good reasons, and a doctored report is
worse than a mislabelled one); the fix is this paragraph, and the mixed-exporter state going into
the integrator's eyes on purpose. If someone runs the full factory on a 5.1.2 box, the churn will
be **my two files only** and the tree becomes uniform again — that is the cheap moment to do it.

View File

@ -1,5 +1,5 @@
{
"blender": "5.1.2",
"blender": "5.0.1",
"assets": [
{
"name": "ref_capsule",
@ -817,6 +817,73 @@
"status": "PASS",
"problems": []
},
{
"name": "hail_net_frame_01",
"dims": [
3.42,
3.02,
2.766
],
"tris": 328,
"nodes": [
"footings",
"hail_net_frame_01",
"legs",
"net_anchor_01",
"net_anchor_02",
"net_anchor_03",
"net_anchor_04",
"net_roll",
"top_frame"
],
"anchors": [
{
"node": "net_anchor_01",
"anchor_type": "hail_net_frame",
"rating_hint": 0.8,
"collateral": "glasshouse"
},
{
"node": "net_anchor_02",
"anchor_type": "hail_net_frame",
"rating_hint": 0.8,
"collateral": "glasshouse"
},
{
"node": "net_anchor_03",
"anchor_type": "hail_net_frame",
"rating_hint": 0.8,
"collateral": "glasshouse"
},
{
"node": "net_anchor_04",
"anchor_type": "hail_net_frame",
"rating_hint": 0.8,
"collateral": "glasshouse"
}
],
"status": "PASS",
"problems": []
},
{
"name": "hail_net_frame_01_wrecked",
"dims": [
4.3007,
3.02,
2.7832
],
"tris": 480,
"nodes": [
"footings",
"hail_net_frame_01_wrecked",
"legs",
"net_down",
"top_frame"
],
"anchors": [],
"status": "PASS",
"problems": []
},
{
"name": "pool_kit_01",
"dims": [
@ -955,5 +1022,42 @@
"problems": []
}
],
"debris": []
"debris": [
{
"file": "BlueCrate_v2.glb",
"dims": [
0.36,
0.36,
0.29
],
"sane": true
},
{
"file": "BlackTub_v2.glb",
"dims": [
0.36,
0.54,
0.2
],
"sane": true
},
{
"file": "WhiteTub_v2.glb",
"dims": [
0.36,
0.54,
0.2
],
"sane": true
},
{
"file": "WoodenBin_v2.glb",
"dims": [
0.35,
0.36,
0.31
],
"sane": true
}
]
}

View File

@ -20,6 +20,25 @@ Outputs (resolved from this file, so no absolute home paths):
tools/blender/contact_sheet.png verification render vs the 1.7 m capsule
tools/blender/asset_report.json measured dims / tris / node names
BLENDER VERSION IS PART OF THE OUTPUT (SPRINT18, E measured, not guessed).
The shipped GLBs were exported by Blender **5.1.2** (glTF I/O v5.1.20), which
`asset_report.json` records in its `blender` field check it before you
build. On Blender 5.0.1 (I/O v5.0.21) this script is still perfectly
deterministic three consecutive full runs gave 56/56 byte-identical
artifacts but its output DIFFERS from the shipped files in two ways, and
both are cosmetic:
· GLB accessor min/max drift in the 7th decimal (~6e-8 m, i.e. 60 nm).
Node graph, mesh topology, materials and every custom prop are
bit-identical; only float formatting of the bounds moves.
· The generated PNGs lose their sRGB/gAMA/cHRM chunks 73 bytes per file.
IDAT pixel data decompresses bit-identically; the newer exporter simply
writes colour-space chunks that 5.0.x omits.
So a full run on the wrong Blender rewrites all 45 binaries with no change
anybody asked for, and quietly DROPS the colour-space chunks. If your `git
status` shows every GLB and texture modified after adding one asset, that is
this and not your work: `git checkout --` the ones you did not author, and
commit only the new files. Better: build on 5.1.2 and the churn is zero.
Idiom follows ~/Documents/Destroyulater/3D-STORE/racks_to_glb.py:
reset_to_empty() per asset -> build under a root empty AT THE ORIGIN ->
join by group -> stamp custom props -> export_scene.gltf(export_yup=True,
@ -150,6 +169,23 @@ PAL = {
# a pool is the one water in the game that is
# supposed to look inviting
"deck_board": "#A8845C", # decking oil, a shade lighter than fence timber
"net_frame_white": "#E6E7E1", # SPRINT18 gate 4 — the hail-net frame's
# powder-coat, and the BRIGHTEST entry in this
# palette on purpose. The reason is metalness,
# not lightness — S17's measured lesson applied
# deliberately this time. Galvanised ships at
# metallic 0.90, and with no environment map a
# near-mirror has nothing to mirror, so every
# steel you are ALLOWED to trust renders
# near-black at yard distance (measured: rgb
# 49,51,49 at 20 m). Powder-coat is paint —
# metallic 0.05, diffuse — so this frame reads
# as a white cage standing over the glass from
# the far fence, and can never be misread as
# the galvanised steel of an honest sail post.
"net_mesh": "#3A4340", # knitted HDPE hail mesh: dark, matte, and the
# only large dark mass any yard carries up at
# 2.7 m. Rolled it is a bar; down it is a heap.
}
# Rainwater caught in a sail is not swimming-pool blue. It's shallow, murky, it
@ -1916,6 +1952,283 @@ def build_glasshouse_01_wrecked(name):
return root
# ---------------------------------------------------------------------------
# THE HAIL-NET FRAME (SPRINT18 gate 4 — the glasshouse tier's LIVE half)
# ---------------------------------------------------------------------------
# SPRINT16 shipped the glasshouse and priced it at 320. SPRINT18 read the gate
# and found the thing the price was waiting on, written in e.test's own words:
# "nothing in the sim wrecks a glasshouse yet — the value is inert data". The
# apex could not be reached. main.js bills collateral off a BROKEN RIG CORNER
# (`world.anchor(c.anchorId).collateral`), and a glasshouse deliberately offers
# no anchors — correctly, it is the one rule pinned from every direction — so
# the most expensive object in the game could not cost anybody a cent. That is
# the free-failure bug inverted: not a hazard billing nothing by accident, but
# a hazard that cannot bill at all by construction.
#
# DESIGN.md line 179 already named the missing asset: "Jobs: hail nets and mesh
# sails rigged OVER glass, where your own failure is the worst debris of all."
# You do not tie to the glasshouse. You tie to the frame that STRADDLES it —
# and every corner of that frame is a corner whose failure drops steel, cloth
# and hardware through fourteen panes.
#
# So this is the tier, and its teaching is new. Every other trap in this game
# punishes you with a bad number: the carport beam at 0.22, the fascia at 0.35,
# the corroded post at 0.55 whose real capacity you cannot read. This one is
# GOOD hardware — purpose-built, plumb, on its own footings, factory eyes —
# and it is the most dangerous anchor in the game because of WHERE IT STANDS.
# The rating is not the hazard. The position is.
NET_FRAME = dict(
CLEAR_X=0.30, # portal stands this far clear of each gable end
CLEAR_Y=0.45, # legs stand this far outside the long walls, on soil
HEAD_H=2.75, # head-beam height. MUST clear the ridge (2.28) — a frame
# that fouled the glass it protects is a joke, and
# e.test measures the clearance off the two shipped GLBs.
TUBE=0.032, # 32 mm net-frame section
PAD=0.22, # footing pad
)
# The rung. PROPOSAL — Lane A rules the number, same as every rung before it.
#
# Ladder as shipped: 0.22 carport beam < 0.30 carport post < 0.35 house fascia
# < 0.45 swing frame < 0.55 corroded post < 0.65 pergola < 1.00 sail post.
# This goes at 0.80, and the argument has two halves:
#
# · WHY IT OUT-RATES THE PERGOLA (0.65) BY A DISTANCE. The pergola is a deck
# structure screwed to a house and it flexes; the corroded post's capacity
# is unknowable; the swing frame stands on grass. This is an engineered
# portal frame on four real footings with welded eyes, and it is the only
# structure in the palette that was BUILT to be tied to. Rating it near the
# bad steel would be the lie the corroded post exists to punish, inverted:
# honest steel de-rated for looking dangerous.
# · WHY IT IS NOT THE SAIL POST (1.00). The section is 32 mm sized for hail
# mesh — a couple of hundred grams per square metre, and near-zero drag
# because mesh SPILLS wind rather than catching it. A 25 m² impermeable
# shade sail in a southerly is a load one order of magnitude larger. 0.80
# is a 20% de-rate for "engineered for a load you are about to exceed".
#
# And 0.80 is chosen so that IT USUALLY HOLDS. That is the point of the tier,
# not a softness in it: a corner that fails one night in five costs you nothing
# on a sail post and 320 dollars here. The player who learns "0.80 is fine" on
# every other yard is exactly the player this yard is for. The lesson is not
# "don't trust this steel" — the steel is fine. It is "the same failure has a
# different price depending on what is underneath it", which is the sentence
# DESIGN.md line 179 has been waiting for an asset to say.
NET_FRAME_RATING = 0.80
def _net_frame(name, root, steel, conc, mesh_m, *, wrecked=False):
"""The portal frame, shared by intact and wreck so the two cannot drift.
The wreck fails at the -x portal ONLY: its feet kick out of their pads and
its head beam comes down onto the glasshouse ridge. +x stands, holding the
eave rails up, which is why the wreck keeps the intact height a hail-net
frame does not fall over, it folds at one end, and the end it folds is the
end that empties the glass."""
G, N = GLASSHOUSE, NET_FRAME
hx = G["W"] / 2 + N["CLEAR_X"] # 1.60 — clear of the gable ends
hy = G["D"] / 2 + N["CLEAR_Y"] # 1.40 — clear of the long walls
H, r, pad_h = N["HEAD_H"], N["TUBE"] / 2, 0.06
def foot(sx, sy):
"""Where the leg meets the ground. Wrecked -x feet are OUT of the pad —
a foot beside its own footing is the most legible steel failure there
is, and it costs one offset to say."""
if wrecked and sx < 0:
return (sx * hx - 0.45, sy * hy, pad_h + (0.14 if sy > 0 else 0.0))
return (sx * hx, sy * hy, pad_h)
def head(sx, sy):
"""Where the leg meets the top frame. The wrecked -x portal has come
down and is RESTING ON THE RIDGE (2.28) rather than passing through it:
the glasshouse's own wreck keeps its aluminium frame standing at full
height, so a beam that ended up below the ridge line would be inside
it. Lying ACROSS the ridge is both true and non-interpenetrating.
The head comes IN (+x, dragged by the sail) while the feet kick OUT
(-x) the portal RACKS into a parallelogram, the carport's failure
mode on a lighter section. The offsets are deliberately large (0.45
each way, ~22° of lean over 2.3 m) because I looked at 0.30/0.16 in
the render and the top frame still read as a clean level rectangle:
a 0.43 m drop on a 2.77 m frame of 32 mm tube is invisible in
perspective, and a wreck you cannot see is not a wreck."""
if wrecked and sx < 0:
return (sx * hx + 0.45, sy * hy * 0.90, G["RIDGE_H"] + 0.05)
return (sx * hx, sy * hy, H)
def mid(sx):
a, b = head(sx, -1), head(sx, 1)
return ((a[0] + b[0]) / 2, 0.0, (a[2] + b[2]) / 2)
pads, legs = [], []
for sx in (-1, 1):
for sy in (-1, 1):
pads.append(add_box(f"{name}_pad_{sx}_{sy}", (N["PAD"], N["PAD"], pad_h),
(sx * hx, sy * hy, pad_h / 2), conc))
legs.append(add_tube_between(f"{name}_leg_{sx}_{sy}",
foot(sx, sy), head(sx, sy), r, steel, verts=8))
join_group(pads, "footings", root)
join_group(legs, "legs", root)
top = []
for sx in (-1, 1): # portal head beams, spanning y
top.append(add_tube_between(f"{name}_head_{sx}", head(sx, -1), head(sx, 1),
r, steel, verts=8))
for sy in (-1, 1): # eave rails, spanning x
top.append(add_tube_between(f"{name}_eave_{sy}", head(-1, sy), head(1, sy),
r, steel, verts=8))
# The tensioning wire down the middle, deliberately thinner than the frame:
# at 20 m the gauge difference is the "made for a net" read, and it is the
# honest reason the whole thing rates 0.80 instead of 1.00.
top.append(add_tube_between(f"{name}_ridge_line", mid(-1), mid(1),
r * 0.35, steel, verts=6))
join_group(top, "top_frame", root)
if not wrecked:
# Rolled along the +y eave, because the JOB is to rig it. A deployed
# net would be a finished yard, and this asset exists to be worked on.
join_group([add_tube_between(f"{name}_roll", (-hx, hy, H - 0.11),
(hx, hy, H - 0.11), 0.085, mesh_m, verts=10)],
"net_roll", root)
else:
# YOUR debris, not the storm's. The net has come off its lashings at the
# collapsed end and is draped down over the glass and heaped out past
# the footing — deterministic scatter, so the same wreck twice.
rng = rng_for(name)
drape = []
pts = [(hx * 0.85, hy, H - 0.10),
(hx * 0.10, hy * 0.92, H - 0.78),
(-hx * 0.55, hy * 0.80, G["RIDGE_H"] * 0.55),
(-hx - 0.28, hy * 0.55, 0.24),
(-hx - 0.80, hy * 0.20, 0.06)]
for i in range(len(pts) - 1):
drape.append(add_tube_between(f"{name}_net_sag_{i}", pts[i], pts[i + 1],
0.055, mesh_m, verts=6))
for i in range(9):
a = rng.uniform(0, math.tau)
rr = rng.uniform(0.05, 0.45)
drape.append(add_box(
f"{name}_net_heap_{i}",
(rng.uniform(0.22, 0.50), rng.uniform(0.16, 0.40), rng.uniform(0.03, 0.09)),
(-hx - 0.58 + math.cos(a) * rr, hy * 0.20 + math.sin(a) * rr * 0.8,
rng.uniform(0.02, 0.07)),
mesh_m, rot=(0, 0, rng.uniform(0, math.pi))))
join_group(drape, "net_down", root)
return hx, hy
def build_hail_net_frame_01(name):
"""The hail-net frame — four good anchors in the worst place in the game.
The glasshouse tier's live half (SPRINT18 gate 4). This is what DESIGN.md
line 179 means by "hail nets and mesh sails rigged OVER glass": a portal
frame that straddles the glasshouse, clear of it on all four sides, with a
head height that clears the ridge and a rolled mesh net waiting to go up.
THE TELL, and it is a position rather than a defect. Every reading of every
other trap in this game is a bad number on the object. This object's numbers
are GOOD 0.80, plumb, four footings, welded eyes and what the player has
to read instead is the plan view: the frame BRACKETS the glass. e.test pins
that geometrically off the two shipped GLBs (frame footprint strictly
contains the glasshouse footprint; head height strictly clears the ridge),
because "it stands over the glass" has to be a measurement, not a vibe.
The 20 m read is the powder-coat: white, matte, metallic 0.05, against a
palette where every trustworthy steel is metallic 0.90 and therefore renders
near-black. A white cage over a glass box, visible from the far fence.
WHAT THE ANCHORS SAY. All four bake collateral="glasshouse". They are the
only nodes in the palette whose collateral names a DIFFERENT structure than
the one they sit on (the house's fascia→"gutter" is the precedent, and it is
the same shape: the anchor names what its failure TAKES, not what it is).
Break a corner here and the sail, the shackle and the turnbuckle go through
fourteen panes: your own failure is the worst debris of all, wired as data.
AND WHAT THIS ROOT DELIBERATELY DOES NOT CARRY: collateral_key. See below
it is the whole of gate 5 in one omission.
"""
root = add_empty(name)
# Powder-coat is PAINT, so metallic stays near zero. This is not a style
# choice, it is the S17 measurement: high metalness with no environment map
# renders near-black, and this asset's entire long-range tell is that it is
# the brightest thing in the yard.
steel = get_material("Mat_NetFrame", PAL["net_frame_white"], 0.50, metallic=0.05)
mesh_m = get_material("Mat_HailNet", PAL["net_mesh"], 0.85, metallic=0.0)
conc = get_material("Mat_Concrete", PAL["concrete"], 0.95)
hx, hy = _net_frame(name, root, steel, conc, mesh_m)
# The four eyes, at the four corners of the top frame — where a net corner
# ties, and therefore where a sail corner will be tied instead.
for i, (sx, sy) in enumerate(((-1, -1), (1, -1), (1, 1), (-1, 1))):
e = add_empty(f"net_anchor_{i + 1:02d}",
(sx * hx, sy * hy, NET_FRAME["HEAD_H"] - 0.06), root, size=0.18)
e["anchor_type"] = "hail_net_frame"
e["rating_hint"] = NET_FRAME_RATING
e["collateral"] = "glasshouse"
e["why"] = ("sound steel, and the most expensive corner on the board: it is "
"rated for hail mesh, and everything it drops lands on glass")
stamp(root, name, "structure")
root["site_hint"] = "glasshouse"
root["is_anchor_trap"] = True
root["rated_for"] = ("knitted hail mesh, ~0.2 kg/m2, near-zero drag — mesh spills "
"wind. A 25 m2 impermeable sail is an order of magnitude more.")
# ⚠️ NO collateral_key ON THIS ROOT, AND THAT IS THE POINT (gate 5).
#
# world.js resolves a collateral key to a structure with structFor(), which
# returns the FIRST structure in site order whose key matches. If this frame
# claimed collateral_key="glasshouse" — which looks like the helpful thing
# to do, since its anchors bill that key — then in a yard where the frame is
# authored before the glasshouse, every failure would bill "the glasshouse"
# for 320 dollars and then WRECK THE FRAME: the player watches their own
# frame fold while the invoice charges them for panes that are still in
# their rebates. The bill and the wreck would agree with each other and
# both be wrong, which is the only kind of wrong this repo cannot see.
#
# So exactly ONE structure owns a key, and it is the one that owns the
# price. This frame POINTS at the key and never claims it. The pointer
# lives in a field nothing resolves, so it can document intent without
# arming anything.
root["collateral_target"] = "glasshouse"
root["rigs_over"] = "glasshouse_01"
return root
def build_hail_net_frame_01_wrecked(name):
"""The frame after the corner you trusted let go.
The -x portal has folded: both its feet are out of their pads, and its head
beam is lying across the glasshouse ridge with the net draped down over the
glass and heaped out past the footing. The +x portal stands, holding the
eave rails at full height so this wreck is the same height as the intact
frame, which is the honest read: a portal frame folds at one end, it does
not lie down.
It carries NO GLASS. The glasshouse has its own wreck variant and swaps
independently; a net-frame wreck that shipped shards would draw them twice
the moment both swapped on the same night, and would also quietly claim
the glass's failure as its own. e.test pins the absence.
UNPRICED, BY RULING the bike/jacaranda pattern. A folded portal frame is
obviously a repair bill, but the player is being taught about the glass, and
one broken corner can only name one collateral key. Inventing a second bill
with no mechanism to split it between them is exactly the guess the invoice
exists to refuse. Price it when a break can bill two things."""
root = add_empty(name)
steel = get_material("Mat_NetFrame", PAL["net_frame_white"], 0.50, metallic=0.05)
mesh_m = get_material("Mat_HailNet", PAL["net_mesh"], 0.85, metallic=0.0)
conc = get_material("Mat_Concrete", PAL["concrete"], 0.95)
_net_frame(name, root, steel, conc, mesh_m, wrecked=True)
stamp(root, name, "structure")
root["broken_variant_of"] = "hail_net_frame_01"
# No anchor survives the wreck — the fascia rule, sixth application. An
# anchor that outlives its structure is the free-failure bug in a costume.
root["collateral_target"] = "glasshouse"
return root
# ---------------------------------------------------------------------------
# THE POOL KIT (SPRINT16 gate 5 — the compliant ring whose steel all says no)
# ---------------------------------------------------------------------------
@ -3824,6 +4137,25 @@ ASSETS = [
dict(name="glasshouse_01_wrecked", fn=build_glasshouse_01_wrecked,
dims=((3.4, 5.4), (2.6, 3.8), (2.15, 2.40)),
nodes=["frame", "door", "panes_left", "shards"]),
# SPRINT18 gate 4 — the glasshouse tier's live half. Every band here is a
# design claim about the glass underneath it, which is why they are tight:
# x and y floors must both EXCEED the glasshouse's ceilings (2.8 / 2.1), or
# the frame has stopped straddling the thing it bills; z must clear the
# ridge (2.40 ceiling) or a frame built to protect glass is resting on it.
dict(name="hail_net_frame_01", fn=build_hail_net_frame_01,
dims=((3.30, 3.55), (2.90, 3.15), (2.70, 2.85)),
nodes=["footings", "legs", "top_frame", "net_roll",
"net_anchor_01", "net_anchor_02", "net_anchor_03", "net_anchor_04"]),
# Folded at one end, not felled: the SAME z band as the intact frame,
# because the +x portal stands and holds the eave rails up. If z ever
# collapses, someone has laid a portal frame down flat and it never does
# that. x runs WIDE on the -x side — the net heaped out past the footing,
# and the wreck apron a yard author has to keep clear.
# 4.10 floor, not 4.30: the measured span is 4.30 and it is set by the NET
# HEAP, not by the racked feet, so it must not sit on the band boundary.
dict(name="hail_net_frame_01_wrecked", fn=build_hail_net_frame_01_wrecked,
dims=((4.10, 5.30), (2.90, 3.15), (2.70, 2.85)),
nodes=["footings", "legs", "top_frame", "net_down"]),
# The compliance ring. z is pinned tight around 1.2 m because the height
# IS the flavour (AS 1926.1); y reaches past the ring for the ajar gate.
dict(name="pool_kit_01", fn=build_pool_kit_01,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

@ -80,6 +80,16 @@ export const FACTORY_ANCHOR_RATINGS = Object.freeze({
"sail_post_corroded/top_anchor"
]
},
"hail_net_frame": {
"collateral": "glasshouse",
"rating_hint": 0.8,
"sources": [
"hail_net_frame_01/net_anchor_01",
"hail_net_frame_01/net_anchor_02",
"hail_net_frame_01/net_anchor_03",
"hail_net_frame_01/net_anchor_04"
]
},
"house": {
"collateral": "gutter",
"rating_hint": 0.35,

View File

@ -100,10 +100,26 @@ export const PHASES = ['forecast', 'prep', 'storm', 'aftermath'];
* can't inspect the inside of). One rating for the whole type, per the
* manifest rule. The GLB wears the tell (rust, a sagging pad eye); the type
* string is the player's pre-rig read of the same fact.
*
* `hail_net_frame` (SPRINT18 gate 4, E): the frame that straddles a glasshouse
* so a hail net can be rigged OVER the glass (DESIGN.md line 179). It is the
* first anchor word in this enum whose danger is not in its own number.
* rating_hint 0.80 the highest rung below the honest post, and it EARNS it:
* an engineered portal frame on four real footings with welded eyes, the only
* structure in the palette that was built to be tied to. The 20% de-rate is
* for section, not condition (32 mm sized for hail mesh, which spills wind;
* a 25 impermeable sail is an order of magnitude more load).
* What makes it the most dangerous anchor in the game is that its baked
* `collateral` names the GLASSHOUSE, not itself the `house`"gutter"
* precedent, and the same shape: an anchor names what its failure TAKES, not
* what it is. Break a corner here and the sail, the shackle and the turnbuckle
* go through fourteen panes at $320. The type string is the player's pre-rig
* read of a hazard that is a POSITION, and the reason it must not be typed
* `post`: "post" promises a corner whose failure costs a shackle.
*/
export const ANCHOR_TYPE = Object.freeze([
'house', 'tree', 'post', 'carport', 'carport_post', 'swing_frame', 'pergola',
'corroded_post',
'corroded_post', 'hail_net_frame',
]);
// ---------------------------------------------------------------------------

View File

@ -113,6 +113,16 @@ const ASSETS = [
nodes: ['frame', 'panes', 'door', 'ridge_anchor'] },
{ name: 'glasshouse_01_wrecked', h: [2.15, 2.40],
nodes: ['frame', 'door', 'panes_left', 'shards'] },
// SPRINT18 gate 4 — the glasshouse tier's live half. The height floor is a
// design claim about the asset next to it: 2.70 sits ABOVE the glasshouse's
// 2.40 ceiling, because a net frame that does not clear the ridge is resting
// on the glass it exists to protect. The wreck shares the band — the +x
// portal stands, so a portal frame folds at one end rather than falling.
{ name: 'hail_net_frame_01', h: [2.70, 2.85],
nodes: ['footings', 'legs', 'top_frame', 'net_roll',
'net_anchor_01', 'net_anchor_02', 'net_anchor_03', 'net_anchor_04'] },
{ name: 'hail_net_frame_01_wrecked', h: [2.70, 2.85],
nodes: ['footings', 'legs', 'top_frame', 'net_down'] },
{ name: 'pool_kit_01', h: [1.12, 1.32],
nodes: ['pool_shell', 'pool_water', 'fence_rails', 'pool_gate',
'fence_post_01', 'fence_post_14'] },
@ -1436,5 +1446,313 @@ export default async function run(t) {
'vacuous: no node-less declared anchors found across the shipped sites — this pin checked nothing');
});
// -------------------------------------------------------------------------
// SPRINT18 gate 4 — THE GLASSHOUSE TIER. Sprint 16 shipped the glass and
// priced it at 320; the gate asked for the tier, and reading the gate found
// that the price could not be REACHED. The comment two hundred lines up says
// it in this file's own words: "nothing in the sim wrecks a glasshouse yet —
// the value is inert data". main.js bills off a broken rig corner's baked
// collateral string, and a glasshouse correctly offers no anchors, so the
// most expensive object in the game could not cost anybody a cent.
//
// DESIGN.md line 179 named the missing asset: hail nets rigged OVER glass.
// You tie to the frame that straddles it. The tier's teaching is new — the
// frame's numbers are GOOD, and it is the most dangerous anchor in the game
// because of where it stands. So the tell being pinned below is a POSITION,
// measured off two GLBs, not a defect painted on one.
// -------------------------------------------------------------------------
t.test('the hail-net frame BRACKETS the glass — the tell is a position, and it is measured', () => {
const frame = loaded.get('hail_net_frame_01');
const glass = loaded.get('glasshouse_01');
assert(frame && glass, 'hail_net_frame_01 or glasshouse_01 did not load');
const f = new THREE.Box3().setFromObject(frame.scene);
const g = new THREE.Box3().setFromObject(glass.scene);
// Both assets are built at the origin, so a shared-origin placement is the
// authored case and these comparisons are the real geometry.
assert(f.min.x < g.min.x && f.max.x > g.max.x,
`the frame spans x ${f.min.x.toFixed(2)}..${f.max.x.toFixed(2)} and the glasshouse `
+ `${g.min.x.toFixed(2)}..${g.max.x.toFixed(2)} — the frame must STRADDLE the glass on x. `
+ 'A frame that no longer brackets it is a frame whose anchors bill a building they are '
+ 'not standing over, and the $320 becomes a number with no geometry behind it');
assert(f.min.z < g.min.z && f.max.z > g.max.z,
`the frame spans z ${f.min.z.toFixed(2)}..${f.max.z.toFixed(2)} and the glasshouse `
+ `${g.min.z.toFixed(2)}..${g.max.z.toFixed(2)} — it must straddle on z as well; `
+ 'legs standing ON the glass would be the joke version of this asset');
assert(f.max.y > g.max.y + 0.35,
`the frame heads at y=${f.max.y.toFixed(2)} and the ridge is at ${g.max.y.toFixed(2)}`
+ 'a net frame built to protect glass must CLEAR the ridge with room for the net to '
+ 'drape, or it is resting on the thing it exists to keep hail off');
// Every eye is above the ridge: the load path arcs OVER the glass, which is
// the whole reason a failure here lands on panes.
for (const n of ['net_anchor_01', 'net_anchor_02', 'net_anchor_03', 'net_anchor_04']) {
const o = frame.scene.getObjectByName(n);
assert(o, `${n} missing`);
const p = new THREE.Vector3().setFromMatrixPosition(o.matrixWorld);
assert(p.y > g.max.y,
`${n} sits at y=${p.y.toFixed(2)}, below the ridge (${g.max.y.toFixed(2)}) — every corner `
+ 'of this frame must be above the glass or the tier is not about the glass');
}
});
t.test('the frame\'s 20 m read is METALNESS, not lightness — S17\'s lesson, applied on purpose', () => {
// S17 measured this the hard way on the corroded post: galvanised ships at
// metalness 0.90, and with no environment map a near-mirror has nothing to
// mirror and renders NEAR-BLACK (rgb 49,51,49 at 20 m). So every steel the
// player is allowed to trust is dark at yard distance. This asset's entire
// long-range tell is being the opposite of that, and the mechanism is
// metalness — pin the mechanism, or the docstring gets "fixed" the wrong
// way by whoever repaints it next.
const pick = (asset, node) => {
let m = null;
loaded.get(asset)?.scene.getObjectByName(node)?.traverse((o) => { if (o.material) m = o.material; });
return m;
};
const paint = pick('hail_net_frame_01', 'top_frame');
const gal = pick('sail_post', 'post');
assert(paint && gal, 'could not reach the frame paint or the honest post steel');
assert(paint.metalness <= 0.15,
`the frame paints at metalness ${paint.metalness} — powder-coat is PAINT, and above ~0.15 `
+ 'it starts mirroring an environment that does not exist and goes dark like every other '
+ 'steel. The tell dies silently: the frame is still white in the palette and black on screen');
assert(gal.metalness >= 0.6,
`the honest post's steel is at metalness ${gal.metalness} — the control for this comparison `
+ 'is that trusted steel IS a near-mirror; if that changed, re-measure the whole claim');
const lum = (c) => c.r * 0.2126 + c.g * 0.7152 + c.b * 0.0722;
assert(lum(paint.color) > lum(gal.color) * 1.3,
`frame paint luminance ${lum(paint.color).toFixed(3)} vs galvanised ${lum(gal.color).toFixed(3)}`
+ 'the powder-coat must also be the brighter base colour, so the two halves of the tell '
+ 'agree instead of fighting');
});
t.test('the hail-net frame is a NEW RUNG at 0.80: good steel, worst place, resolvable node-less', () => {
const T = FACTORY_ANCHOR_RATINGS.types;
const entry = T.hail_net_frame;
assert(entry, 'hail_net_frame is not in the ratings manifest — the type cannot resolve '
+ 'node-less, and a site declaring it plainly would play at the default hint (the '
+ 'sail_post bug shape, on the highest-collateral anchor in the game)');
assert(!FACTORY_ANCHOR_RATINGS.ambiguous.hail_net_frame,
'hail_net_frame went manifest-AMBIGUOUS — its four eyes disagree, and the runtime will '
+ 'refuse to guess; they are one frame and must bake one number');
// Pinned against the manifest, never a literal — a literal would stay green
// through a whole reshuffle of the ladder.
assert(entry.rating_hint > T.pergola.rating_hint,
`the frame rates ${entry.rating_hint} and the pergola ${T.pergola.rating_hint} — the frame `
+ 'must out-rate it by a distance: a deck screwed to a house flexes, this is an engineered '
+ 'portal on four footings. De-rating honest steel for looking dangerous is the corroded '
+ 'post\'s lie inverted');
assert(entry.rating_hint < T.post.rating_hint,
`the frame rates ${entry.rating_hint} against the honest post's ${T.post.rating_hint} — at or `
+ 'above the post there is no de-rate left for "32 mm sized for mesh, not for 25 m² of sail"');
// THE FIELD THAT MAKES IT A TIER: what it takes with it is the GLASS.
assert(entry.collateral === 'glasshouse',
`the manifest says this type bills ${JSON.stringify(entry.collateral)} — it must bill `
+ '"glasshouse". That string is the entire tier: the anchor is fine, the thing underneath '
+ 'it is $320, and a node-less declaration has to carry that or the yard is free to break');
assert(entry.sources.length === 4,
`the manifest cites ${entry.sources.length} eyes; the frame has four corners and a net `
+ 'ties at all of them — a missing source means a corner baked something different');
});
// ⚠️ THE GATE, AS ONE MEASUREMENT. Before this sprint this assert was RED by
// construction: no node anywhere in the palette named the glasshouse's key,
// so the collateral apex was unbillable and the 320 was decoration. It is the
// free-failure bug's mirror image — RULE 2 catches a key with no price, and
// this catches a price with no key that can reach it.
t.test('⚠️ the collateral APEX is reachable: some anchor in the palette can actually bill the glass', () => {
const g = loaded.get('glasshouse_01')?.scene.getObjectByName('glasshouse_01');
assert(g, 'glasshouse_01 root missing');
const key = g.userData?.collateral_key;
assert(typeof key === 'string', 'the glasshouse carries no collateral_key to reach');
const billers = [];
for (const [asset, o] of allNodes()) {
const u = o.userData ?? {};
if (u.collateral !== key) continue;
if (typeof u.rating_hint !== 'number' || u.tie_off === false) continue;
billers.push(`${asset}/${o.name}`);
}
assert(billers.length > 0,
`NOTHING in the palette can bill "${key}" ($${g.userData?.collateral_value}). The apex is `
+ 'priced and unreachable: main.js bills collateral off a BROKEN RIG CORNER\'s baked key, '
+ 'a glasshouse rightly offers no anchors, so the most expensive thing in the game costs '
+ 'nobody anything. It needs an adoptable anchor whose collateral names the glass — that '
+ 'is what the hail-net frame is for (SPRINT18 gate 4)');
// ...and the corollary that keeps gate 5 disarmed on my own asset: the
// frame POINTS at the key and must never CLAIM it. structFor() returns the
// FIRST structure matching a key, so a frame carrying collateral_key
// "glasshouse" would, in any yard authored frame-first, bill $320 for the
// panes and then wreck THE FRAME — the bill and the wreck agreeing with
// each other and both wrong, which is the only kind of wrong this repo
// cannot see from the outside.
const claimants = [];
for (const [asset, gltf] of loaded) {
const root = gltf.scene.getObjectByName(asset);
if (root?.userData?.collateral_key === key) claimants.push(asset);
}
for (const c of claimants) {
assert(c.startsWith('glasshouse_01'),
`${c} claims collateral_key "${key}", and it is not the glasshouse. Exactly one structure `
+ 'may OWN a key — the one that owns the price. structFor() takes the first match, so a '
+ 'second claimant means some yard bills the glass and wrecks something else');
}
assert(claimants.length > 0, `vacuous: nothing owns "${key}" at all`);
});
t.test('the net-frame wreck folds at ONE end: keeps its height, carries no glass, offers nothing', () => {
const w = loaded.get('hail_net_frame_01_wrecked');
const i = loaded.get('hail_net_frame_01');
assert(w && i, 'the net frame or its wreck did not load');
const bw = new THREE.Box3().setFromObject(w.scene);
const bi = new THREE.Box3().setFromObject(i.scene);
assert(Math.abs(bw.max.y - bi.max.y) < 0.10,
`the wreck tops out at y=${bw.max.y.toFixed(2)} vs the intact ${bi.max.y.toFixed(2)} — the +x `
+ 'portal stands and holds the eave rails, so a portal frame folds at one end. A wreck lying '
+ 'flat is claiming a failure mode this thing does not have');
assert((bw.max.x - bw.min.x) > (bi.max.x - bi.min.x) + 0.6,
`the wreck spans ${(bw.max.x - bw.min.x).toFixed(2)} m on x against the intact `
+ `${(bi.max.x - bi.min.x).toFixed(2)} — the net has to end up OUT past the footing, or your `
+ 'own debris is tidier than the storm\'s and the tier stops teaching what it is for');
// No glass here. The glasshouse has its own wreck variant and swaps
// independently; shards in both would draw twice the night both swap, and
// would quietly claim the glass's failure as this asset's.
const glassy = [];
w.scene.traverse((o) => {
const m = o.material;
if (m && m.transparent === true && m.opacity < 0.6) glassy.push(o.name);
});
assert(glassy.length === 0,
`the net-frame wreck ships translucent geometry on [${glassy.join(', ')}] — the glass belongs `
+ 'to glasshouse_01_wrecked, which swaps on its own; shards in both double-draw');
// No anchor survives the wreck (the fascia rule, sixth application).
const offers = [];
w.scene.traverse((o) => {
if (typeof o.userData?.rating_hint === 'number' || o.userData?.anchor_type !== undefined) {
offers.push(o.name);
}
});
assert(offers.length === 0,
`the wreck still offers [${offers.join(', ')}] — you cannot clip to a folded frame, and an `
+ 'anchor that outlives its structure is the free-failure bug in a costume');
const wr = w.scene.getObjectByName('hail_net_frame_01_wrecked');
assert(wr?.userData?.broken_variant_of === 'hail_net_frame_01',
'the wreck must name its intact twin so A can pair the swap');
// UNPRICED BY RULING — the bike/jacaranda pattern. One broken corner can
// only name one collateral key, and the player is being taught about the
// glass. Splitting a bill between the frame and the panes with no mechanism
// to split it is exactly the guess the invoice exists to refuse.
assert(wr?.userData?.collateral_value === undefined,
'the net frame is UNPRICED BY RULING — build the mechanism that can bill two things off '
+ 'one broken corner before pricing the second one');
});
// -------------------------------------------------------------------------
// SPRINT18 gate 5 — THE SECOND-RUST-POST LANDMINE (D's filing, E+A).
//
// Two structures in one yard sharing a collateral key is currently ARMED and
// not firing: world.js's structFor() returns the FIRST structure whose key
// matches, main.js's billing loop dedupes by key, and C's scorecard envelope
// dedupes by key again — so the second one is invisible to the bill and to
// the wreck, while board.js's exposureOf() counts per STRUCTURE and prices
// both. D measured the split on a real two-post yard: $115 on the offer card
// against $45 on the failure route.
//
// The pool yard dodges it by shipping exactly one corroded post ON PURPOSE.
// That is a yard author remembering, which is not a defence — and gate 4 just
// made it likelier, because a glasshouse yard carries a structure that OWNS
// the key and a frame whose anchors POINT at it. Two glasshouses with a frame
// each trips this on the first night.
//
// So: the asset/manifest half is to make it impossible to SHIP quietly. The
// runtime half (a per-instance key a site can override) is A's and the exact
// wiring is filed in THREADS.
// -------------------------------------------------------------------------
const shippedSites = [];
for (const name of ['backyard_01', 'site_02_corner_block', 'site_03_swing_lawn',
'site_04_pool_yard']) {
shippedSites.push({ name, site: await loadSite(name) });
}
t.test('⚠️ no shipped yard gives two structures the same collateral key, and ids are unique', () => {
// Mirrors world.js structKey() exactly: site JSON wins, then the baked
// GLB key, then the structure's own id as a last resort.
const resolvedKey = (s) => {
if (s.collateralKey) return s.collateralKey;
const base = String(s.model ?? '').replace(/_v1$/, '');
return loaded.get(base)?.scene.getObjectByName(base)?.userData?.collateral_key ?? s.id;
};
let checked = 0;
for (const { name, site } of shippedSites) {
const structs = site.structures ?? [];
const ids = structs.map((s) => s.id);
const dupeIds = ids.filter((v, i) => ids.indexOf(v) !== i);
assert(dupeIds.length === 0,
`${name}: duplicate structure ids [${[...new Set(dupeIds)].join(', ')}]. world.js keys its `
+ 'structures Map by id, so the second one SILENTLY REPLACES the first — it never renders, '
+ 'never bills, and never wrecks, and nothing anywhere says a word');
const keys = structs.map(resolvedKey);
const dupeKeys = keys.filter((v, i) => keys.indexOf(v) !== i);
assert(dupeKeys.length === 0,
`${name}: two structures resolve to the same collateral key `
+ `"${[...new Set(dupeKeys)].join('", "')}" (keys: [${keys.join(', ')}]). structFor() `
+ 'returns the FIRST match, so the bill and the wreck would both land on the wrong one, '
+ 'agreeing with each other; meanwhile exposureOf() prices per structure and the offer '
+ 'card would quote a total the failure route can never reach (D measured $115 vs $45). '
+ 'Give each instance its own key in the site JSON — and until A lands the per-anchor '
+ 'override, the anchors bake the shared key, so the second one cannot be made to bill '
+ 'at all: see THREADS [E] SPRINT18 gate 5');
checked += keys.length;
}
assert(checked > 0, 'vacuous: no structures found across the shipped yards');
});
// The other half of the same seam, from the asset side: an anchor names the
// collateral it TAKES, and in a real yard something has to own that name. The
// hail-net frame is the first asset where those are different objects, so a
// frame placed in a yard with no glasshouse would bill a building that is not
// there — collateralFor() returns null and the corner is FREE. RULE 2 proves
// the key is priced SOMEWHERE in the palette; this proves it is owned in
// every yard that names it.
t.test('⚠️ every collateral an authored anchor names is OWNED by exactly one thing in that yard', () => {
const types = FACTORY_ANCHOR_RATINGS.types;
let checked = 0;
for (const { name, site } of shippedSites) {
const owners = new Map();
const own = (key, what) => {
if (typeof key !== 'string') return;
owners.set(key, (owners.get(key) ?? 0) + 1);
void what;
};
const houseBase = String(site.house?.model ?? '').replace(/_v1$/, '');
own(site.house?.collateralKey
?? loaded.get(houseBase)?.scene.getObjectByName(houseBase)?.userData?.collateral_key,
'house');
for (const s of site.structures ?? []) {
const base = String(s.model ?? '').replace(/_v1$/, '');
own(s.collateralKey
?? loaded.get(base)?.scene.getObjectByName(base)?.userData?.collateral_key ?? s.id, s.id);
}
// What every authored anchor on a structure will actually bill: the baked
// node's key when it names a node, else the manifest's key for its type.
for (const s of site.structures ?? []) {
const base = String(s.model ?? '').replace(/_v1$/, '');
for (const a of s.anchors ?? []) {
const baked = a.node
? loaded.get(base)?.scene.getObjectByName(a.node)?.userData?.collateral
: types[a.type]?.collateral;
if (typeof baked !== 'string') continue; // bills nothing, by design
assert(owners.get(baked) === 1,
`${name}/${a.id} will bill "${baked}" on failure, and ${owners.get(baked) ?? 0} things `
+ 'in this yard own that key. Zero means collateralFor() returns null and breaking '
+ 'this corner is FREE (the gutter bug, RULE 2\'s runtime twin); more than one means '
+ 'structFor() picks the first and the wreck lands on the wrong structure');
checked += 1;
}
}
}
assert(checked > 0,
'vacuous: no structure anchor in any shipped yard names collateral — this pin checked nothing');
});
for (const { world } of siteWorlds) world.dispose();
}

Binary file not shown.

Binary file not shown.