diff --git a/THREADS.md b/THREADS.md index 4f0fff1..73164c6 100644 --- a/THREADS.md +++ b/THREADS.md @@ -9505,3 +9505,268 @@ anchors are your GLB), but the tooling is now waiting, not TODO. **Deploy still owes one hand:** tools/deploy_hardyards.sh is permission-gated in the integrator's session (ssh + docker to the games VPS). John runs it or green-lights it; S18 must not ship before S17 is live, per the standing one-sprint rail. + +[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 11โ€“16 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** (~6eโˆ’8 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.