acdream/docs/research/2026-08-07-s2-static-sphere-contract.md
Erik ce0bfce1cf docs: S2 contract — AP-155's static sphere-as-cylinder emission, both sites pinned
Both static publication paths emit an authored Setup Sphere as a
base-anchored Cylinder (r, 2r) while the live path emits a Sphere for
the same data — different narrow-phase dispatch and a route-dependent
collision difference for the same object. The fix mirrors FromSetup's
step-3 emission at both sites; the contract's first test is the
route-independence property asserted shape-for-shape, and the dispatch
test picks geometry where cap-hit and curve-hit verdicts differ so the
sphere verdict is observable, not inferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:00:11 +02:00

3.3 KiB
Raw Blame History

S2 contract (Campaign S) — AP-155: static publication must emit authored Spheres as Spheres

Date: 2026-08-07 (overnight). Scoped by: the session lead. Implementer: one Sonnet agent. Review: dual Opus.

The divergence (AP-155, narrowed to its surviving half)

Both static publication paths emit an authored Setup Sphere as a height-capped Cylinder:

  • src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs (~10181036)
  • src/AcDream.Content/LandblockPhysicsContentBuilder.cs (~672690)

Identical code in both: ShadowShape.Cylinder(radius = r*scale, cylHeight = 2r, base = origin*scale - r*ẑ rotated). The LIVE path (ShadowShapeBuilder.FromSetup, step 3) emits ShadowShape.Sphere for the same authored data. Consequences: (a) narrow-phase dispatch differs (CCylSphere tests vs CSphere tests — a mover meets a flat cap where retail meets a curved surface), and (b) the SAME object collides differently depending on whether it arrived as a landblock static or a live spawn — the route-dependence Campaign S's plan calls out. Retail's CPhysicsObj::FindObjCollisions dispatches Spheres to CSphere::intersects_sphere — there is no cylinder substitution anywhere in retail for this branch.

The fix

Replace both emissions with ShadowShape.Sphere, mirroring FromSetup step 3's emission EXACTLY — same origin/scale composition, same guards (Radius <= 0 skip stays). Read FromSetup first and copy its semantics rather than inventing; if FromSetup's sphere emission differs from what a plain mirror would produce here (e.g. BoundsCenter handling), STOP and report the difference instead of choosing.

Then check the ripple: BuildFloodSpheres' sphere branch (uncapped) now sees these as Spheres — flood behaviour matches the live path, which is the point. RetailSphereCap applies to the Cylinder branch only (AP-156's retirement of the 10-cap on non-cyl branches) — verify with a test, not by reading alone.

Tests

  1. Parity: for a representative Setup with authored Spheres, the static publication's registered shapes are shape-for-shape identical (type, local position, radius, scale) to FromSetup's output for the same inputs. This is the route-independence property, asserted directly.
  2. Dispatch: a mover resolve against a statically-published sphere object takes the Sphere narrow phase (assert via outcome on a diagonal approach that distinguishes cap-hit from curve-hit: pick geometry where cylinder and sphere verdicts differ, assert the sphere verdict).
  3. Sabotage: restore the Cylinder emission at ONE site; tests 1 and 2 both redden; restore.
  4. Existing static publication and content-builder suites stay green.

Measurement (report, not gate)

Count, over installed landblocks already swept by existing content tests (reuse their enumeration), how many static entities carry sphere-only Setups — the affected population. Three example object ids.

Scope — OUT

Register/ISSUES edits (session lead). ShadowShapeBuilder, ShadowObjectRegistry internals. The live path. Anything about SortingSphere (AP-157 is measured and deferred).

Acceptance

Full suite dotnet test AcDream.slnx -c Release -m:1 green, totals reported; sabotage reported verbatim; nothing committed. Only C:\Users\erikn\source\repos\acdream, absolute paths, no subagents; contradictions → STOP and report.