diff --git a/docs/research/2026-08-07-s4b-tangent-rest-contract.md b/docs/research/2026-08-07-s4b-tangent-rest-contract.md new file mode 100644 index 00000000..21c5c3c4 --- /dev/null +++ b/docs/research/2026-08-07-s4b-tangent-rest-contract.md @@ -0,0 +1,99 @@ +# S4b contract (Campaign S) — port the retail resting pair: tangent walkable rest + bare-radius push-out + +**Date:** 2026-08-07. **User decision, same day: "port the retail pair."** +**Implementer:** one Sonnet agent, AFTER S1B and S2 have landed (one physics +slice in flight at a time; build slots). **Review:** dual Opus. **Gate:** the +user's eyes on a slope — feet are EXPECTED to hover slightly (authentic +retail): ~3 cm at 30°, up to ~20 cm near the walkable limit. + +## The mechanism, settled by measurement + two decompilers (#341) + +Retail's walkable placement (`CPolygon::adjust_sphere_to_plane` @0x00538210, +BN pseudo-C 322032 + Ghidra cross-confirm) solves +`t = (dist ∓ r) / dot(N, stepDir)` — the sphere rests TANGENT to the slope, +perpendicular distance = radius. Its `adjust_offset` push-out uses the BARE +radius (AD-66's byte evidence) and is therefore structurally inert at rest. +acdream rests PLANTED (perp = r·N.z) and its substituted r·N.z trigger is +inert here by the same algebra. The user's live capture: bare trigger vs our +rest would fire on 84% of grounded slope ticks (2,471/2,955), lifts 2–88 mm. +**The pair ports together or not at all.** + +## The suspect line, pinned at scoping + +`Transition.ValidateWalkable` (TransitionTypes.cs ~3652) computes +`lowPoint = sphereCenter − (0,0,radius)` — the sphere's VERTICAL bottom — and +`dist = dot(lowPoint, N) + D + waterDepth`, pushing up by `dist/N.z` until +the FEET clear the plane. Resting identity: `dot(center,N)+D = r·N.z`. +Meanwhile our `AdjustSphereToPlane` (BSPQuery.cs:364 and its FlatBspQuery +twin) is ALREADY a faithful tangent port of @0x00538210 — so acdream today +mixes tangent (BSP walk solve) with planted (ValidateWalkable) and the +planted one wins the resting height on terrain. + +## Deliverables, in order + +**D0 — byte-pin retail's `OBJECTINFO::validate_walkable` FIRST.** Grep the +named pseudo-C; disassemble the distance basis from the PDB-paired binary if +BN is ambiguous (the flag-idiom artifact class). The ONE question: does +retail measure the perpendicular sphere clearance +(`dot(center,N)+D − radius` — tangent semantics) or a vertical foot point? +Cross-check ACE's `ObjectInfo.ValidateWalkable`. Write the pseudocode doc. +**If retail's validate_walkable turns out to ALSO use a vertical foot point, +STOP — the whole slice premise changes and the session lead re-decides.** +The waterDepth term's exact placement in retail's expression must be pinned +too (AP-10's sink-in behaviour must survive byte-for-byte). + +**D1 — port the pair, one commit:** +1. `ValidateWalkable`'s distance basis → retail's (per D0's pin). +2. `AdjustOffset`'s push-out → bare radius in trigger AND numerator (the + exact diff that was withheld — its two `[Skip]`-ed conformance tests in + `S4AdjustOffsetConformanceTests` un-skip and must pass unchanged). +3. **AD-69 rides along:** the same block's `dist` gains retail's + `LandDefs::get_block_offset` cell-relative correction (the plane's cell + frame vs the mover's), per that row's citations. + +**D2 — the #341 harness anomaly retest.** With the pair in, run +`RuntimeRemoteUphillProgressTests` TEN times (mixed single/class filters, +one full bin/obj clean-room among them) and report every outcome. The +mechanism predicts the absorbed-tick latch becomes STABLE at the tangent +rest (no lift — the settle leaves perp = r, bare trigger inert). If the +flip persists, capture per-tick positions inside the test (temporary +prints) and report the matrix — do NOT recalibrate goldens on a flipping +measurement. + +**D3 — conformance + regression:** +- Exact-value tests for the new resting identity on a tilted plane + (perp = r), the flat-ground case (unchanged: N.z=1 makes planted and + tangent identical — this is why the suite never saw the difference), and + the water sink-in cases (AP-10's 0.1/0.45/0.9 depths, unchanged + behaviourally per D0's waterDepth pin). +- The uphill no-flap guard (`Uphill_NoContactFlapAcrossTicks`) must stay + green — the mechanism predicts it does (tangent rest + bare trigger = + retail's own stable pair). +- Sabotage: revert ONLY the ValidateWalkable half — the un-skipped AD-66 + exact-value tests stay green but the new resting-identity test reds AND + a re-run of the user's capture analysis script over a synthetic planted + rest shows the 84% fire pattern returning. Report verbatim. + +**D4 — bookkeeping (session lead does the register/ISSUES edits):** AD-66 +retires with the pair; AD-69 retires; #341 closes; the AD-65 register +retirement gains a sibling note. + +## Scope — OUT + +The sliding/crease arm, the #331 absorb semantics (crease arm — untouched +by this pair), any scaling change (AP-156's question is separate), indoor +EnvCell walkable paths beyond what D0 proves shares the same +validate_walkable, S1B's files. + +## Acceptance + +Full suite clean-room green (all bin/obj deleted first — this family has +burned three verdicts on stale artifacts); D2's ten-run matrix reported; +nothing committed. Absolute paths, no subagents, contradictions → STOP. + +## The gate (user, ~3 min) + +Same slope run as this morning's G1, plus: stand still on a steep slope and +LOOK DOWN — feet slightly above the surface is CORRECT (retail's look). +Downhill speed must remain the post-AD-65 feel. Any per-tick jitter, +bouncing, or Falling-animation flicker on slopes = FAIL, revert the slice.