Wire the kid's bike: site key + loader, gnome pattern; decline the $60

Gate 3.2: bike_kid_01_v1 on backyard_01's south fence, rotYDeg 180 so the
baked -Z lean lands ON the fence — bars measured to the rail (0.286 m
reach, z 9.69 -> grip at 9.98), tyres 0.31 m clear, between posts. Unpriced
by ruling: the sim can't knock a bike over, and billing an unseen event is
the lie the invoice kills; the windward-fence pose is the honest one.
a.test pins lean direction, standoff band, and the ruling. 318/0/0.
This commit is contained in:
type-two 2026-07-17 20:34:47 +10:00
parent 3d89ce8fe8
commit 807de80969
4 changed files with 98 additions and 1 deletions

View File

@ -4009,3 +4009,32 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
arithmetic, neg class) — because both of last sprint's card bugs were invisible to a suite that
never rendered a card. hud.dispose() also stops leaking the dawn div. selftest **317/0/0**
(315 + the two card renders).
[A] 2026-07-17 — 🚲 **The bike is WIRED (SPRINT12 gate 3.2), and the $60 is ruled: DECLINED, for E's own
reason.** site-JSON key + loader line, the gnome pattern exactly — `bike` is a named top-level key
in backyard_01, world.js loads it in the same Promise.all and drops it at `heightAt`. Not in
solids, not billed.
**Placement is measured, not reasoned** (the numbers live in the site JSON's `_why`): I parsed the
GLB in node before placing anything — the bars' grip reaches 0.286 m past the tyre-contact line
toward the baked lean, so at rotYDeg 180 (local Z → global +Z, per E's exporter note, read FIRST
as instructed) and z = 9.69, the grip lands on the south fence rail's inner face at 9.98 and the
tyres stand 0.31 m clear. x = 7 sits between fence posts so bars meet rail, not post. Verified by
LOOKING: walked the player over in the browser — it reads as a kid's bike leaning on the palings,
step-through diagonal doing the work E said it would. New a.test pins the placement side of E's
axis trap: rotYDeg must point the baked lean at the fence (dot > 0.95) and the standoff must stay
in the bars-touch band — drag it mid-yard or flip the rotation and the suite reddens with the
right sentence.
**RULING — no collateralValue, and an assert enforces the reasoning.** E's framing decides it:
"a bike that ignores that gust is a worse lie than no bike." Priced, the lie gets WORSE — the sim
cannot knock a bike over, so the $60 would bill for an event the player never sees, which is the
exact species of unearned consequence the invoice exists to kill (the carport bills because the
roof visibly leaves). The placement is the mitigation: leaning on the WINDWARD fence is the one
pose a southerly can't falsify — the gust pins it harder into the palings, so standing still is
physically honest in every storm the backyard hosts. The tripwire assert fails if anyone prices
the bike without building the fall, and says so: build the fall first, then bring the $60 back —
it's the right number (under the gnome-to-carport span, and the one loss the client's kid would
notice).
selftest **318/0/0** (317 + the placement pin). Landing pushed.

View File

@ -87,6 +87,24 @@
"gnome": { "model": "garden_gnome_01_v1", "x": 4.3, "z": 4.4, "rotYDeg": -126, "collateralValue": 25 },
"bike": {
"model": "bike_kid_01_v1", "x": -7, "z": 9.69, "rotYDeg": 180,
"_why": [
"The Hendersons' kid's bike, against the south fence. THE LEAN IS BAKED INTO THE GLB and it",
"leans toward local -Z (E's THREADS note; e.test pins it) - a bike doesn't stand up on its own.",
"rotYDeg 180 turns that lean into global +Z, INTO the fence. Any placement that doesn't point",
"the lean at something to lean ON reads as a physics bug and isn't one.",
"z is MEASURED, not reasoned: the bars' grip reaches 0.286 m past the origin (the tyre contact",
"line) toward the lean, and the fence rails sit at z = 10 +/- 0.02, so 9.69 + 0.286 = 9.98 =",
"the rail's inner face - bars touch the fence, tyres stand 0.31 m clear. x = -7 sits between",
"fence posts (-7.5 and -5), so the grip meets rail, not post.",
"NO collateralValue, BY RULING (THREADS [A] sprint 12): the sim cannot knock a bike over, and",
"billing $60 for an event the player never sees is the kind of lie the invoice exists to kill.",
"Leaning on the windward fence is the one pose a southerly can't falsify - the gust pins it",
"harder. Price it the day it can fall."
]
},
"fence": {
"sides": ["south", "west", "east"]
},

View File

