# AP-159 / #335 contract (Campaign S slice S1B) — port the INDOOR box-admit half of retail's part-array `find_transit_cells` **Date:** 2026-08-07 (overnight). **Scoped by:** the session lead. **Implementer:** one Sonnet agent against THIS contract. **Review:** dual Opus after. **Severity honest-check:** LOW — over-inclusive only (extra indoor broadphase candidates, never a missed one). It is in scope tonight because it is the last half of AP-156's traversal residual and Campaign S's S1; if any step below turns out larger than described, STOP and report rather than compressing quality. ## The divergence acdream's indoor cell-membership walk for multi-part BSP objects (`CellTransit.BuildShadowCellSetFromParts`, indoor arm) admits a neighbouring EnvCell on a **sphere**-vs-portal-plane test (`FindTransitCellsSphere` over `BuildBspPartSpheres` output). Retail's part-array `CEnvCell::find_transit_cells` uses the sphere only as a **cheap reject**; the ADMITTING test is **box**-vs-plane, followed by a **box-vs-cell BSP** gate in the destination cell. Full disassembly-backed spec: the #335 entry in `docs/ISSUES.md` (steps 1–4 with addresses) — that entry is the oracle of record; this contract adds the pseudo-C line map. ## Oracle map (grep-named-first is already done — these are the exact places) `docs/research/named-retail/acclient_2013_pseudo_c.txt`: - **Part-array overload** `CEnvCell::find_transit_cells(this, count, CPhysicsPart** parts, CELLARRAY*)` @0x0052cae0 — lines **310127–310257**. Per portal x per part: sphere cheap-reject (`physics_sphere` centre through `Position::localtolocal`, eps = `F_EPSILON + radius` @0x0052cb65) -> box admit (`CPhysicsPart::GetBoundingBox` @0x0050d600 -> `BBox::LocalToLocal` @0x005b1e60 -> `Plane::intersect_box` @0x005aa170 @0x0052cc05) -> if portal-side differs: `other_cell_id == 0xFFFFFFFF` sets leads-outside, else `CCellPortal::GetOtherCell` (threading `do_not_load_cells`) then box-vs-destination-BSP `CCellStruct::box_intersects_cell` @0x00533910 (line 317675: one-liner into `BSPTREE::box_intersects_cell_bsp` @0x005398b0 -> `BSPNODE::box_intersects_cell_bsp` @0x0053c880, body at line **325993**) gating the add @0x0052cc5a; after all portals, leads-outside runs `add_all_outside_cells` @0x0052ccea. - **Adjacent overload** @0x0052c820 (lines **309968–310126**) also contains a box refinement block (@0x0052c76c–0x0052c7d2). Its Binary Ninja signature (`CSphere const* arg4`) is inconsistent with its body indexing parts — a known BN artifact class. Your MANDATORY pseudocode step (below) disentangles the two overloads BEFORE any C# is written; if your reading contradicts the #335 entry's step list, STOP and report. - `Plane::intersect_box` @0x005aa170 — line 439037. Note its result contract (the caller accepts `== 3 || == side`). - `BSPNODE::box_intersects_cell_bsp` @0x0053c880 — line 325993. This is the box-vs-cell traversal you must port TWICE (graph + flat, below). ## Deliverables, in order **D0 — pseudocode doc FIRST** (`docs/research/2026-08-07-ap159-pseudocode.md`): the part-array overload and the box-vs-BSP traversal, translated to readable pseudocode with the addresses inline, per the repo's mandatory workflow. The overload-signature confusion above must be resolved here. No C# before this file exists. **D1 — the box primitives, in BOTH representations + referee.** `BSPQuery` (graph) and `FlatBspQuery` (production flat) each gain the box-vs-cell-BSP traversal (`BoxIntersectsCellBsp`), and `Plane`-vs-box gains `intersect_box` semantics wherever the shared math lives. House rule from Slices I4/I5: a NEW traversal in two representations ships with an **exact differential referee test** — same inputs through both, assert identical verdicts over (a) synthetic cells covering each BSP node type and (b) a sweep of installed-DAT EnvCells (follow `FlatBspQueryDifferentialTests` patterns). **D2 — rewire the indoor arm.** `BuildShadowCellSetFromParts`'s indoor arm follows retail's order exactly: per portal x per part sphere cheap-reject -> box admit -> other-cell resolution with `do_not_load_cells` -> destination `box_intersects_cell` gate -> leads-outside flag -> `AddAllOutsideCellsFromParts` after the portal loop. The box data ALREADY EXISTS: `ShadowPartBox` (`worldParts`) is passed in today and used only by the outdoor rectangle. Do NOT touch the outdoor arm, the AD-49 seed-time rectangle, or the static prune. The outdoor **building bridge** (`CheckBuildingTransit`, retail `check_building_transit` @0x0052c5d0) has the same sphere-as-admit defect: port it ONLY if it reuses D1's primitive with a small diff; otherwise leave it and report it as the explicit remainder. **D3 — conformance + direction tests.** 1. A synthetic two-EnvCell fixture where a part's SPHERE overlaps the portal plane but its BOX does not: pre-fix the neighbour is admitted, post-fix it is not. Sabotage-verify: re-widen the admit to the sphere test and the test must redden. 2. The inverse guard: a part whose box DOES cross admits — unchanged. 3. Direction assertion over an installed-DAT sweep: the post-fix indoor membership set for every swept object is a SUBSET of the pre-fix set (over-inclusion strictly shrinks; any ADDED cell is a defect). Report counts: objects swept, cells removed, cells added (must be 0). **D4 — measurement in the report:** from D3.3, the installed-DAT population whose membership actually changed, with three worst examples (object, cells before/after). If the population is ZERO, say so plainly — that is a valid outcome and the register row then records "ported, no installed data affected". ## Scope — OUT - The AP-156 SCALE residual (retail doesn't scale flood spheres; we do) — reserved for the user's explicit decision. Do not change scaling anywhere. - The outdoor arm, `AddAllOutsideCellsFromParts`, the 3x3 sphere path, anything in `ShadowObjectRegistry`. - Register/ISSUES edits — the session lead does those. ## Acceptance Build green; FULL suite `dotnet test AcDream.slnx -c Release -m:1` green with exact totals reported; D3.1 sabotage performed and reported verbatim; nothing committed. Work ONLY in `C:\Users\erikn\source\repos\acdream` with absolute paths; do NOT spawn subagents. If any oracle reading contradicts the #335 entry or this contract, STOP and report the contradiction.