acdream/docs/research/2026-08-06-c5c-closeout-handoff.md
Erik e6457cc849 fix(physics): close the AP-156 fix review — real containment oracle, type-level invariant, AP-158
Both review lenses PASSED; this is the cleanup, not a rescue. Evidence:
docs/research/2026-08-06-ap156-review-closure.md (the review itself is
committed alongside it as the received artifact).

R1 — the load-bearing containment test could not fail. Its truth and flood
values were two hand-copies of the same expression over the same part set,
so the shortfall was algebraically identically zero for any DAT input. The
oracle is now PHYSICS-POLYGON VERTICES — a different DAT field from the
bounding sphere the builder emits, so the two sides can genuinely disagree.
Sabotage-verified three ways after full cleans: dropping the bounds centre
in production reddens it (428 Setups, worst 35.869 m on 0x0200129A, matching
an independent out-of-repo sweep exactly); dropping only the scale on the
centre reddens it (326); and corrupting the TEST's own bounds oracle reddens
it (467) where under the shipped oracle that same corruption was invisible
by algebra. Renamed accordingly. A6's stale "cap control" comment corrected:
that loop is the test's own uncapped re-implementation and cannot observe a
cap regression — the cap is covered in Core.

R2 — the population was understated. 172 is AP-152's DISPATCH population;
AP-156's is 530 BSP-bearing Setups, of which 525 have a flood sphere move
and 428 fail vertex containment before the fix (412 at a 1 cm tolerance —
the review's figure; the gap is 16 Setups between 1.4 mm and 10 mm, real
geometry). 0 fail after, at any tolerance down to zero. Corrected in the
AP-156 row, the section-3 header, the C5c handoff and two test docstrings.
Dated review artifacts are left as written — "170 of 172" was correct for
what they measured, and rewriting evidence to match a later measurement
loses provenance.

A1 — BoundsCenter = default reopened at the type what the commit closed at
the seam. Dropping the default alone would NOT have closed the review's own
scenario (a copied Cylinder call site would write Vector3.Zero explicitly
and stay green), so ShadowShape's constructor is now private and BSP shapes
are built only through ShadowShape.Bsp(..., FlatCollisionSphere localBounds),
which takes radius and centre as ONE value and scales them together. There
is no expression a caller can write that carries one and drops the other.
22 construction sites converted; the same sabotage now reddens 5 Core tests
where the review's sabotage A reached 4, because both BSP producers share
one scaling path.

A2 — #333 is real and bigger than filed, and its retail question is
answered. I disassembled CObjCell::find_obj_collisions @0x0052b750 from the
PDB-paired binary myself (check_exe_pdb.py MATCH) rather than inheriting the
claim: its only early-out is sphere_path.insert_type == INITIAL_PLACEMENT_
INSERT, then it calls FindObjCollisions on every unparented non-self shadow
object UNCONDITIONALLY. Retail has NO distance pre-filter, so acdream's
"+ movement + 2f" reach filter is an invention with no register row — filed
as AP-158, carrying the disassembly, the F_EPSILON = 0.0002 m contrast, and
the measured blast radius (118 of 477 unique installed physics-BSP GfxObjs
exceed its ~2.5 m budget, 46 exceed 5 m). Active AP rows 109 -> 110.

Recorded prominently in three places a reader will hit: TALL PROPS MAY SHOW
NO VISIBLE CHANGE UNTIL #333 LANDS, and a null result at the connected gate
is EXPECTED, not evidence against AP-156.