@ -342,6 +342,38 @@ export default async function run(t) {
} finally { hud.dispose(); }
});
t.test("SPRINT12: the kid's bike leans INTO the south fence, not into open air", () => {
// E's axis trap, pinned from the PLACEMENT side. e.test proves the GLB
// leans toward local Z; nothing until now proved the site points that lean
// at anything. Un-rotate the bike or drag it mid-yard and this reddens —
// the failure mode is a bike leaning on air, which reads as a physics bug
// and would get chased in the wrong lane (E's words, their lean note).
const b = site.bike;
assert(b, 'backyard_01 places the bike');
assertEq(b.model, 'bike_kid_01_v1', "E's model, the v1 with the baked lean");
// Local Z rotated by rotY must land on +Z — the south fence side.
const ry = (b.rotYDeg * Math.PI) / 180;
const leanTowardFence = -Math.cos(ry); // z of (0,0,1) rotated about Y
assert(leanTowardFence > 0.95,
`rotYDeg ${b.rotYDeg} points the baked lean at z=${leanTowardFence.toFixed(2)} — not the fence`);
// The standoff, measured: bars reach 0.286 m past the tyre line toward the
// lean; the fence rails sit at z = depth/2 ± 0.02. The band accepts
// bars-on-rail ± a hand's width, and rejects mid-yard or tyres-in-fence.
const gap = site.yard.depth / 2 - b.z;
assert(gap > 0.15 && gap < 0.45,
`bike stands ${gap.toFixed(2)} m off the fence line — bars touch at ~0.31`);
// RULING (SPRINT12 gate 3.2): E's $60 is DECLINED until the sim can knock a
// bike over. Billing collateral for an event the player never sees is the
// lie the invoice exists to kill; leaning on the windward fence is the one
// pose a southerly can't falsify. This assert is the ruling's tripwire: if
// someone prices the bike, they must also build the fall, and this message
// is where they find that out.
assert(b.collateralValue == null, 'the bike is unpriced BY RULING — build the fall first');
});
// --- SPRINT11 gate 1: the three rulings, each pinned ----------------------
t.test('ruling: the carport is typed as a carport, not smuggled in as a post', () => {

View File

@ -200,6 +200,9 @@ export function createWorld(scene, opts = {}) {
const SHED = site.shed;
const SHED_TABLE = site.shedTable ? { ...site.shedTable, rotY: rad(site.shedTable.rotYDeg) } : null;
const GNOME = site.gnome ? { ...site.gnome, rotY: rad(site.gnome.rotYDeg) } : null;
// SPRINT12: the per-client prop, the gnome pattern — a named top-level key,
// not a generic props[]. See the site JSON's _why for the lean/fence math.
const BIKE = site.bike ? { ...site.bike, rotY: rad(site.bike.rotYDeg) } : null;
const treeSpecs = site.trees ?? [];
const postSpecs = site.posts ?? [];
@ -614,9 +617,10 @@ export function createWorld(scene, opts = {}) {
// Model names come off the site now. `load(null)` resolves null, so a site
// that simply has no shed (site_02 has a carport) skips it without a branch.
const [shed, table, houseGlb, bedGlb, gnome] = await Promise.all([
const [shed, table, houseGlb, bedGlb, gnome, bike] = await Promise.all([
load(SHED?.model), load(SHED_TABLE?.model), load(HOUSE?.model),
load(site.gardenBedModel ?? 'garden_bed_v1'), load(GNOME?.model),
load(BIKE?.model),
]);
const treeGlbs = await Promise.all(treeSpecs.map((s) => load(s.model)));
@ -642,6 +646,20 @@ export function createWorld(scene, opts = {}) {
root.add(gnome);
}
// --- the kid's bike (per-client juice, SPRINT12) ----------------------
// The lean is baked into the GLB toward local Z (E's export note — in
// Blender it leans +Y; the exporter maps (x,y,z)→(x,z,y)), so placement's
// ONE job is to point that lean at the fence. rotY comes from site data;
// the site's _why records the measured bars-to-rail arithmetic. Not in
// solids — you can walk past a kid's bike — and not billed: unpriced by
// ruling until wind can knock it over.
if (bike) {
bike.name = 'bike_kid_01';
bike.position.set(BIKE.x, heightAt(BIKE.x, BIKE.z), BIKE.z);
bike.rotation.y = BIKE.rotY;
root.add(bike);
}
// --- house (decision 6: no re-cut, the GLB's data wins) ---------------
// E's fascia sits at 2.80 m and their anchors span x=-3..3, where my
// graybox guessed 2.6 m and -5..5. Reading them narrows the house span by