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:
parent
3d89ce8fe8
commit
807de80969
29
THREADS.md
29
THREADS.md
@ -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.
|
||||
|
||||
@ -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"]
|
||||
},
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user