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>
This commit is contained in:
Erik 2026-08-07 01:00:11 +02:00
parent 2edfc70467
commit ce0bfce1cf

View file

@ -0,0 +1,72 @@
# 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.