acdream/docs/research/2026-08-06-280-review-architecture.md
Erik 73cdb95c7b fix(streaming): make a demoted landblock render-ready like a published one (#280 D-1)
Both #280 review lenses returned FAIL on the same defect, and both were
right. IsRenderNeighborhoodResident's widened outer arm requires
IsRenderReady out to FarRadius, justified by "a Far-tier landblock
registers with an empty mesh set and is therefore render-ready." That held
only for a landblock that ARRIVED as Far. The second, equally first-class
way to be Far tier is a Near->Far DEMOTE:

  DemoteLandblock -> EnqueueNearLayerRetirement
    -> LandblockRetirementStage.MeshReferences
    -> GpuWorldState.ReleaseLandblockMeshReferences
    -> LandblockSpawnAdapter.OnLandblockUnloaded  => WantsLoaded = false

while DetachNearLayer deliberately keeps the landblock loaded, terrain-mesh
resident, terrain-collision resident and DRAWN. Nothing re-publishes an
already-loaded landblock, so the demoted member satisfied NEITHER arm of
the gate, permanently: wormhole tunnel plus centered "In Portal Space -
Please Wait..." forever, no recovery short of relog.

Reachable by ordinary play. Two consecutive recalls to the same landblock
with walking in between makes ChangesStreamingCenter false, so there is no
origin recenter and the region recentres through the ordinary demote diff.
Also reachable via a mid-hold quality-preset drop -- ironically the exact
scenario ReconcileDestinationReservationRadius was added to support. The
pre-#280 radius-1 gate never touched that band, because nothing inside the
Near ring can demote.

FIX SHAPE. Make the two routes genuinely equivalent rather than teaching
the predicate to tolerate the difference. ReleaseLandblockMeshReferences
becomes "reconcile the registration to the post-retirement tier": after the
release converges, if the landblock is still loaded AND still Far tier,
re-assert the empty registration -- the identical OnLandblockLoaded(lb,
empty) a PublicationKind.Far activation makes. It is empty by construction:
DetachNearLayer retains only live server projections, which the adapter's
atlas-tier filter skips. A full retirement is unaffected (DetachLandblock
clears both _loaded and _tierByLandblock), and a throwing release still
retries because the re-assert is only reached after the adapter converged.

The alternative -- "|| (IsFarTier && IsLoaded)" at the gate -- was
rejected: it fixes one caller while leaving IsRenderReady meaning two
different things, which is precisely how this defect arose. After this
change the predicate reads "drawable at its current tier" for every caller,
with no knowledge of how the landblock got there.

WHY THE TESTS MISSED IT, fixed here too:

- Proof obligation P2 was discharged against RESIDENCY (the FarRadius+2
  eviction threshold) rather than against IsRenderReady, the gate's actual
  atom. The contract now carries the correction and the restated
  obligation: no transition may REVOKE IsRenderReady from a landblock that
  stays inside FarRadius.
- WorldRevealDerivedWindowIntegrationTests advertised itself as end-to-end
  against the real GpuWorldState but constructed it with no spawn adapter,
  so its IsRenderReady degenerated to IsLoaded via the "?? true". The
  single most load-bearing predicate in the change was stubbed out by a
  null in the test named after it -- the same shape as C5b's D3 and #276's
  three settler tests. Every fixture in that file now owns a real
  LandblockSpawnAdapter.
- The P1 test's comment described its subject as "a Near-shaped completion
  the streaming window has since DEMOTED to Far". It is not; it is a fresh
  PublishAsFar, the case that does hold. Corrected, since a future reader
  would have taken it as demote coverage.

Four new regression tests, all driving the real GpuWorldState +
LandblockSpawnAdapter + LandblockPresentationPipeline through an actual
demote, and all sabotage-verified in both directions (fail with the
production change reverted, pass with it):

  NearToFarDemote_LeavesTheLandblockRenderReadyThroughTheRealPipeline
  NearToFarDemote_LeavesTheLandblockRenderReadyUnderBudgetedRetirement
  TieredWindow_StaysResidentAfterAnOuterRingDemote
  OutdoorReveal_SurvivesAnOuterRingDemoteDuringTheHold

The budgeted variant exists because production composes
LandblockRetirementCoordinator.CreateBudgeted, whose MeshReferences stage
is a separate call site from the legacy pipeline's.

SECONDARY, same commit:

- R-1: ACDREAM_PROBE_REVEAL_RADIUS=0 was parser-accepted and
  Runtime-rejected -- it yields far = 0 for an outdoor destination, which
  fails invalid-readiness-shape on every acknowledgement, hanging the very
  A/B route the probe exists to measure. Parser floor raised to 1, with a
  7-case table test.
- R-2: the composite-warmup TRIGGER had silently moved onto the far
  window's critical path. Pre-#280 the gate and the composite domain were
  the same radius-1 square; #280 widened the gate without widening the
  domain, so every composite upload serialised behind the last outer-ring
  landblock for no readiness benefit. Warmup now starts once the NEAR
  sub-window is published -- trigger scope == domain scope, as before. The
  reveal gate is untouched: Evaluate still requires the full window AND
  composite readiness.
- AP-150 filed: acdream's RetailWaitCueDelay = 5 s arming is NOT retail's
  trigger, and #280's commit message got this wrong on both clauses. Retail
  emits the notice unconditionally per tunnel rotation segment, in the else
  arm of the segment-expiry test at 0x004D6FCD; segment duration is
  RandDouble(0.6, 1.8) s, byte-decoded at 0x004D6FE6. The 5.0 constant at
  VA 0x007991B0 is CellManager::CheckPrefetchStatus's prefetch RETRY
  cadence and has nothing to do with the cue. acdream's own 0.6/1.8 segment
  constants already match retail exactly; only the arming is wrong.
  Adopting retail's unconditional emit is filed as #329 rather than folded
  in here -- it is a user-visible presentation change and wants the user's
  eyes.
- AP-151 filed: the gate is materially STRICTER than retail on the
  mesh-build/GPU-upload axis. Retail's LScape::PreFetchCells blocks on DAT
  RESIDENCY only -- no geometry construction, no upload; that work is lazy
  at draw. acdream requires a DAT read, terrain mesh build, render-thread
  upload, spatial commit, collision admission and spawn-adapter activation
  per member of a 625-member window, metered at MaxCompletionsPerFrame.
  Nothing bounds the hold. This is the OPPOSITE asymmetry from AP-149; both
  are live at once, on different axes.
- AD-2's amendment stated the false Far-tier readiness assumption verbatim;
  corrected, along with the same error in
  claude-memory/reference_two_tier_streaming.md, which now carries an
  explicit DO-NOT-RETRY on the special-case-the-predicate shape.
- AP-115 scope-noted (it covers the cue's presentation, not its arming).
- #326's SmartBox::set_mid_radius citation corrected: the entry is
  0x00453180; 0x004531D0 is the mid-function re-arm branch.

Blast radius: GpuWorldState, LandblockSpawnAdapter,
WorldRevealReadinessBarrier and StreamingDiagnostics are all App-internal;
AcDream.Headless and AcDream.Runtime reference none of them outside
comments. Headless tests run green as part of the gate below, per C5b's
lesson about surveys that skip the no-window host.

Gates: Release build 0 errors, 18 pre-existing xUnit analyzer warnings.
Complete suite "dotnet test AcDream.slnx -c Release -m:1" with
ACDREAM_PAK_PATH set: 11,192 passed / 4 skipped / 0 failed, from a clean
rebuild (a prior session's deleted probe file had been compiled into a
stale test DLL). Baseline at fafc0b65 was 11,179 / 4 / 0; the +13 delta
reconciles exactly to this commit's additions -- 3 readiness tests, 1
integration test, 7 parser table cases, 2 warmup-trigger tests. None of the
known flakes #302/#308/#321 surfaced, and none is conflated with the
finding above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 07:27:28 +02:00

321 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# #280 architecture review — ownership, layering, blast radius, test quality
- **Commit under review:** `3aab05b0` — *fix(streaming): derive the portal
reveal window from the live streaming radii (#280)*
- **Branch / HEAD:** `claude/acdream-physics-divergence-5aa784` @ `fafc0b65`
- **Worktree:** `.claude/worktrees/peaceful-visvesvaraya-e0a196`
- **Scope:** ownership, layering, blast radius, test quality. Retail fidelity
is a separate reviewer's.
- **Mode:** read-only. Production edits were made only as sabotage probes and
reverted; the tree is clean at `fafc0b65` with no working-tree changes.
## Verdict: **FAIL**
One confirmed defect, reproduced end-to-end through production code paths.
The predicate D2 widened from a 3×3 always-Near neighbourhood to the full Far
window now demands `IsRenderReady` from landblocks that the streaming system
**permanently** takes out of render-readiness while leaving them loaded. There
is no code path that restores them. Under two ordinary player flows — one of
which is the exact mid-hold radius change this commit added
`ReconcileDestinationReservationRadius` to support — the reveal gate becomes
unsatisfiable and the client stays in "In Portal Space - Please Wait…"
forever.
Everything else checked out. Layering held, the no-window host is genuinely
untouched, the allocation fix is real, and the tests are unusually well
disciplined about not re-encoding the constant under test. The failure is a
single missed state transition, not sloppy work — but it is the failure mode
the change's own proof obligation P2 was written to exclude, discharged
against the wrong predicate.
---
## Defects
### D-1 — A demoted-but-loaded landblock is permanently not `IsRenderReady`, so the widened gate can never open (**CONFIRMED, reproduced**)
**Severity: defect. Blocks the fix.**
`src/AcDream.App/Streaming/StreamingController.cs:270-278` — the new outer arm
requires `_state.IsRenderReady(canonical)` of every in-bounds member out to
`farRadius`.
`IsRenderReady` is not a property of "loaded":
- `src/AcDream.App/Streaming/GpuWorldState.cs:179-181`
`IsRenderReady = _loaded.ContainsKey(id) && _wbSpawnAdapter.IsLandblockRenderReady(id)`
- `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs:135-139`
returns **false** when the landblock has no registration, or a registration
with `WantsLoaded == false`.
A Near→Far **demote** destroys exactly that registration while keeping the
landblock loaded:
- `src/AcDream.App/Streaming/StreamingController.cs:555-559`
`DemoteLandblock``LandblockPresentationPipeline.EnqueueNearLayerRetirement`
- `src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs:309-312`
a `NearLayer` ticket still runs `CoreStages`, which includes
`LandblockRetirementStage.MeshReferences`
- `src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs:738-740`
`GpuWorldState.ReleaseLandblockMeshReferences`
- `src/AcDream.App/Streaming/GpuWorldState.cs:1559-1560`
`LandblockSpawnAdapter.OnLandblockUnloaded``WantsLoaded = false`,
registration dropped
- `src/AcDream.App/Streaming/GpuWorldState.cs:1800-1835`
meanwhile `DetachNearLayer` **keeps** `_loaded[canonical]` and flips
`_tierByLandblock[canonical] = Far`
Nothing re-publishes an already-loaded landblock. `RecenterTo`
(`StreamingRegion.cs:232-244`) emits `ToLoadFar` only for ids absent from
`_tierResidence`, and a demoted block is still present as `Far`. Only a
**promote** back into the Near ring (`StreamingRegion.cs:239-244`
`PublicationKind.PromoteExisting``ActivateLandblockPresentation`
`OnLandblockLoaded`) restores the registration. Members of the band
`NearRadius+1 … FarRadius` are never promoted, so they stay dead for the rest
of the window's life.
**Evidence (temporary probes, run then removed):**
| Probe | Result |
|---|---|
| `GpuWorldState.IsRenderReady` after `LandblockPresentationPipeline.BeginNearLayerRetirement` (the production demote entry point) | **false** |
| `GpuWorldState.IsRenderReady` after `GpuWorldState.RemoveEntitiesFromLandblock` (same `DetachNearLayer` + `ReleaseLandblockMeshReferences` pair) | **false** |
| Full Near 5×5 window; `controller.IsRenderNeighborhoodResident(dest, 1, 2)` before one outer-ring demote | **true** |
| …the same call after that single demote | **false**, and never recovers |
All three probes used a real `GpuWorldState` + real `LandblockSpawnAdapter` +
real `LandblockPresentationPipeline`.
**Reachable failure scenarios**
1. **Mid-hold quality change (the one this commit explicitly added code for).**
`StreamingController.ReconfigureRadii` at `:484-488` emits
`DemoteLandblock` for every currently-Near landblock that the new preset
puts in the Far band. Presets are High `(4,12)` → Low `(2,5)`
(`src/AcDream.UI.Abstractions/Settings/QualityPreset.cs:31-34`), so a
High→Low switch demotes the Chebyshev 35 band, which is *inside* the new
Far window. The gate then requires `IsRenderReady` from all of them.
Result: the hold never ends, and
`WorldRevealCoordinator.ReconcileDestinationReservationRadius`
(`:521-543`) cheerfully re-opens a reservation on a square that can no
longer converge. Note also that `NearRadius`/`FarRadius` only publish at
transaction convergence (`StreamingController.cs:886-888`) while the
demote mutations run earlier (`:860-876`), so for several frames the
barrier measures the OLD wide window against blocks already demoted for
the NEW narrow one.
2. **A reveal that does not recenter the origin.**
`LocalPlayerTeleportController.cs:934` only calls `_streaming.BeginRecenter`
when `transition.ChangesStreamingCenter`, i.e. when the destination
landblock differs from the **world-origin** landblock. Origin recenter is
the only thing that clears the window (`GpuWorldState.cs:1481`
`_loaded.Clear()`). Walking ≳ `NearRadius + 2` landblocks from the last
origin (`StreamingRegion.cs:263-270`) demotes blocks into the Far band;
a subsequent teleport back to the origin landblock (lifestone recall →
travel → lifestone recall) takes the no-recenter path and inherits them.
Blocks that land back inside the Near ring are promoted and recover;
blocks in `near+1 … far` do not.
**Why the old gate was safe:** at `OutdoorNeighborhoodRadius = 1` the required
set was the destination's own 3×3, which during a hold is pinned to the
streaming centre (`StreamingFrameController.SelectObserver:194-200` freezes the
observer at `_origin` while `PlayerState.PortalSpace`) and therefore always
Near tier. Demotion inside the Near ring cannot happen. The defect is
introduced by this commit, not exposed by it.
**Why P2 missed it.** `docs/research/2026-08-05-280-contract.md:876-884` states
P2 as "an outer-ring member cannot be **evicted** while it is inside
`FarRadius`" and cites the `FarRadius + 2` unload threshold. That is true and
irrelevant: the gate's atom is `IsRenderReady`, not residency. P2 was
discharged against the wrong predicate. Every remaining sentence of the
static proof (hysteresis, recenter, dungeon collapse) is correct.
**Also note:** the P1 test's own comment
(`tests/AcDream.App.Tests/Streaming/StreamingControllerReadinessTests.cs:567-570`)
describes its subject as *"A Near-shaped completion that the streaming window
has since demoted to Far"*. It is not — it is a fresh `PublishAsFar` of a
landblock that was never loaded, which is the case that **does** hold. The
comment names the one case that is false. A future reader will take it as
coverage of the demote path.
---
## Latent risks
### R-1 — `ACDREAM_PROBE_REVEAL_RADIUS=0` is accepted by the parser and rejected by Runtime
`src/AcDream.App/Streaming/StreamingDiagnostics.cs:54-55` accepts any
`value >= 0`. `ApplyRevealRadiusOverride` then yields
`StreamingRevealWindow(0, 0)`, so `RequiredWindow` returns far = 0 for an
**outdoor** destination, and
`src/AcDream.Runtime/World/RuntimeWorldTransitState.cs:585-597` fails
`invalid-readiness-shape` on every acknowledgement (outdoor ⇒ `>= 1`). The
A/B probe would hang the route it is meant to measure. The documented value is
`1`; the parser should refuse `0` rather than let the two halves of the same
commit disagree. One line.
### R-2 — the composite-warmup **trigger** moved onto the far window's critical path, undocumented
`WorldRevealReadinessBarrier.Prepare:120-133` starts composite preparation only
once `_isRenderNeighborhoodReady(dest, near, far)` is true. Before this commit
that predicate was the destination's 3×3; it is now the entire 25×25. The
commit message and D3 justify keeping the composite **radius** at `NearRadius`
(correct — Far builds carry no entities), but say nothing about the trigger.
Net effect: composite upload can no longer overlap far-ring streaming; the
hold is longer than the streaming work alone requires by the whole warmup
duration. This is a real serialisation the "expect longer holds" paragraph
does not account for, and it is separable — warming composites at Near-ready
would restore the overlap without weakening the gate.
### R-3 — two different windows describe "destination publication incomplete"
`StreamingController.cs:719-724` computes the destination-lane preference with
`(Math.Min(NearRadius, DestinationRadius), DestinationRadius)` — live
`NearRadius` against the *reservation's* radius — while the gate uses
`RequiredWindow`'s `(clamp(NearRadius, 0, FarRadius), FarRadius)`. Between a
preset change and reservation reconciliation these name different squares, so
the work-lane prioritisation can consider the destination complete on a
square the gate still rejects. Cosmetic today; it becomes a real starvation
question once R-1/D-1 are fixed and holds get long.
### R-4 — Runtime lost its only cross-host consistency check
The `indoor ? 0 : 1` equality became `indoor ⇒ 0`, `outdoor ⇒ ≥ 1`
(`RuntimeWorldTransitState.cs:585-597`). The layering argument is right —
Runtime cannot learn the graphical host's streaming configuration, and
plumbing App radii in would be exactly the C5b failure. The cost is that
Runtime can no longer detect an App regression to a hardcoded radius: a
future revert of D1 would emit `1` and pass. Accepted, but worth recording
that the invariant is now shape-only and the only remaining guard on the
value is the App-side tests.
---
## What was checked and is sound
**Layering / ownership (attack 6) — held.**
`WorldRevealCoordinator` and `WorldRevealReadinessBarrier` are `internal` to
`AcDream.App`. `src/AcDream.Headless/AcDream.Headless.csproj` references only
`AcDream.Runtime`. Both non-graphical producers
(`HeadlessSessionWorldProjection.cs:979`,
`RuntimeLiveEntitySessionController.cs:782`) keep emitting their own
centre-ring token and are legal under the loosened shape by construction, and
both were annotated in place explaining why they must **not** track the
graphical radius. No App type crossed into Runtime. `AcDream.Headless.Tests`
passes 89/89 including its dependency/loaded-assembly guards. C5b's lesson was
applied, not repeated.
**P1 (attack 2) — independently verified TRUE, by reading rather than by
reading the test.** `PublishAsFar` (`LandblockPresentationPipeline.cs:432-522`)
constructs a `LoadedLandblock` with `Array.Empty<WorldEntity>()` and
`PhysicsDatBundle.Empty`; the spatial commit
(`LandblockPresentationPipeline.cs:900-916`) routes `PublicationKind.Far`
through `CommitLandblockSpatial`, which returns a publication with
`RequiresActivation` defaulted true (`GpuWorldState.cs:24`), so
`ActivateLandblockPresentation` (`:1009-1035`) calls `OnLandblockLoaded` with
an empty entity list and empty `AdditionalRenderIds`. The registration is
created with `WantsLoaded = true` and both reference dictionaries empty, so
`IsLandblockRenderReady` returns true with no `IWbMeshAdapter` upload. The one
early-out (`GpuWorldState.cs:913-923`, a stale Far completion over a live Near
tier) returns `RequiresActivation: false`, but that case is already
registered. **P1 holds. The fix shape does not collapse.** Its failure is D-1,
a different transition.
Same check for the collision arm, which is equally load-bearing and was not
called out as a proof obligation: `LandblockPhysicsPublisher.AdvanceBeginOne`
(`:390-435`) builds the terrain surface from the heightmap with no dat bundle
and stages it, so a Far publication does register terrain collision;
`AdvanceDemotion` (`:675-683`) calls `DemoteCollisionToTerrain`, which
**retains** terrain. `IsNeighborhoodTerrainResident` therefore stays satisfiable
across a demote. The render arm is the only one that breaks.
**Reveal-hang sweep (attack 1) — all other configurations converge.**
| Configuration | Result |
|---|---|
| Map edge (coords clamped 0/254) | Safe. Gate skips `> 254` (`StreamingController.cs:265-266`); `StreamingRegion` loads up to `0xFF` (`:80`). The loaded set is a strict superset of the required set, and `PhysicsEngine.cs:157` uses the identical bound. |
| Dungeon → outdoor | Safe. `TryCommitOriginRecenterCore:1235-1251` clears `_collapsed` for a non-dungeon destination and nulls `_region` so the next tick bootstraps the full window. |
| Indoor destination | Safe. `RequiredWindow` returns `(0,0)` before touching the live window (`WorldRevealReadinessBarrier.cs:206-215`), so no streaming state can gate it. |
| Recenter in flight | Safe. `Tick` is blocked while `_originRecenterRetirement` is open, radii requests defer, and `_loaded.Clear()` (`GpuWorldState.cs:1481`) guarantees every member of the rebuilt window gets a fresh publication and registration. |
| Destination/observer landblock offset | Safe **during a hold**: `StreamingFrameController.SelectObserver:194-200` pins the observer to `_origin` while `PlayerState.PortalSpace`, so the gate's inner ring and the streaming Near ring are the same square. Worth noting the new inner arm has **zero** margin here where the old one had `NearRadius 1`; the pin is now load-bearing. |
| `FarRadius == 0` on an outdoor destination | Not reachable from presets (min far = 5); reachable only via R-1. |
**D6 allocation fix — real, and the sabotage number is honest.**
`PhysicsEngine.cs:49-52,146-149`. Single production call site
(`SessionPlayerComposition.cs:384`), main thread, leaf method — the
instance-owned scratch is safe, and it is cleared at entry so no stale state
can leak across sessions.
**Test discipline — good, with two gaps.** No test re-encodes the constant
under test: every radius assertion references the fake window's own input
(`WorldRevealReadinessBarrierTests`, `WorldRevealDerivedWindowIntegrationTests`).
No new `Skip=`. No test deleted — one was renamed
(`OutdoorReveal_JoinsNearRenderTexturesAndTerrain`
`RequiredWindow_IsRereadOnEveryEvaluationWithoutReconstruction`) with its
original assertions carried into a new
`OutdoorReveal_JoinsRenderTexturesAndTerrainOverTheDerivedWindow`; coverage is
preserved and the +36 arithmetic holds.
Gaps:
- No test exercises a demote. That is D-1.
- `WorldRevealDerivedWindowIntegrationTests` advertises itself as end-to-end
against "the REAL `StreamingController`, `GpuWorldState`, and
`PhysicsEngine`", but constructs `new GpuWorldState()` with no spawn
adapter, so `IsRenderReady` degenerates to `IsLoaded` via the
`?? true` at `GpuWorldState.cs:181`. The single most load-bearing predicate
in the whole change is stubbed out by a null in the test named after it.
It also builds its own `revealWindow` lambda rather than the production one
in `SessionPlayerComposition.cs:373-379`, so the
`ApplyRevealRadiusOverride` wrapper is never exercised in composition.
**Sabotage spot-checks — 4 of 9 families reproduced (asked for ≥ 3).**
| Sabotage | Named test(s) | Result |
|---|---|---|
| `PhysicsEngine`: scratch → `new HashSet<uint>()` | `WarmedNeighborhoodQuery_AtTheFarRadius_AllocatesNothing` | FAIL, **"allocated 27,712,000 bytes"** — S5's claimed figure reproduces to the byte |
| `StreamingController`: revert tier split to `!IsNearTier \|\| !IsRenderReady` | `TieredWindow_OuterRingFarTierMemberIsResident`, `TieredWindow_AbsentOuterRingMemberIsNotResident`, `TieredWindow_MapCornerDestinationConvergesAtAWideRadius`, `OutdoorReveal_HoldsUntilTheWholeDerivedWindowIsPublished` ×2, `LoginReveal_UsesTheSameWidenedGateAsPortalArrival` | 6 FAIL |
| `RuntimeWorldTransitState`: shape → `== (isIndoor ? 0 : 1)` | `OutdoorReadinessShape_AcceptsAnyDerivedStreamingRadius` | 6 FAIL (all inline radii) |
| `WorldRevealCoordinator`: neuter `ReconcileDestinationReservationRadius` | `MidHoldRadiusChange_ReopensTheReservationOnTheSameGeneration` | 1 FAIL |
All four reverted; `git status` clean afterwards. No sign of the C5b-D3 class
(a test that passes with its own change reverted) in the families checked.
**Suite at HEAD `fafc0b65`, Release, verified by running it:**
`11,179 passed / 4 skipped / 0 failed` — exactly the expected
11,178 + 1 Core settler test. Per project: UI 546, Content 124, Runtime 1217,
Cli 4, Bake 15, App 4157/3 skips, Headless 89, Core.Net 764, Core 4263/1 skip.
None of #302/#308/#321 surfaced in this run and none are conflated with the
finding above.
**Process rules — clean.** No suppression flag, grace period, retry loop, or
symptom guard was introduced. `ACDREAM_PROBE_REVEAL_RADIUS` lives in a
diagnostic owner per Code Structure Rule 5 and is correctly argued as a
measurement probe rather than a shipped knob. `AD-2` was amended and `AP-149`
filed in the same commit, satisfying the divergence-register rule. The
`ACDREAM_STREAM_RADIUS` CLAUDE.md correction is accurate against
`SessionPlayerComposition.ComposeCore:249-257`.
---
## Recommended disposition
Do not ship the gate widening until D-1 is closed. The two shapes worth
considering, in preference order:
1. **Make a demote re-register the Far tier.** The demote already leaves a
fully valid Far-tier landblock behind; the registration it drops is the
*Near* mesh set. `LandblockRetirementStage.MeshReferences` on a `NearLayer`
ticket should release the Near references and then re-assert an empty
Far registration, exactly as `PublishAsFar` does — i.e. the same
`OnLandblockLoaded(landblock, empty)` call the Far publication makes.
This makes `IsRenderReady` mean "drawable at its current tier", which is
what both the render path and the gate already assume it means.
2. **Re-publish demoted members inside a destination reservation.** Weaker:
it fixes the gate without fixing the predicate, and leaves the next caller
of `IsRenderReady` holding the same trap.
Whichever is chosen, it needs a test at the demote transition —
`Near publish → demote → IsRenderNeighborhoodResident(dest, near, far)` — and
the `WorldRevealDerivedWindowIntegrationTests` fixture should be given a real
`LandblockSpawnAdapter` so its `IsRenderReady` stops being a tautology.
R-1 and R-2 are one-line and one-decision respectively and can ride along.