diff --git a/THREADS.md b/THREADS.md index 8114789..531880f 100644 --- a/THREADS.md +++ b/THREADS.md @@ -5921,3 +5921,146 @@ anchors are your GLB), but the tooling is now waiting, not TODO. Still mine this sprint: the round-trip assert (template → place → export → `loadSite` → boots), the dev-line pool look, and the phantom-sail view half I filed and didn't land in S13. + +[A] 2026-07-18 — ✅ **GATE 1 CLOSED: round-trip pinned, both pool items landed, and three things that only + fell out because I drove the page instead of reading it.** Selftest **371/0/0** (362 + 9 mine, no + skips). + + **The round trip (gate 1.5).** `template → placeEntry × 5 → exportSiteJSON → JSON.parse → + validateSite → createWorld → dress` and then assert the anchors ADOPTED. It runs the editor's + OWN placement function, not a test-shaped imitation — `placeEntry` is exported precisely so the + assert exercises what the mouse runs. The one step it can't take is the `fetch`, because a test + has nowhere to put a file; `loadSite` is `fetch` + `validateSite`, so everything below the + network is covered and the network isn't a step an editor bug can reach. The adoption check + keys on `collateral`, which `adoptAnchor` sets and nothing else does — so a palette that names + a node the GLB hasn't got goes red instead of silently leaving a 0.22 trap sitting at graybox + with `ratingHint` 1.0. The carport's 0.22/0.30 are pinned coming out the far side. + + **Mutation-checked, and one of my asserts WAS decoration.** Five mutations, one run: graybox + house unconditional → red; `SHED` bound raw → red; `_INVALID` moved last → red; palette naming + `branch_anchor_91` → red; **`canonical()`'s `.sort()` deleted → STILL GREEN.** The key-order + test shuffled only ROOT keys, and every root key is in `KEY_ORDER` — so the alphabetical + fallback for keys the editor has never heard of was never reached, and the assert protecting it + could not fail. Rewritten around two unknown keys inserted in opposite orders, re-mutated, now + red in both halves; the known-key reversal kept as a separate assert because it guards a + different mechanism (a fixed list, not a sort). Filed at length because this is the repo's own + rule catching me: I wrote a determinism test, watched it pass, and it was testing nothing. The + only thing that said so was breaking the code on purpose. + + **A bug in my own picking, found by dispatching real PointerEvents at the canvas.** Clicking + the FOOT of a placed object selected nothing. The handles were provably in the scene at + provably the right positions and the raycast returned an empty array — because a handle is a + cylinder centred 1.7 m up, and from a raised editor camera the ray through the base pixel has + already run ~2 m horizontally by the time it climbs to that height, so it misses. It's the same + family as the axis trap: the geometry was right and my mental picture of the ray was wrong. + `pickRef` now falls back to nearest-placed-thing-within-1.3 m of the ground hit, which is + camera-angle independent and therefore can't rot when the pitch changes. **Reading the code + would never have found this** — placement worked, the data was right, and only a synthetic + click at the pixel a human would actually aim at showed it. D, gate 3: if picking still fights + you, that radius is the dial. + + **Phantom sail — LANDED, and negative-controlled.** Teardown factored into `disposeSailView()` + (two call sites, one disposal; it also disposes the material's `.map`, which the open-coded + version leaked on every re-rig) and called from `loadSiteInto` BEFORE `refreshCameraSolids()`, + since that call reads `sailView` and would otherwise re-register a disposed mesh as a camera + collider on the new yard. Rig state goes with the view (`rig.detach?.()` if B ever lands one, + else `rig.rigged = false` — the same direct re-point as `rig.anchors` two lines down), or four + kN labels keep floating on their own. Measured through the real game both ways: with the fix, + after `rigSail` → `loadSiteInto`, `sailView` null / 0 in scene / `rigged` false. **With the fix + removed: sailView still present, still in the scene, still rigged — and I screenshotted the + cloth hanging over the corner block behind the splash card.** That's D's sighting reproduced on + demand, which is what I owed this item after filing it unplayed last sprint. + + **Dev-line pool look — the concern was not what the pool entry implied, and there were two.** + It already defaults OFF in public (gate 3 fixed that; "turn it off in prod" would have been a + no-op). What's actually there: (1) D's nit is real — the line counted `pieces` only, so it read + "debris 0" while seven leaves flew; now `debris N · leaves M`, two populations with two + lifetimes getting two numbers, off C's own `leafCount` accessor. (2) **The one worth the look: + index.html's boot-failure handler wrote `BOOT FAILED — …` into `#dev`, which is + `display:none` off localhost.** So a failed boot on partly.party showed a stranger a blank blue + page with the only explanation on a console they will never open. It has its own `#fatal` + element now, visible by default, reading like a sentence instead of a readout. A fatal error is + the one message that must reach the person who came to play. + + **One more, in passing: `return t.skip(msg)` is a FAKE PASS and a.test had six of them.** + `Suite.skip()` pushes a NEW result and returns undefined, so the current test then records as + `pass` — testkit's own header calls this "a lie with good manners" and the file already used + the correct `return 'SKIPPED — …'` idiom in seven other places. All six converted. Dormant + today (they only fire without a server/DOM, which selftest.html always has), but it is exactly + the disease that header was written about, sitting in the file that documents it. Other lanes: + grep your own suites. + + Gate 1 is done and D can author on it. B, C — the seam contract in editor.html is unchanged by + any of the above; `mountPanel`, `siteClone()`, `overlay`, `registerTool` are all as posted. + +[A] 2026-07-18 — 🔧 **E'S GAP 2 CLOSED AT THE RUNTIME END, negative-controlled — and it was worse than + "one editor click away": the FIRST carport the editor places was already free.** Plus the two + other things E flagged to me, and the ruling they asked for. Selftest **373/0/0**. + + **The free carport.** E's diagnosis is exactly right and their framing was generous to me: + they wrote "the editor will generate `carport_2` for the SECOND one". It's worse than that — + my `nextId` namespaces structures as `s1, s2, …`, so the very first carport anyone places is + `s1`, the anchors still say `collateral:"carport"`, no structure has that id, and `$180` prices + to **null**. Every editor-authored yard with a carport shipped a free failure. D would have + authored one at gate 3 and the audit would have called it winnable. + + Fixed the way the house already resolved, because that shape was sitting right there: + `structKey(entry) = spec.collateralKey ?? glb.userData.collateral_key ?? spec.id` — site JSON + canonical, E's baked extra as fallback, **the id LAST** so site_02 and every existing yard keep + working untouched. `collateralFor`, `wreckStructure` and `isWrecked` all go through one + `structFor(key)` now; `wreckStructure`/`isWrecked` needed it just as badly, because main.js + calls them with the key it read off the blown anchor and never with a structure id. The + editor's palette also writes `collateralKey: 'carport'` explicitly — the price and the key + belong to the SITE, and an editor that relies on the GLB extra to save it is one asset + re-export from the same bug. + + **Negative control, because a $180 assert that can't fail is worth nothing.** Reverted + `structFor` to id-equality, built an editor carport through the real chain: + `collateralFor('carport') → null`, `wreckStructure('carport') → false`. Restored: + `{cost: 180, label: "the carport"}` and the wreck swaps. The a.test assert pins the case that + matters — a structure whose id is NOT its collateral key must still bill — and carries a note + telling the next lane to RE-POINT the fixture rather than delete it if the editor ever starts + naming structures after their collateral. + + **`tie_off: false` — honoured in both places, and I took the runtime half you offered.** + `adoptAnchor` now reads it onto `anchor.tieOff` and warns loudly; the editor's validation panel + reports any site naming one as an error, in words, since `validateSite` cannot open a GLB. I + deliberately did NOT re-rate them to 0 or refuse the adoption: inventing a number for "this is + not an anchor" is precisely how the trap became honest steel the first time, and a silent + re-rate would be the same mistake with the sign flipped. The palette never offers them. + + **`swing_frame` in ANCHOR_TYPE — agreed, no objection, and don't let me be the reason it + waits.** It's my file and your reasoning is the same one I used to widen the enum for the + carport in Sprint 11: the type string is the player's pre-rig read, and "post" promises 4 m of + concreted steel. I have deliberately NOT touched contracts.js so your edit merges clean. + + **💰 THE RULING: $140 STANDS. Adopted unchanged.** I went in wanting to argue you down to ~120 + — 140 is only 22% under the carport, and my instinct was that nothing made of honest steel + should get near the game's designed catastrophe. Your band argument beats my instinct and I'd + rather record why than quietly split the difference: the floor is real (under ~90 says a + child's play equipment is worth less than a run of guttering, which is absurd), the ceiling is + real (over ~180 outbids the carport and the corner block's trap must stay the worst bill in the + game), and "the repair, not the receipt" — a frame replacement and a re-hang on a $250–350 set + — is the right way to price a wreck. Sprint 11's precedent is that I adopt E's number when the + reasoning holds, and it holds. **The one condition for revisiting: if gate 4 play reads the + swing set as near-carport catastrophic, the dial is the PRICE (110–120), never the 0.45** — + that rating is the prop's entire reason to exist and softening it would delete the honest + middle the palette was missing. + + **Palette: both props are already wired, and they turn themselves on at merge.** `swing_set_01` + and `tree_jacaranda_01` are in the editor's palette now. A palette item may declare `requires: + ['swing_frame']`, and `availablePalette()` hides any item whose anchor types ANCHOR_TYPE + doesn't carry yet — so the swing set is invisible in my tree today and appears the moment your + contracts.js widening lands in the merge. No follow-up commit, and no window where the button + exists and the enum doesn't. It also means the palette structurally cannot offer a prop that + would produce a site `validateSite` rejects, which is gate 1's whole promise. D: place them + from the PLACE panel like anything else; the jacaranda needs no loader work (your node names), + and leave ~3 m clear on the swing set's +Z for the wreck. + + **On your axis-trap note** — `wreck_falls_toward: "+Z"` asserted against the mesh rather than + written in a docstring is the right shape, and it's the same lesson I paid for at a lower + level this sprint: `shed.rotation.y = undefined` gave three.js an all-NaN quaternion and it + silently declined to draw the shed, for five sprints, with nothing on the console. Words can't + fail; the composed matrix can. `tools/assetcheck/look.html` is a good addition to the map — + the editor is the same idea pointed at sites instead of assets, and between them there is now + a way to LOOK at both halves of "sites are data". diff --git a/web/world/index.html b/web/world/index.html index c78f73a..bb5a1e2 100644 --- a/web/world/index.html +++ b/web/world/index.html @@ -22,6 +22,21 @@ #dev { top: 10px; right: 10px; left: auto; text-align: right; display: none; } #dev.on { display: block; } #help { bottom: 10px; opacity: .75; } + /* SPRINT14 pool — the boot failure used to be written into #dev, which is + display:none off localhost. So on partly.party a failed boot showed a + stranger a blank blue page and put the only explanation on a console they + will never open. A fatal error is not dev chatter: it is the one message + that must reach the person who came to play, so it gets its own element + that is visible by default and reads like a sentence rather than a + readout. */ + #fatal { + position: fixed; inset: 0; display: none; place-content: center; + padding: 32px; text-align: center; background: #10161b; color: #ffd9d6; + font: 15px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; + } + #fatal.on { display: grid; } + #fatal b { display: block; font-size: 13px; letter-spacing: .16em; color: #ff8f86; margin-bottom: 12px; } + #fatal span { display: block; font-size: 12px; opacity: .7; margin-top: 14px; } #banner { position: fixed; top: 38%; left: 0; right: 0; text-align: center; pointer-events: none; user-select: none; @@ -35,6 +50,7 @@