Two band-honesty bugs found by the M14a physics fuzz (wedged under both control laws): (1) multi-cell slope parts claimed j>0 cells at the already-dropped pose — [y-delta, y+0.99] sits a delta below the piece that actually descends through them; the unclaimed upper half of a slant-long-d walled a channel legally crossing above. Every part cell now claims the part's full envelope (commitSlope + island slopes). (2) The realized climb cubic spills past its slope window; the next cell's upper-terrace mass poked above the still-climbing floor and walled the run. Terrace low-base sharing now extends one cell past a climb / one before a descent lip, and the slope picker sits one cell further from the shoreline to keep the 2-flat-cell contract. Streams reshuffle: re-pinned diamond->wide-scan-1, wide->wide-scan-7, gravity->grav-scan-2. Gates: 112 tests green, 90-seed x 8-segment fuzz 0 falls 0 wedges (was 2 wedges), goldens/genVersion untouched (finite pipeline unchanged).
- commit
- 78a530f4b9e170f51627676f9b6d03cdbb4105e5
- parent
- 2a22e08c7d02a04342eda9b8e8a825273e4b6a90
- author
- Dami <dami.etoile@gmail.com>
| M | progress.md | 33 | ++++++++++++-------- |
| M | src/ | 49 | ++++++++++++++++++++++++------ |
| M | tests/ | 24 | +++++++------- |
@@ -1147,19 +1147,26 @@ multiplier on the traction budget (same knob, now principled). Gates: 7
11471147 survivability seeds, all named scenarios, deep fuzz, determinism hash,11481148 re-pin scenario table.11491149 1150-**KNOWN GENERATION BUGS (M14a fuzz finds, pre-existing — wedge under BOTH1151-laws, rare 2/90 seeds, netted by respawn escalation; fix = a generation1152-round, it reshuffles every stream):** (a) apfuzz-81 s=41.9: seg2's1153-slant-long-d chute under-crosses seg0's channel with Δ0.55tr — its ribbon1154-wall tops (floor+0.55tr) form a face at exactly seg0's floor level, fully1155-blocking the channel (band overlap in cell (19,0): steep-part cells claim1156-bands that under-represent wall geometry). (b) apfuzz-58 s=330.3: an1157-island-run slope window climbs into island mass boxes (terrace band leak1158-on the slope part). Both diagnosed by scratch tri-scan-all.ts (point→1159-triangle distance over every co-live proxy — the "invisible wall" finder;1160-recreate from this note when needed). Marble telemetry: a one-frame1161-velocity REVERSAL at ratio ≈ restitution means a static face head-on;1162-constant-force zero-velocity equilibrium means normal through the center.1150+**GENERATION BAND BUGS — FOUND BY M14a FUZZ, FIXED (user: "include bug1151+fix for fuzz findings"):** (a) apfuzz-81 s=41.9: multi-cell slope parts1152+put the whole delta on cell 0's stepAfter, so j>0 cells claimed1153+[poseY−delta, poseY+0.99] a full delta BELOW their real geometry — a1154+slant-long-d's unclaimed upper half walled a channel legally crossing1155+above (one-frame velocity reversal at ratio ≈ restitution = static face1156+head-on; constant-force zero-velocity equilibrium = contact normal1157+through the center). FIX: every part cell claims the part's FULL envelope1158+(commitSlope + island slope steps — j>0 bands flip sign). (b) apfuzz-581159+s=330.3: the realized climb cubic SPILLS past the slope window (rail1160+still ty 0.45 inside the following "straight" window), so the upper-1161+terrace mass poked 0.6 world above the still-climbing floor. FIX: the1162+terrace low-base sharing extends one cell past a climb / one before a1163+descent lip (path.ts partLow spillover), and the slope picker moves one1164+cell inward on that side to keep the ≥2 flat shoreline contract.1165+Diagnosis tool: scratch tri-scan-all.ts (point→triangle distance over1166+every co-live proxy — the "invisible wall" finder; recreate from this1167+note). Gates: 112 tests green, 90-seed × 8-segment physics fuzz 0 falls1168+0 wedges. Streams reshuffled → pins moved: diamond→wide-scan-1,1169+wide→wide-scan-7, gravity→grav-scan-2 (others held).11631170 11641171 **M14b — Jumping as a decision.** Shim ph_marble_jump (top-up: velocity11651172 along normal SET to jumpImpulse, never added — deterministic height);@@ -482,8 +482,16 @@ class SegmentBuilder {
482482 for (const part of parts) {483483 const signed = climb ? part.delta : -part.delta;484484 for (let j = 0; j < part.len; j++) {485- const lo = climb ? 0 : signed;486- const hi = (climb ? signed : 0) + CHANNEL_CLEARANCE;485+ // Every cell of the part claims the part's FULL envelope. The486+ // whole delta rides on cell 0's stepAfter, so later cells sit at487+ // the part's EXIT y — their band must reach BACK across the delta488+ // (the piece descends/climbs continuously through them). The old489+ // j>0 band repeated cell 0's, leaving the geometry's upper half490+ // unclaimed on descents: apfuzz-81's slant-long-d walls topped out491+ // 1.0 above their claim and barricaded the channel crossing above.492+ const lo = j === 0 ? Math.min(0, signed) : Math.min(0, -signed);493+ const hi = (j === 0 ? Math.max(0, signed) : Math.max(0, -signed)) +494+ CHANNEL_CLEARANCE;487495 steps.push({488496 move: "F",489497 band: [lo, hi],@@ -858,11 +866,15 @@ class SegmentBuilder {
858866 );859867 if (options.length > 0) {860868 const option = sRng.pick(options);861- slope = {862- at: sRng.int(2, runLen - option.totalLen - 1),863- option,864- climb,865- };869+ // The cubic-spillover cell (one past a climb / one before a870+ // descent lip) shares the part low base, so the slope must sit871+ // one cell further from the shoreline on that side to keep the872+ // ≥2 flat shoreline cells contract.873+ const lo = climb ? 2 : 3;874+ const hi = runLen - option.totalLen - (climb ? 3 : 1);875+ if (hi >= lo) {876+ slope = { at: sRng.int(lo, hi), option, climb };877+ }866878 }867879 }868880 const slopeCells = new Map<number, Step>();@@ -874,8 +886,14 @@ class SegmentBuilder {
874886 for (const part of parts) {875887 const signed = slope.climb ? part.delta : -part.delta;876888 for (let k = 0; k < part.len; k++) {877- const lo = (slope.climb ? 0 : signed) - 1; // mass headroom below878- const hi = (slope.climb ? signed : 0) + CHANNEL_CLEARANCE;889+ // Full part envelope per cell (same fix as commitSlope): cells890+ // after the stepAfter sit at the part's exit y, so their band891+ // reaches BACK across the delta. Mass headroom −1 preserved.892+ const lo = (k === 0 ? Math.min(0, signed) : Math.min(0, -signed)) -893+ 1;894+ const hi =895+ (k === 0 ? Math.max(0, signed) : Math.max(0, -signed)) +896+ CHANNEL_CLEARANCE;879897 slopeCells.set(j++, {880898 move: "F",881899 band: [lo, hi],@@ -943,12 +961,25 @@ class SegmentBuilder {
943961 ? slope.option.parts944962 : slope.option.parts.slice().reverse();945963 let at = slope.at;964+ let lastLow = 0;946965 for (const part of parts) {947966 const entryBase = baseYs[at];948967 const low = slope.climb ? entryBase : entryBase - part.delta;949968 for (let k = 0; k < part.len; k++) partLow.set(at + k, low);969+ lastLow = low;950970 at += part.len;951971 }972+ // Cubic spillover (apfuzz-58 wedge): the realized climb/chute973+ // centerline is a smooth cubic that is still below the upper974+ // terrace in the first cell PAST a climb (and already dipping in975+ // the cell BEFORE a descent's lip). Those cells share the adjacent976+ // part's low base too, or the mass face pokes up through the977+ // still-sloping channel floor and walls the run.978+ if (slope.climb) {979+ if (at < runLen && !partLow.has(at)) partLow.set(at, lastLow);980+ } else if (slope.at > 0 && !partLow.has(slope.at - 1)) {981+ partLow.set(slope.at - 1, partLow.get(slope.at)!);982+ }952983 }953984 const profile = baseYs.map((y, j) => partLow.get(j) ?? y);954985 @@ -276,20 +276,20 @@ Deno.test("scenario lip-undershoot rescue: the jump decision saves a slow arriva
276276 277277 Deno.test("scenario diamond-branch: both branches carry the marble through", async () => {278278 module_ = await factory();279- // grav-scan-5 early segments host an island diamond (re-pinned after279+ // wide-scan-1 early segments host an island diamond (re-pinned after280280 // the gravity clause). Drive through segment 2281281 // twice — once on the main (left) branch rail, once on the alt (right)282282 // — the marble must clear the split, branch, and merge on BOTH. This is283283 // the branch-respawn contract: after a fall, the retry may ride the284284 // other branch, so both must be survivable.285- const w = new EndlessWorld("grav-scan-5", catalog);285+ const w = new EndlessWorld("wide-scan-1", catalog);286286 const hasDiamond = [0, 1, 2].some((i) =>287287 w.get(i).plan.reservations.some((r) => r.pieceIds[0]?.startsWith("diamond"))288288 );289- assert(hasDiamond, "grav-scan-5 early segments lost their diamond");289+ assert(hasDiamond, "wide-scan-1 early segments lost their diamond");290290 const goal = w.get(3).startS;291291 for (const useAlt of [false, true]) {292- const r = drive("grav-scan-5", 5, 90, goal, 0, useAlt);292+ const r = drive("wide-scan-1", 5, 90, goal, 0, useAlt);293293 assert(294294 !r.fell && r.reachedS >= goal,295295 `${useAlt ? "alt" : "main"} branch failed: fell=${r.fell} ` +@@ -334,17 +334,17 @@ Deno.test("scenario helix-dive: marble rides the full spiral and the underpass",
334334 335335 Deno.test("scenario wide-run: marble rides funnel → wide channel → funnel", async () => {336336 module_ = await factory();337- // wide-scan-0 segment 0 hosts an island wide run (funnel-bracketed by337+ // wide-scan-7 segment 0 hosts an island wide run (funnel-bracketed by338338 // construction). The proxies widen the ribbon over those windows339339 // (wideScalesOf) — the marble must enter the funnel, cross the wide340340 // deck, and be gathered back to the narrow channel.341- const w = new EndlessWorld("wide-scan-0", catalog);341+ const w = new EndlessWorld("wide-scan-7", catalog);342342 const seg = w.get(0);343343 const wi = seg.pieceFamilies.indexOf("wide");344- assert(wi >= 0, "wide-scan-0 seg 0 lost its wide run");344+ assert(wi >= 0, "wide-scan-7 seg 0 lost its wide run");345345 let endS = 0;346346 for (let k = 0; k <= wi + 1; k++) endS += seg.path.segments[k].length;347- const r = drive("wide-scan-0", 3, 60, endS + 8);347+ const r = drive("wide-scan-7", 3, 60, endS + 8);348348 assert(!r.fell, "marble fell in the wide run");349349 assert(350350 r.reachedS >= endS + 7,@@ -416,18 +416,18 @@ Deno.test("scenario bouncer-pins: marble caroms the wide-deck pins and carries o
416416 417417 Deno.test("scenario gravity-run: tilted gravity surges the marble on the flat", async () => {418418 module_ = await factory();419- // grav-scan-7 segment 0 plans a GRAVITY zone: world gravity tilts419+ // grav-scan-2 segment 0 plans a GRAVITY zone: world gravity tilts420420 // forward along the run (shim gravityDir), so a geometrically flat421421 // straight accelerates the marble — the assist stays silent between422422 // cruise and the envelope cap, gravity itself does the work. The zone423423 // stops 2 cells short of its clause (brake runway) and the marble must424424 // roll on past it.425- const w = new EndlessWorld("grav-scan-7", catalog);425+ const w = new EndlessWorld("grav-scan-2", catalog);426426 let gravAt: number | null = null;427427 for (let i = 0; i < 3 && gravAt === null; i++) {428428 if (w.get(i).plan.mods.some((m) => m.kind === "gravity")) gravAt = i;429429 }430- assert(gravAt !== null, "grav-scan-7 early segments lost their gravity zone");430+ assert(gravAt !== null, "grav-scan-2 early segments lost their gravity zone");431431 const rails: Rail[] = [];432432 for (let i = 0; i < 4; i++) rails.push(railForSegment(w.get(i)));433433 const rail = concatRails(rails);@@ -446,7 +446,7 @@ Deno.test("scenario gravity-run: tilted gravity surges the marble on the flat",
446446 const plain = stationGravity(rail, 0);447447 assertEquals(plain, [0, -20, 0]);448448 const r = drive(449- "grav-scan-7",449+ "grav-scan-2",450450 4,451451 40,452452 rail.s[last] + 10,