LOW items. R3: the comment claiming the cited evidence justified the whole
cap line is corrected, but int.MaxValue on the sorting-sphere branch stays —
capping at 1 would take Spheres[0], and retail's one sphere is
CSetup::sorting_sphere, a different DAT field; capping keeps the wrong field
AND flips the substitution under-inclusive (#98/#168 direction). AP-157
already owns it. R4: acdream scales the flood sphere where retail's
find_transit_cells never reads gfxobj_scale — added as a second residual on
AP-156. R5: retail's slack constant carried into AP-158 and #333. A3: the
per-call delegate allocation is back to a cached field, still derived from
the single bounds resolver. A5: noted; b52967de's message cannot be amended.

Gates: all 44 bin/obj deleted before every verdict-deciding build, each test
run gated on a verified "Build succeeded" in the same invocation. Release
build 0 errors / 21 pre-existing warnings. Complete suite 11,208 passed /
4 skipped / 0 failed — reconciles exactly with the e2b2d04c baseline; one
test renamed, none added, removed or skipped. Nothing conflated with the
known load-sensitive flakes #302 / #308 / #321.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 16:44:48 +02:00

16 KiB
Raw Blame History

C5c closeout + successor handoff — the placement cutover's automated half is done (2026-08-06)

Read this before any C5c or post-campaign work. It is the successor to 2026-08-05-c4-closeout-handoff.md (whose ⚠ BISECT HAZARD block still applies and is repeated in §6).

Branch claude/acdream-physics-divergence-5aa784, 21 commits from 02578441 to 7b3e2895. Nothing is pushed — the branch does not exist on the remote, and there are 388+ unpushed commits ahead of origin/main.


1. One-paragraph state

Every implementation item in the placement cutover campaign is now landed and dual-reviewed. C5b (classify-before-merge), #280 (portal destination prefetch), #276's remainder, AP-22 and AD-10 all shipped with both review lenses PASS. #309 was accepted as a standing divergence by user decision rather than fixed. What remains for C5c is entirely connected/visual work plus the ledger close — none of it can be done without the user at the client, and the probe strip cannot be done before it.


2. What landed, by slice

Slice Commits Outcome
C5b — classify before merge (#275) 735f0a72, ed806997, 23aa62f2, ff100cf3, 9ee9c1a1 Retired AP-131 and AD-60's legacy half. Seven production lines; five commits, because the review found a headless regression the change itself introduced.
#280 — portal destination prefetch 3aab05b0, 73cdb95c, bcb66ccd Reveal window now derives from the live streaming radii. D-1, an unrecoverable portal hang, was found by review and fixed.
#276 remainder 408c8e8f, fafc0b65 Settle now adopts the transition's resolved cell across an indoor seam.
#316, #317 429775d4, 1d2d4bb8 Report-only investigations. #316 cosmetic; #317 no retail basis.
#309 43cfdc4a Accepted as a standing divergence (user decision), not a planned fix. AP-136 is its permanent record.
AP-22 bc4679cd, 619de97a, ef976c6d Invented collision cylinder deleted in all three copies. Row retired.
AD-10 fe6ee877, 886333a2, fb454b74, 2223ed17, 7b3e2895 Retired by deletion — its stated justification was false at HEAD.

2.5 C5c's automated gate — PASS

Run at 7b3e2895 on the final binary, after deleting all 44 bin/obj directories (see §7 rule 4 — this session had three stale-artifact incidents, so an incremental result would not have been evidence):

dotnet test AcDream.slnx -c Release -m:1     # ACDREAM_PAK_PATH set

11,196 passed / 4 skipped / 0 failed, all nine projects:

Project Passed Skipped
AcDream.App.Tests 4,172 3
AcDream.Core.Tests 4,261 1
AcDream.Runtime.Tests 1,220 0
AcDream.Core.Net.Tests 764 0
AcDream.UI.Abstractions.Tests 546 0
AcDream.Content.Tests 125 0
AcDream.Headless.Tests 89 0
AcDream.Bake.Tests 15 0
AcDream.Cli.Tests 4 0

The 4 skips are the pre-existing set; no new skip was added anywhere in the campaign. None of the three known load-sensitive flakes (#302, #308, #321) fired in this run — but they are separately filed and must never be conflated if one does.

Net movement across the campaign: 11,106 at 0257844111,196, +90.


2.6 Connected gate — #280 USER-PASSED 2026-08-06; the rest NOT RUN

#280's reveal gate: PASS, user-accepted. User's words: "now portal space takes longer but terrain is complete when I exit." Both halves are the criteria the gate specified — a measurably longer hold (the fix doing its job) and a complete destination on reveal (the acceptance).

Probe evidence, ACDREAM_PROBE_PARK=1, retail UI, Release, ACDREAM_STREAM_RADIUS unset (c5c-gate-after2.log):

  • Three Portal reveals plus a Login reveal, every one at radius=12. Pre-#280 this was a hardcoded 1.
  • Each portal hold raised the wait cue at ~5.0 s (elapsedMs=5031 / 5000 / 5010) before materialized → world-visible → complete.
  • Generation 4 revealed cell 0x3032001C — the same cell as generation 1's login, i.e. a repeat visit to an already-seen landblock.
  • The park path was exercised (95 [park...] lines).

An accidental but genuine A/B. An earlier run in the same session set ACDREAM_PROBE_REVEAL_RADIUS=1, which is a radius VALUE, not an on/off flag — so it forced the pre-fix window. The user observed the original defect (landscape building in the background) with radius=1 on every probe line, and did not observe it with radius=12. That is the before/after pair the gate asked for, obtained by accident. Successors: this probe overrides the radius; it does not merely enable logging.

NOT RUN — do not read this section as gate coverage

Owed gate Status
D-1's two reachability scenarios (double-recall to the same landblock with a walk between; mid-hold quality-preset drop) NOT RUN. Generation 4's repeat visit is suggestive but is not the demote path D-1 needs.
AP-136's six-step park check NOT RUN as the six-step protocol. The probe fired 95 times incidentally; that is not the check.
Route-7 thickening NOT RUNACDREAM_PROBE_REMOTE_TELEPORT recorded 0 lines; no remote teleport occurred.
Two-client observation NOT RUN
Canonical nine-stop soak NOT RUN
Lifecycle/reconnect route on the final binary NOT RUN
AD-65 / AD-66 local-player visual gate NOT RUN

The campaign ledger closes with these outstanding by user direction, not because they were discharged. Anyone citing "C5c passed" must cite §2.6's table alongside it.


3. WHAT C5C STILL OWES — all of it needs the user

Nothing below can be discharged without a live client. The automated half is complete.

3.1 Connected gates, batched into one sitting

  1. #280's reveal gate. The user's original repro was a recall, so the route needs a lifestone leg, not only /teleloc, plus a first-login stop. A/B with ACDREAM_PROBE_REVEAL_RADIUS, ACDREAM_STREAM_RADIUS unset. The pre-fix run is expected to show the defect; the post-fix hold must be measurably longer. If it isn't, the gate widened nothing.

  2. #280 D-1's two reachability scenarios, never reproduced live: two consecutive recalls to the same landblock with walking in between, and a mid-hold quality-preset drop. Both are fixed and unit-covered; neither has been seen on a running client.

  3. AP-136's six-step park checkACDREAM_PROBE_PARK=1. This survives #309's deferral: it validates the SHIPPED rollback path (restorableOnCancel in SubmitPreparedPlacementCore, the shared core behind every production placement), not the deferred fix.

  4. Route-7 thickening — its gate passed on one cause=propagate probe line; the evidence is THIN.

  5. AD-65 / AD-66 need a local-player visual gate before any fix (see §5).

  6. C5c proper: two-client observation, canonical nine-stop soak, lifecycle/reconnect route on the final binary, and the user's visual matrix.

  7. AP-156 — the BSP flood-sphere placement fix (b52967de). The criterion is CONTAINMENT, not size, and a null result on tall props is EXPECTED. Two things must be said to the user before this one is run, or its outcome will be misread in both directions:

    • "A prop stopped blocking" is not by itself a bug report. AP-156 shrinks the flood for 143 of the 172 AP-152 Setups on purpose, because retail's BSP branch (calc_cross_cells @0x00515230, 0x0051528f jne) contributes no primitive at all — the old flood was larger only because it included a sphere retail never reads. The question to ask of any such observation is whether the object's own BSP geometry still reaches the cell it stopped blocking from. If it does, that is a bug; if it does not, that is retail.
    • "A tall prop STILL does not block" is the expected symptom of AP-158 / #333, not of this fix. AP-156 puts the geometry in the right cell; acdream's own maxReach broadphase filter — which retail does not have at all — then discards it one layer down, for 118 of the 477 unique installed physics-BSP GfxObjs. A null result on tall props is not evidence against AP-156. Run this gate after #333, or run it on short/wide off-centre props where the offset is inside the ~2.5 m budget.

    Population, for the user: the change touches 525 of the 530 BSP-bearing Setups (not the 172 the commit body names — that is AP-152's dispatch population). See the AP-156 register row.

3.2 Then, and only then

The probe strip — DEFERRED 2026-08-06, deliberately, and the ledger closes without it.

Six flags — ACDREAM_PROBE_REMOTE_LANDING, ACDREAM_PROBE_REMOTE_SLIDE, ACDREAM_PROBE_PARK, ACDREAM_PROBE_REMOTE_TELEPORT, ACDREAM_PROBE_CHILD_CELL, ACDREAM_PROBE_LOCAL_TELEPORT — plus ACDREAM_PROBE_REVEAL_RADIUS.

Not stripped, because §2.6's table says their gates were never run. Only #280's reveal gate was discharged. Route-7 thickening recorded zero remote-teleport lines; AP-136's six-step park protocol was not performed; the two-client observation, nine-stop soak and lifecycle/reconnect route did not happen. Stripping the family now would delete exactly the instrumentation those still-owed gates need, which is the failure this section was written to prevent — so honouring it means not stripping, even though the campaign is closing.

ACDREAM_PROBE_REVEAL_RADIUS is kept too, despite #280 being closed: AP-149 (outer ring accepts terrain-only) and #326 (the missing Viewing Distance option) are both open and both would want the same A/B harness. Retiring it now would mean rebuilding it.

Successor: strip the family when the §2.6 gates are actually run, not on a calendar. Note again that ACDREAM_PROBE_REVEAL_RADIUS is a radius value, not a boolean — setting it to 1 silently reproduces the pre-#280 defect.

3.3 Then close the ledger

Register / roadmap / milestones / memory, and the campaign ledger close.


4. Issues filed this session

# Subject
#321 DatSoundCacheTests concurrent-decode dedup, full-suite load (third load-sensitive flake)
#322 Two callers compute the same pre-placement flags from the same inputs
#323 A far-snap store can silently stale a pending initial-create receipt
#324 Graphical and no-window hosts run parallel, non-shared inbound routes
#325 Gate A's teleport test is narrower than retail's== where retail is "not older"
#326 acdream has no Viewing Distance option (retail's Render.LandscapeDrawDistance)
#327 No analogue of retail's DDD prefetch progress readout
#328 Camera far plane hardcoded 5000 f; retail's zfar is byte-verified 4000
#329 Portal wait cue arms 5 s late; retail emits per tunnel rotation segment
#330 Headless registers no live-entity collision at all — a bot walks through every NPC
#331 ResolveWithTransition refuses ALL uphill motion with a body supplied
#332 Headless bots appear to have no remote dead-reckoning

#331 is the one to look at first. Severity was raised from UNKNOWN once the discriminator was found: it is the body: parameter, not the fixture. With body: null the uphill sweep climbs; with a body it returns ok=False and zero movement — under a call profile identical to the local player's (IsPlayer | EdgeSlide, human two-sphere Setup), on ramps as shallow as 1.1°. A diagonal request keeps cross-slope X and zeroes only up-slope Y. Nothing in the suite asserts uphill progress on a walkable slope, which is why it was invisible — the test that found it passed vacuously.


5. Register movement

Retired: AP-1, AD-1 (C5a) · AP-145 (C5a) · AP-131, AD-60's legacy half (C5b) · AP-22 · AD-10.

Filed: AP-147 (delta-stream cardinality) · AP-148 (#325's Gate A narrowing) · AP-149 (outer ring accepts terrain-only) · AP-150 (wait-cue delay is not retail's trigger) · AP-151 (gate stricter than retail on the GPU-upload axis) · AP-152 (live path emits primitives and BSP additively where retail is exclusive; 172/5,935 Setups incl. BSP doors) · AD-64 (the duplicated residency decision) · AD-65, AD-66.

AD-65 deserves attention. Its magnitude was filed at half the truth: the row states cos²θ but quantified 1cosθ. Corrected to 25% short at 30°, 50% at 45°, confirmed by measurement (#331's probe: 0.0735 m for a 0.1 m request at 30.96° = cos²(30.96)). It is a live lead for #269's slope-slide residual — and note that project memory's #269 do-not-retry covers friction and jump chains, which are byte-exonerated; AdjustOffset is a different function and is not covered by it.


6. ⚠ BISECT HAZARD — carried forward

Commits 735f0a72..23aa62f2 contain a live headless defect: every remote entity's FullCellId is frozen at its placement value for the whole session in AcDream.Headless, and the local player loses one of AP-146's three cell-refresh edges. Introduced by 735f0a72, fixed at ff100cf3. Nothing throws; no test in the range catches it.


7. Process findings — stated as rules, each paid for this session

  1. A blast-radius enumeration only reaches as far as the call graph its author walked. C5b's was performed over the graphical OnPosition path and missed AcDream.Headless entirely — 11,000 green tests, one frozen host. Ask which traversal, and what it structurally could not reach. Both hosts, every time.
  2. Two independent adversarial reviewers converging is near-proof; a lone finding is a lead. It happened three times this session (the headless hole, the missing payload gate, D-1) and all three were real.
  3. Assume a test does not discriminate until sabotage proves it. SEVEN green tests covering nothing were found or avoided: C5b's conservation test, #276's three settler tests, #280's tautological integration fixture, the atlas-tier seam 4,170 tests missed, AD-10's only existing test (a dead method with a hard-coded formula), and AD-10's contract-specified T1 sabotage which came back green and was rejected rather than shipped.
  4. bin/obj can serve deleted code even under --no-incremental and -t:Rebuild. Three incidents. Delete all 44 directories before any verdict-deciding result. A stale artifact does not look like an error — it produces a plausible failure, or a plausible pass.
  5. Every contract in this campaign has been wrong at least once, and the implementation is what catches it. C5b's §3-D2 (a "dead" ternary that was live), #280's §7 (three false items), AP-22's §7.1 (a literal that was a three-way condition), AD-10's §7.1 (a non-discriminating sabotage). Brief implementers to rebut, and treat a reasoned rebuttal as more valuable than a compliant edit.
  6. Verify a cited address is the construct you claim. AP-150 mis-cited 0x004D7064 — a PStringBase constructor — as the SendNotice call, despite being filed with a byte-level disassembly. Precision of method does not prevent an error of line.
  7. Binary Ninja drops flag tests. if (-((eax_7 - eax_7)) == 0) renders an always-true where a real wrap-safe compare lives. Confirmed at Gate A (0x00454054), DoVectorUpdate (0x004521F5), HandlePlayerTeleport (0x00452186), and FindObjCollisions. Disassemble the PDB-paired binary wherever a comparison or constant is load-bearing.
  8. Do not fan out subagents. Two blowups: six agents spawning their own children exhausted a session usage limit and killed four tasks mid-flight; a later three spawned five more. Every brief must say "do not spawn subagents" explicitly — none of the early ones did. One code-writer at a time; the shared worktree tolerates no more.
  9. A shell failure inside a compound command can leave a commit claiming work it did not do. ef976c6d was needed because a heredoc invoked python (absent here; py is the binary) while the commit still reported success.

8. Where to start

  • Post-campaign: #331 first (§4), then AP-152 and #330 — both are real collision divergences with user-visible consequences.
  • Domain entry points remain claude-memory/project_physics_collision_digest.md and claude-memory/project_render_pipeline_digest.md.
  • The campaign plan (docs/plans/2026-08-02-placement-cutover.md) item 5 now records AP-22 and AD-10 as retired; item 3 records #280 as done.