# 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` (~1018–1036) - `src/AcDream.Content/LandblockPhysicsContentBuilder.cs` (~672–690) 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.