Skip to content

fix(#753): make the floating exemption unable to bypass floor-memo invalidation - #834

Merged
djhenry merged 5 commits into
mainfrom
fix-753-floor-memo
Aug 1, 2026
Merged

fix(#753): make the floating exemption unable to bypass floor-memo invalidation#834
djhenry merged 5 commits into
mainfrom
fix-753-floor-memo

Conversation

@djhenry

@djhenry djhenry commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Closes #753.

The bug

smooth_entity_motion's floating-entity floor-snap exemption was a pure early-exit:

if b.floating {
    // Boats/ships float on the water surface: keep their server-sent z, do NOT snap...
} else {
    match collision {
        Some(col) => { /* raycast + snap */ }
        None => m.floor_at = [f32::NAN; 3], // invalidate the memo across a zone reload
    }
}

Whenever b.floating was true, the whole else — including the None arm's explicit
m.floor_at = [f32::NAN; 3] invalidation — was unreachable, no matter what collision did.
A zone reload always drives self.collision through None before the new zone's Some(new)
lands (src/app.rs, the zone_needs_reload path). So a reload that happened to land while an
entity was floating left the memo cache pointed at the OLD zone's geometry, invisibly.

Verified at HEAD: floating() is genuinely dynamic

The issue's premise — that this is reachable via more than boats — is confirmed by reading
crates/eqoxide-core/src/game_state.rs:192, Entity::floating():

pub fn floating(&self) -> bool {
    crate::coord::skips_wire_z_offset(self.is_boat, self.flymode)
}

flymode is documented (and, per that doc comment, refreshed at runtime by
OP_SpawnAppearance type-19) as re-derived every call — not a one-time spawn classification —
per #578. So a levitate toggle flips floating() mid-session, same as a boat ride. I did not
re-verify the OP_SpawnAppearance wiring itself (out of scope); I read the doc comment and the
function body, which is enough to confirm the premise the bug needs (that b.floating can be
true at an arbitrary time, not only for boat entities at spawn).

Independently re-verified by the reviewer at source (crates/eqoxide-net/src/packet_handler.rs,
crates/eqoxide-renderer/src/scene.rs, src/app.rs) during round-2 review — the AT_FlyMode (19)
handler writes e.flymode, Billboard::floating is rebuilt from e.floating() every snapshot,
and the scene is rebuilt every frame. Established, not just read, for remote entities.

Judgement call 1 — what to invalidate, and when

The issue's own suggestion ("invalidate whenever b.floating is true") is a second guard next
to the first one that was already forgotten once. I didn't take it. Instead I restructured so
there is a single match collision that always runs, regardless of b.floating:

match collision {
    Some(col) => {
        if !b.floating {
            if b.pos != m.floor_at { /* raycast + memoize */ }
            b.pos[2] = m.floor_z;
        }
        // floating: keep server z, write-free (see comment at the call site for why)
    }
    None => m.floor_at = [f32::NAN; 3], // now unconditionally reachable
}

b.floating now only gates whether the Some(col) arm applies the snap (the #194 boat
behavior — ride the server-sent z). It can no longer gate whether the None arm's invalidation
runs, because there is no floating-conditioned branch wrapped around the match anymore — the
bypass is structurally impossible, not guarded against. This is the "make the bad state
unrepresentable" option from the brief, not the third-guard option.

I considered and rejected two alternatives:

  • Invalidate whenever b.floating is true (the issue's suggestion): works, but is exactly
    the "add a guard" shape that produced this bug in the first place — a second place that has to
    remember the contract the first place already forgot.
  • Invalidate on the floating→grounded transition: requires tracking the previous frame's
    floating state per entity (a new EntityMotion field) to detect the edge. More machinery for
    no more coverage than the match restructure gives for free, since the restructure already
    makes the None-arm invalidation unconditional.

Judgement call 2 — not blocked on #194

The fix is separable from #194's boat mechanics. The change is entirely about when the memo
cache is invalidated
; it does not touch what z a floating entity is given (still the
server-sent z, untouched), how boats are classified, or anything else #194's still-open
gap-1/gap-3 work would plausibly change. I did not expand scope into #194 and did not need to —
confirming the owner's inference in the issue.

Test

Added floating_across_a_zone_reload_does_not_resurrect_the_old_zones_floor in
src/app.rs's mod tests, driving the exact call-site sequence with b.pos held
bit-identical throughout (so the only things that change are b.floating and collision,
matching the bug's actual trigger shape):

  1. Grounded on col_a (floor z=-3) — caches the snap. col_a's height is deliberately
    non-zero (round-1 review finding 3): EntityMotion's own zero-init for floor_z is also
    0.0, so a col_a at z=0 couldn't tell "served the stale col_a raycast" apart from "served the
    never-initialised default" from the failure value alone.
  2. Floating starts exactly as a zone reload drops collision to None.
  3. Still floating, the new zone's col_b (floor z=5) arrives.
  4. Lands (floating clears) at the same position — must re-raycast against col_b (z=5), not
    resurrect the pre-reload col_a value (z=-3).

After step 1, assert_eq!(motion[&9].floor_at, p, ...) pins the bit-identity the test's
discriminating power depends on (round-1 review finding 4), rather than relying on it silently.

Mutation-checked (call-site mutation, not a body-wrap):

  • Reintroduced the exact original structure (if b.floating { } else { match collision { ... } }
    at the call site): RED, new test only.
  • None => {} — delete only the invalidation, keep the restructure: RED, new test only, failure
    now self-attributing (got z=-3.000001, unambiguous vs. the zero-init default) thanks to the
    col_a=-3.0 fix above.
  • All mutations reverted from a cp -p copy taken before mutating, md5sum-verified identical,
    touched for cargo — never git checkout/git restore.

Orthogonal / adjacent-value checks (SURVIVING is the correct signature):

  • Bumped an unrelated constant (MAX_UPD, governs the update-pace estimate, not the floor-snap
    memo) — new test still passed, not spuriously coupled to unrelated code in the same function.
  • Reviewer's two adjacent controls (same match, same concern, genuinely plausible alternate
    designs, not an unrelated constant): also invalidating in the Some arm while floating, and
    memoizing floor_at/floor_z while floating (gating only the b.pos[2] = write) — both
    SURVIVE, confirming the test pins the invalidation this fix is about, not the shape of the
    refactor.

Review history

Two independent-review rounds on this PR, both by the same reviewer (fleet shares one GitHub
identity; verdict posted as a PR comment, not a formal approval — see the fleet's review process).

Round 1 — CHANGES REQUESTED, four findings addressed, all comment/test-text, no
production-code behavior change:

  • Two MEDIUM findings: a comment above match collision stated an unmeasured "consequence
    clause" as fact (fixed — now explicitly marks it NOT measured, and names the two mechanisms,
    motion.retain and begin_zone_in's entity purge, that the reviewer found sit between "the
    invalidation never ran" and an actual stale serve); and a "deliberately don't write
    floor_at/floor_z" comment claimed a hazard the reviewer measured unpinned and this fix's own
    unconditional None arm forecloses anyway (fixed — states the real rationale: diff
    minimalism, preserving pre-smooth_entity_motion: floating-entity floor-snap exemption leaves the memo cache stale instead of invalidating it #753 behavior).
  • Two LOW findings: col_a moved from 0.0 to -3.0 (self-attributing failure value); pinned
    the test's bit-identity assumption with an explicit assert_eq!.

Round 2 — one LOW, non-blocking finding, fixed before merge rather than filed: the
match collision comment claimed [f32::NAN; 3] "appears nowhere else in this file" —
false; it also appears at the motion.entry(..).or_insert_with(..) initialiser. Not pedantry:
entry re-creation is itself a second invalidation path (NaN != anything), and it's the exact
path the next paragraph (motion.retain) already leans on. Fixed with the reviewer's verbatim
correction: the only other occurrence is the entry initialiser, which invalidates only on
entry creation; nothing else invalidates a live entry.

Round 2 verdict: APPROVED. The reviewer checked both round-1 replacement blocks clause by
clause against their surrounding paragraphs (not just the changed lines), found no subtler
falsehood swapped in, upgraded two claims from inference to measurement (the begin_zone_in
ordering traced through both production callers; begin_zone_load's single production caller
with the other eight confirmed #[cfg(test)]), and reran the round-1 call-site mutations on the
merged tree to confirm they still discriminate.

Five figures (workspace cargo test --workspace --locked --no-fail-fast, remote builder,

stdout/stderr captured separately, current head)

  1. Compile sentinel: Finished `test` profile [unoptimized + debuginfo] target(s) in 6m 22s.
    Completion confirmed by process exit (ps -p gone) and a complete final test result: line
    — the sentinel alone is not treated as run-completion.
  2. ^running [0-9]+ tests? headers: 55; ^test result: lines: 55 (equal — no lost binary).
  3. Non-canonical ^test result: lines: 0.
  4. Empty targets: 14, under both the anchored ^test result: ok\. 0 passed; 0 failed; 0 ignored
    predicate and the independent ^running 0 tests cross-check (same 14, not re-derived —
    established by two independent reviewers/runs, superseding this PR's earlier stale "16").
  5. 1839 passed + 0 failed + 47 ignored + 0 filtered = 1886, matching the summed running N tests header total of 1886. 0 failed.

This PR contributes exactly one new test to the suite
(app::tests::floating_across_a_zone_reload_does_not_resurrect_the_old_zones_floor); the
remaining delta versus earlier runs in this PR's history is unrelated commits merged from main
in the interim (most recently #840), not this diff.

NOT verified, and not claimed

  • No live reproduction. Like the issue itself, this fix is source-derived, not
    live-reproduced. I did not run a live client to trigger a levitate-toggle-during-zone-reload
    and observe a wrong z before the fix, or a correct one after. The severity is genuinely low
    (narrow trigger — position must land back on the exact cached value) so a live repro is
    disproportionate effort for this fix; the unit test exercises the exact call-site sequence
    instead.
  • Coalescing / ArcSwap snapshot behavior — whether the render thread can skip an
    entity-free snapshot window during a zone swap is reasoned, not measured, by either the author
    or the reviewer.
  • Whether an id from zone B can collide with a still-memoized id from zone A in practice
    not measured.
  • Whether any other call site of smooth_entity_motion's pattern exists elsewhere in the
    codebase
    — not searched.
  • Round-2 mutation re-runs: I only re-ran the None => {} mutation myself on the merged
    tree; the rest of the round-1 mutation table (structural revert, if !b.floating inversions,
    the two adjacent-design controls) was re-verified by the reviewer, not independently re-run by
    me a second time.
  • Clippy/lint impact of either round's edits — not run separately; CI's test job passes.

…validation

smooth_entity_motion's floating-entity floor-snap exemption was a pure
early-exit: `if b.floating { ... } else { match collision { ... } }` skipped
the whole match, including the None arm's `m.floor_at = [f32::NAN; 3]`
invalidation, whenever an entity was floating. A zone reload always drives
`collision` through None before the new zone's Some(new) arrives, so a
reload landing while an entity was floating (levitate toggle, boat ride —
Entity::floating() is re-derived from the live flymode every frame, not a
one-time spawn flag, per #578) left the memo cache silently pointing at the
old zone's geometry. A later grounded frame at a bit-identical position
could then serve a z computed against collision that was no longer loaded.

Restructured so there is a single `match collision` that always runs,
regardless of `b.floating`; the floating flag now only gates whether the
Some(col) arm *applies* the snap (the #194 boat behavior — keep the
server-sent z), never whether the None arm's invalidation is reachable.
This makes the bypass structurally impossible rather than adding a second
"remember to invalidate when floating" guard next to the first one.

Added a regression test driving the exact call-site sequence (grounded ->
floating across a collision None/Some(new) transition -> grounded again at
the same position) and confirmed it fails without the fix (serves the
stale pre-reload floor) and passes with it.
@djhenry

djhenry commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

CHANGES REQUESTED

Independent review of #834 (head a22e3e5, base 0497f6b). I did not write this change. Measured on the merged tree — a22e3e5 sits directly on top of current origin/main (0497f6b is an ancestor), so the merged tree is the PR head and there is no conflict to report.

The code is right. Every claim I could break by mutation held up, including the two I most expected to fail. What I am blocking on is not the code — it is two mechanism claims written as fact into a tracked source comment, one of which I believe is false and the other of which omits the architecture that prevents the failure it describes. Per this repo's own history, that is the defect class that matters here.


A. Is the stated root cause the real pre-fix shape? — YES, confirmed

I read smooth_entity_motion at the parent commit myself (git show 0497f6b:src/app.rs, lines 2694-2712). The shape was exactly as claimed:

if b.floating {
    // comment only — empty body
} else {
    match collision {
        Some(col) => { /* raycast + snap */ }
        None => m.floor_at = [f32::NAN; 3],
    }
}

The floating arm's body is comment-only, so the entire else — including the None arm — was unreachable while floating. Confirmed the None arm was the only invalidation path: [f32::NAN; 3] appears exactly twice repo-wide, at src/app.rs:2592 (entry creation) and src/app.rs:2728 (the None arm). Nothing else writes floor_at.

Also confirmed the collision-swap premise, which the PR asserts but does not show: self.collision has exactly two production writers — src/app.rs:904 (Some, on load completion) and src/app.rs:1447 (None, on reload start) — and begin_zone_load has exactly one production caller (src/app.rs:1448). So Some(A) → Some(B) without an intervening None is not reachable, and the restructure closes every collision-swap path. Claim 3 (not blocked on #194) holds.

B. Does the restructure preserve the #194 boat behaviour? — YES, and it is defended

Verified by mutation at the call site, not by reading:

  • if !b.floatingif true (exemption removed, boats snap to the seabed): RED on floating_entity_keeps_server_z_and_grounded_one_still_snaps, and not on the new test. So a fix that traded the memo bug for a boat regression would have been caught.
  • if !b.floatingif b.floating (inverted): RED on 4 tests including both halves of the boat test.

C. Mutation table

Every mutation applied at the call site in src/app.rs (no body-wrap). Reverted from a cp -p pristine copy, md5sum-verified before each apply and after the last; never git checkout/git restore. Reach control: every row produced a distinct file md5 and a fresh Compiling eqoxide v0.1.0 line in its own build log, so a SURVIVING row is a real survival, not an unbuilt mutation. Command: test -p eqoxide --lib --locked --no-fail-fast app::tests (29 tests in scope).

# Mutation Result Tests red
M0 none (baseline) GREEN 29/29
M1 None => if !b.floating { m.floor_at = … } (minimal semantic revert — keeps the new structure, restores the old semantics) RED new test only
M2 None => {} — delete only the invalidation RED new test only
M2b M2 + diagnostic: move col_a from 0.0 to -3.0 RED, got z=-3.000001 new test only
M3 if !b.floatingif true RED floating_entity_keeps_server_z_and_grounded_one_still_snaps
M4 if !b.floatingif false RED 4: distant_entity_is_still_floor_snapped_for_labels, near_entity_floor_snaps_and_memoizes, both floating tests
M5 if !b.floatingif b.floating RED same 4
M6 adjacent control: also invalidate in the Some arm while floating (the issue's own suggested fix) SURVIVING
M7 adjacent control: memoize floor_at/floor_z while floating too, gate only the b.pos[2] = write SURVIVING
M8 structural revert to the exact claimed pre-fix shape RED new test only
M9 delete the b.pos[2] = m.floor_z; snap application not completed — build never finished (builder saturated); abandoned, confirmatory only

M1 and M2 are the rows the PR was missing. M2 is the important one: deleting only the invalidation, with the new structure otherwise intact, goes RED on the new test alone. The test is pinning the thing the PR is about, not merely the shape of the refactor.

M6 and M7 are the false-positive controls I was asked for — genuinely adjacent (same match, same concern, both plausible alternate designs) rather than an unrelated constant. Both SURVIVE, which is the correct signature: the test is not spuriously coupled to the neighbourhood. M7's survival is also a finding — see below.

D. Grading the test (not its name)

  • It drives the real discriminator. M2 RED proves b.pos really is bit-identical to m.floor_at at step 4 — that is the only way step 4 can serve a stale value at all.
  • The reported failure value is genuinely the stale floor, not noise. The test's col_a sits at z=0.0, which is also EntityMotion { floor_z: 0.0 }'s initialiser — so got z=-0.0000009536743 cannot, from the value alone, distinguish "served the stale col_a raycast" from "served the never-initialised default". I resolved this by measurement (M2b): moving col_a to -3.0 changes the failure to got z=-3.000001. The author's attribution is correct. But the test as written does not establish it — see finding 3.
  • Step 3 is inert. Post-fix, the Some(col) arm with b.floating == true executes nothing (its body is if !b.floating { … } plus comments), and pre-fix the whole block was skipped. So the test does not pin the None → Some(new) transition; it pins "None invalidates". That is the right invariant — just don't claim more for it. (Source-derived from an empty branch, not separately mutation-measured.)
  • It does not assert the bit-identity it depends on — see finding 4.

E. Claim 4 (levitate toggles) — the author marked this unverified; I verified it, and it holds

Source-derived, no live run:

  • crates/eqoxide-net/src/packet_handler.rs:2332-2335 — the AT_FlyMode (19) handler for id != gs.player_id does e.flymode = param as u8. The handler exists and writes the field.
  • crates/eqoxide-renderer/src/scene.rs:337floating: e.floating(), rebuilt per snapshot.
  • src/app.rs:1289self.scene = SceneState::from_game_state(…) runs every frame.

So Billboard::floating really is re-derived from live flymode every frame, and a remote entity's levitate toggle really does flip it mid-session. Claim 4 is established (for remote entities; the player's own billboard is constructed with floating: false, so the player is never the subject).


Findings, ranked

1. MEDIUM — src/app.rs:2698-2704: the comment's consequence clause is stated as fact and omits the drain that prevents it

The new comment says a reload landing while an entity floats meant:

A later grounded frame landing at a bit-identical b.pos then trusted m.floor_z, computed against the OLD zone's collision.

For that to happen, the EntityMotion entry must survive the whole Some(A) → None → Some(B) window. Three things sit between the two halves of that sentence, and none is mentioned:

  • src/app.rs:2732motion.retain(|id, _| live.contains(id)); drops the entry the first frame the entity is absent from the billboard list. This is measured, by the repo's own existing test distant_entity_is_not_glided_and_despawn_drops_state (src/app.rs:2831-2848, asserting motion.is_empty() after one absent frame).
  • crates/eqoxide-core/src/game_state.rs:1397begin_zone_in clears world.entities at the top of every zone-entry handshake, i.e. before OP_NewZone, therefore before self.collision = None at src/app.rs:1447.
  • src/app.rs:1289 — billboards are rebuilt from that state every frame.

So on a real zone change the memo map is drained before the collision goes None. The described end-to-end path additionally requires the render thread to never sample an entity-free snapshot (game_state_view is an ArcSwap load_full, so coalescing is possible) or a zone-B spawn to reuse a zone-A id, and then a bit-identical position. That is a materially narrower story than the comment tells.

To be clear about what I am and am not saying: the first half of the comment ("the invalidation never ran") is true and I verified it. The consequence clause is a reasoned, unmeasured mechanism claim presented as fact, in a tracked file. I have not measured the coalescing behaviour either — my counter-argument is source-derived too. The fix is still correct and worth landing as structural hardening; the comment should say what is demonstrated and hedge what is not.

Same applies to the PR body's "driving the exact call-site sequence" — the unit test's sequence (one id continuously live across the collision swap) is a sequence the production call site may not be able to produce.

2. MEDIUM — src/app.rs:2721-2723: a "deliberately, because " rationale that nothing pins and that this PR's own fix forecloses

Deliberately don't write floor_at/floor_z here either: memoizing a floor the entity was never actually snapped against would just be a subtler version of the same stale-but-plausible hazard this fix closes.

Measured: M7 does exactly what the comment forbids — memoizes while floating, gates only the b.pos[2] = write — and the whole suite stays green (29/29). Nothing defends the stated decision.

Reasoned: I don't think the named hazard exists. Under M7 the memo is refreshed from the current collision, and the only way a memo outlives its collision is the Some(A) → None → Some(B) swap, which this PR's own unconditional None arm now handles. The comment cites, as justification, the very hazard the same commit eliminates. Either substantiate it (a mutation or a test that goes red) or reduce the claim to what is true — that keeping the floating arm write-free preserves pre-#753 behaviour exactly and keeps the diff minimal.

3. LOW — src/app.rs:3168: col_a at 0.0 collides with EntityMotion::floor_z's initialiser

The failure value cannot distinguish "stale col_a" from "uninitialised default". I resolved it by measurement (M2b) and the attribution is right, but the test should carry that discrimination itself: set col_a = flat_collision_at(-3.0) (or any non-zero height) and adjust the step-1 precondition. Then the failure message's value is the proof.

4. LOW — src/app.rs:3197-3203: the test relies on bit-identity without asserting it

The discriminating power comes entirely from b.pos == m.floor_at at step 4. That is true today (M2 RED proves it), but it is incidentally true — it depends on the glide leaving m.display untouched. A future change that perturbs m.display by an epsilon would make this test pass vacuously (re-raycast for the wrong reason) while silently no longer pinning #753. Add assert_eq!(motion[&9].floor_at, p, …) after step 1.

5. INFO — the four aggregate figures reproduce exactly; the fifth does not

See below. Figure 4 in the PR body is 16; the measured value is 14.


Five figures (merged tree = PR head; test --workspace --locked --no-fail-fast, remote builder, stdout/stderr captured separately)

  1. Compile sentinel: Finished `test` profile [unoptimized + debuginfo] target(s) in 24m 01s. Run completion confirmed by process exit plus a complete final test result: line — the sentinel alone is not run-completion.
  2. running [0-9]+ tests? headers: 55. test result: lines: 55. Equal — no lost binary. (55 also equals the count of Running/Doc-tests lines on stderr.)
  3. Non-canonical test result: lines: 0.
  4. Targets with the full 0 passed; 0 failed; 0 ignored triple: 14. See next section.
  5. 1831 passed + 0 failed + 47 ignored + 0 filtered = 1878, matching the header sum of 1878. 0 failures.

Figures 1-3 and 5 reproduce the PR body exactly (the wall-clock differs only by builder load).

The 14-vs-16 question — resolved, and it is neither 16 nor the ignore-only hypothesis

The answer is 14, and 16 is a regex artifact.

An unanchored 0 passed; 0 failed; 0 ignored also matches lines beginning **2**0 passed; … and **1**0 passed; …. Exactly two such lines exist in this run, and they account for the entire 14→16 delta:

test result: ok. 20 passed; 0 failed; 0 ignored; …   <- crates/eqoxide-renderer/tests/shadow_caster_selection.rs
test result: ok. 10 passed; 0 failed; 0 ignored; …   <- crates/eqoxide-renderer/tests/shadow_shader.rs

Both targets are not empty — they ran 30 tests between them. I cross-checked the pairing independently: those two files contain exactly 20 and exactly 10 #[test] attributes.

Anchored full triple (result: ok\. 0 passed; 0 failed; 0 ignored) = 14, and this agrees exactly with the independent running 0 tests header count of 14:

  1. unittests src/main.rs
  2. unittests src/bin/render_model.rs
  3. unittests src/bin/crash_probe.rs
  4. unittests src/diagnose_glb.rs
  5. unittests src/validate_glb.rs
  6. Doc-tests eqoxide
  7. Doc-tests eqoxide_assets
  8. Doc-tests eqoxide_command
  9. Doc-tests eqoxide_crash
  10. Doc-tests eqoxide_nav
  11. Doc-tests eqoxide_protocol
  12. Doc-tests eqoxide_renderer
  13. Doc-tests eqoxide_telemetry
  14. Doc-tests eqoxide_ui

Anchored loose predicate (0 passed; 0 failed;, ignored allowed > 0) = 18 — the 14 above plus these four ignore-only targets:

  1. tests/asset_sync_live.rs0 passed; 0 failed; 1 ignored (1 #[test], ignored)
  2. tests/water_capability.rs0 passed; 0 failed; 5 ignored (5 #[test], all ignored)
  3. Doc-tests eqoxide_http0 passed; 0 failed; 2 ignored
  4. Doc-tests eqoxide_net0 passed; 0 failed; 1 ignored

So the ignore-only hypothesis is real but is not the explanation for 16: it produces a 14→18 spread, not 14→16. And the unanchored version of that same loose predicate gives 20. Four different numbers from the same run:

predicate count
running 0 tests headers 14
anchored full triple 14
unanchored full triple (the "16") 16
anchored loose (ignored may be > 0) 18
unanchored loose 20

Recommendation for whoever standardises this: anchor on result: (ok\|FAILED)\. 0 passed; (or just count ^running 0 tests$), and state which predicate the figure uses. Two of the five numbers above are measurement bugs, not disagreements about definition.

Log integrity: exactly one Finished `test` profile sequence and one Compiling eqoxide v0.1.0 line in the stderr capture; every mutation used its own output path; no path was reused by a retry.

Overlap with #837 (also touches src/app.rs)

Checked, since merge order has burned this repo before. No conflict, textual or semantic. #837's src/app.rs change is +34/-0 and is entirely a doc comment above zone_needs_reload (~line 2473); #834's hunks are at ~2691-2729 and ~3125-3204. No shared lines, no shared behaviour — #837 adds no executable code to src/app.rs at all.

The one adjacency worth naming: #834's correctness argument rests on self.collision passing through None on every zone swap, and #837 hardens ZoneAssetState in crates/eqoxide-nav/src/zone_assets.rs (+150/-1), which owns that slot. I grepped #837's full diff for any change to begin_zone_load, finish_zone_load or *collision_slot writes — none. Its change is a compile-time coupling between collision() and usability(), not a change to when the slot is cleared. So #834's premise survives #837 in either order. Merging #837 first only shifts #834's hunk context by 34 lines, which git resolves on context.


NOT verified (by me)

  • No live run. I did not reproduce the stale-z live, before or after. Everything above is source- or unit-test-derived.
  • The coalescing question in finding 1. Whether the render thread can actually skip the entity-free snapshot window (game_state_view ArcSwap load_full) is reasoned, not measured. I did not build an integration test for the Some(A) → None → Some(B) sequence at the real call site.
  • Whether an id from zone B can collide with a still-memoized id from zone A in practice. Not measured.
  • M9 (delete the snap application) had not finished when I wrote this; it is confirmatory only and changes none of the conclusions above.
  • Clippy / lint impact of the restructure. Not run separately; CI's test job passes.
  • Other similarly-shaped memoize/invalidate pairs elsewhere in the codebase — the author flagged this as unchecked. I checked one narrow proxy ([f32::NAN; 3] appears only in the two places in src/app.rs) but did not search for the pattern under other sentinels.

What would clear this

Findings 1 and 2 are comment-text edits — hedge the consequence clause in the #753 block to what is demonstrated (and mention motion.retain / begin_zone_in), and either substantiate or soften the "deliberately don't write here" rationale. Findings 3 and 4 are two-line test strengthenings I'd like but won't insist on. The code change itself I'd land as-is.

Verdict is a PR comment, not a GitHub review approval — the fleet shares one identity and GitHub blocks self-approval.

@djhenry

djhenry commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Addendum — base drift since I measured.

My run above was measured against origin/main = 0497f6b, which is what main was when I started; at that point a22e3e5 was a fast-forward on it, so the merged tree was the PR head exactly. main has since advanced three commits (716cd28, dfa11ba, 56b851f), so a22e3e5 is now behind.

Rechecked, so the review's scope claim stays honest:

  • None of the three touches src/app.rs. Changed files are under crates/eqoxide-nav, crates/eqoxide-renderer, crates/eqoxide-net, plus docs/http-api.md, src/bin/render_model.rs and tests/walker_sim.rs. So every finding, the whole mutation table, and the root-cause/boat-behaviour analysis are unaffected — they are all statements about src/app.rs and the collision-slot writers, none of which moved.
  • The five aggregate figures are now stale, and I am not restating them as current: the 1878-test total and the 55/55 target count were measured on a tree that is three commits behind. The 14-vs-16 empty-target finding is a property of the predicate, not of the tree, and stands regardless — but whoever re-measures at merge time should expect different totals.

Verdict is unchanged: CHANGES REQUESTED on the two tracked-comment claims, code approved as written.

djhenry and others added 2 commits August 1, 2026 08:07
…aims, strengthen regression test

Addresses PR #834 review (independent reviewer, CHANGES REQUESTED):

- Rewrite the #753 comment above `match collision`: drop the unmeasured
  "consequence clause" (a live end-to-end resurrection was never measured),
  and explicitly name the two other mechanisms that also touch this cache
  (motion.retain's per-frame purge, begin_zone_in's entity purge) as
  unaddressed by this fix, rather than implying the restructure alone closes
  the whole hazard.

- Rewrite the "deliberately don't write floor_at/floor_z" comment in the
  floating arm: the reviewer measured the named hazard unpinned (mutation M7
  survives, whole suite green) and the fix's own unconditional None arm
  forecloses it anyway. State the real rationale (diff minimalism, preserve
  pre-#753 behavior) instead of a claim nothing here actually tests.

- Test: col_a moved from flat_collision_at(0.0) to flat_collision_at(-3.0) so
  a stale-serve failure is distinguishable from EntityMotion's own
  zero-initialized floor_z (LOW finding 3).

- Test: pin the bit-identity the test's discriminating power depends on with
  an explicit assert_eq!(motion[&9].floor_at, p, ...) after step 1, rather
  than relying on it silently (LOW finding 4).

No production-code behavior changes in this commit — comment and test-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQVEpaaKeXsZcW9VT2roeV
@djhenry

djhenry commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Pushed c796b9c addressing all four items from the review. No production-code behavior changes — comment and test text only. Also merged origin/main (b7d6e81, fast-forward-compatible merge, not a rebase) to pick up the three commits that had landed since your run (716cd28, dfa11ba, 56b851f) — confirmed, same as your addendum, none touch src/app.rs.

MUST-1 (your finding 1, MEDIUM) — rewrote the #753: comment above match collision

Dropped the unmeasured consequence clause and named the two mechanisms you found that sit between "the invalidation never ran" and an actual stale serve.

Before:

A later grounded frame landing at a bit-identical b.pos then trusted m.floor_z, computed against the OLD zone's collision. Restructuring so there is a single match with no floating-gated branch around it makes that bypass structurally impossible rather than relying on a second place remembering to invalidate too: b.floating now only gates whether the snap is applied (the boat/#194 behavior — keep the server-sent z), never whether the cache stays honest.

After:

NOT measured: whether a floating entity's cache entry can actually survive a live Some(A) -> None -> Some(B) zone swap end to end — motion.retain (below) drops an absent entity's entry the first frame it's missing from the billboard list, and begin_zone_in clears world.entities before self.collision goes None (crates/eqoxide-core/src/game_state.rs). This restructure closes the code-shape hazard (a floating exemption able to bypass an invalidation) regardless of whether that live sequence is reachable today.

(Full new comment is longer than this excerpt — it also states the two production-writer / one-caller facts you re-verified. Diff: src/app.rs around the match collision above line 2693.)

MUST-2 (your finding 2, MEDIUM) — rewrote the "deliberately don't write floor_at/floor_z" comment

Dropped the unpinned hazard claim your M7 measured false; stated the real rationale.

Before:

(#194). Deliberately don't write floor_at/floor_z here either: memoizing a floor the entity was never actually snapped against would just be a subtler version of the same stale-but-plausible hazard this fix closes.

After:

(#194). Left write-free (not memoizing floor_at/floor_z here) to keep this fix's diff minimal and preserve pre-#753 behavior exactly — memoizing while floating is a plausible alternate design (mutation-checked as M7 in the #753 PR review: the suite stays green under it), but changing it is unrelated to this fix's scope.

No pin added for M7 — out of scope for this fix per your "What would clear this," and per my own instructions to avoid scope creep on a text-only finding. If a pin is wanted, that's a separate follow-up issue, not part of this PR.

MUST-3 (your finding 3, LOW) — col_a now flat_collision_at(-3.0), not 0.0

Changed the test's step-1 collision height so a stale serve is distinguishable from EntityMotion::floor_z's own zero-init, per your M2b measurement. Precondition assertion and comment text updated to match (-3.0 throughout, not 0.0).

MUST-4 (your finding 4, LOW) — pinned the bit-identity

Added, right after step 1:

assert_eq!(motion[&9].floor_at, p, "memo must key on the exact position it raycast at");

Mutation table update — M2 re-run on this tree, crediting your finding

I did not have M1/M2/M2b/M6/M7 in my original table; they're yours. I re-ran M2 myself on the current tree (post MUST-1..4, post main-merge) to confirm it's still discriminating and to check the MUST-3 fix actually makes the failure self-attributing as you predicted:

# Mutation Result Tests red
M2 (re-run, this tree) None => {} — delete only the invalidation RED new test only, 28 passed; 1 failed; 0 ignored; 0 measured; 186 filtered out

Failure message on this tree:

thread 'app::tests::floating_across_a_zone_reload_does_not_resurrect_the_old_zones_floor' panicked at src/app.rs:3216:9:
grounded frame after a floating zone-reload transition must re-raycast against the CURRENT collision (col_b, z=5), got z=-3.000001 — a stale memo would report the pre-reload col_a value of z=-3

z=-3.000001 is now unambiguous — it can only be the stale col_a value (-3.0), not EntityMotion's zero-init default. That's the MUST-3 fix doing exactly what your finding-3 analysis said it would. Mutation reverted from a cp -p snapshot taken before mutating, md5sum-verified identical, touched. Command: test -p eqoxide --lib --locked --no-fail-fast -- app::tests, remote builder.

I did not re-run M0/M1/M2b/M3–M9 myself — your table already covers them and nothing in the MUST-fixes changes production code, so I have no reason to believe they'd move. Flagging that as a gap rather than silently relying on it: I'm crediting your rows, not re-deriving them.

Five figures (workspace cargo test --workspace --locked --no-fail-fast, remote builder, stdout/stderr captured separately, on c796b9c)

  1. Compile sentinel (stderr): Finished `test` profile [unoptimized + debuginfo] target(s) in 14.30s. Completion confirmed by both the process exiting (ps -p gone) and a complete final test result: line in stdout — sentinel alone is not treated as run-completion.
  2. ^running [0-9]+ tests? headers (stdout): 55. ^test result: lines (stdout): 55. Equal — no lost binary. Singular/plural split: 2 running 1 test, 53 running [0-9]+ tests.
  3. Non-canonical ^test result: lines: 0.
  4. Empty targets, ^test result: ok\. 0 passed; 0 failed; 0 ignored (anchored full triple): 14. Cross-checked against ^running 0 tests headers: 14. Same count, not re-derived — matches your established 14 (not the PR body's stale 16).
  5. 1837 passed + 0 failed + 47 ignored + 0 filtered = 1884, matching the summed running N tests header total of 1884. 0 failed, 0 measured.

(Delta from your last run — 1878 → 1884 — is the three merged commits' own test additions, not anything from this PR's diff; this PR still contributes exactly the one new test, floating_across_a_zone_reload_does_not_resurrect_the_old_zones_floor.)

NOT verified, and not claimed

  • Everything already in your "NOT verified (by me)" and my original PR body's "NOT verified" sections still applies unchanged — no live run, no coalescing/ArcSwap-snapshot measurement, no id-collision-across-zones measurement, no search for other similarly-shaped memoize/invalidate pairs elsewhere in the codebase, no separate clippy/lint pass.
  • I did not re-run M0/M1/M2b/M3–M9 on this tree myself (see mutation-table note above) — only M2, the row most directly touched by MUST-3.
  • I did not re-verify the AT_FlyMode / Entity::floating() dynamism claim myself beyond what you already traced in section E — relying on your measurement, not re-deriving it.

scripts/check-no-local-detail.sh passes on the current tree; I hand-scanned this comment and the commit message for /home/|dhenry@|jimbo|\.lan|192\.168|builder@ — clean.

Over to you — this goes back to the same review, not a fresh one, per the project's process.

@djhenry

djhenry commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

APPROVED

Round-2 review of c796b9c. One LOW, non-blocking finding (a sentence in the new MUST-1 comment that a grep falsifies); everything else in the round holds up under measurement. Measured on the MERGED tree: c796b9c merged with current origin/main 53ded87clean merge, no conflicts. Note your figures were taken on the branch merged with b7d6e81; #840 (53ded87) landed after, so the branch as pushed does not contain current main. The five main-side files that merge brings (crates/eqoxide-core/src/coord.rs, crates/eqoxide-http/src/{lib,observe}.rs, crates/eqoxide-net/src/action_loop.rs, docs/http-api.md) do not include src/app.rs; the coord.rs delta is a FOOT-datum doc comment and does not touch skips_wire_z_offset.

First, a mechanical confirmation that this round is text-only. Every non-comment line in git diff a22e3e5 c796b9c -- src/app.rs is inside the one test:

-        let col_a = flat_collision_at(0.0);
+        let col_a = flat_collision_at(-3.0);
-        assert!(bbs[0].pos[2].abs() < 1e-3, "precondition: grounded on col_a at z=0");
+        assert!((bbs[0].pos[2] + 3.0).abs() < 1e-3, "precondition: grounded on col_a at z=-3");
+        assert_eq!(motion[&9].floor_at, p, "memo must key on the exact position it raycast at");
-             col_a value of z=0",
+             col_a value of z=-3",

Zero production statements changed. So the round-1 production-code verdicts carry over on their own terms; what needed re-checking was the prose and whether the test edits moved anything.


1. MUST-1 / MUST-2 prose — clause by clause, whole paragraph

I checked every factual assertion in both replacement blocks against the merged tree, not just the changed lines.

MUST-1, true as written:

  • "the collision match runs UNCONDITIONALLY — including while b.floating" — TRUE (src/app.rs:2714-2736).
  • "The original shape nested the whole match inside if !b.floating" — TRUE in effect; the original was if b.floating { /* comment only */ } else { match … }, which is the same shape.
  • "floating() is re-derived from the LIVE flymode every frame, flymode-Z follow-ups (#548 residuals): runtime OP_SpawnAppearance flymode change + patrolling-Levitating mid-route offset #578, not a one-time spawn flag" — TRUE, and I re-verified the whole chain on the merged tree rather than carrying it forward: Entity::floating() reads self.flymode (crates/eqoxide-core/src/game_state.rs:191-193); crates/eqoxide-net/src/packet_handler.rs:2332-2335 writes e.flymode on OP_SpawnAppearance type-19; crates/eqoxide-renderer/src/scene.rs:337 sets floating: e.floating(); src/app.rs:1289 rebuilds SceneState every frame.
  • "self.collision has exactly two production writers (Some on load completion, None on reload start)" — TRUE on the merged tree: src/app.rs:904 and src/app.rs:1447; no .take()/.replace()/.insert(). And begin_zone_load still has exactly one production caller, src/app.rs:1448 — the other eight call sites (crates/eqoxide-nav/src/walker.rs:2544, crates/eqoxide-nav/src/zone_assets.rs:529/541/560/679, crates/eqoxide-net/src/action_loop.rs:3989/4318/4352) are all under #[cfg(test)]. So "every real collision swap passes through None" holds.
  • "b.floating now only gates whether the snap is applied … never whether the None arm is reachable" — TRUE.

MUST-1's second paragraph, true as written — and its ordering claim is now measured, not inferred:

  • "motion.retain (below) drops an absent entity's entry the first frame it's missing" — TRUE (src/app.rs:2739), and pinned by distant_entity_is_not_glided_and_despawn_drops_state.
  • "begin_zone_in clears world.entities before self.collision goes None" — TRUE. In round 1 I took this from begin_zone_in's own doc comment; this round I traced the callers. begin_zone_in() clears at crates/eqoxide-core/src/game_state.rs:1397, and its two production callers are crates/eqoxide-net/src/gameplay.rs:986 (first statement of run_zone_entry_handshake, before send_zone_entry — therefore before the new zone's OP_NewZone can land) and crates/eqoxide-net/src/login.rs:212 (on ReconnectZone, before on_zone_connected). The renderer-side self.collision = None at src/app.rs:1447 only fires once zone_needs_reload observes the new world.zone_name, which OP_NewZone sets — strictly after both. Ordering confirmed.
  • The "NOT measured" label itself is correctly scoped and honestly applied.

MUST-2, true as written:

  • "Left write-free … to keep this fix's diff minimal and preserve pre-smooth_entity_motion: floating-entity floor-snap exemption leaves the memo cache stale instead of invalidating it #753 behavior exactly" — TRUE; the pre-fix floating branch was comment-only.
  • "memoizing while floating is a plausible alternate design (mutation-checked as M7 …: the suite stays green under it)" — TRUE, and I re-ran M7 on this tree to check the word "suite" is earned. Round 1 I ran M7 filtered to the 29 app::tests; this round unfiltered over the whole target: 214 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out — SURVIVING lib-wide. smooth_entity_motion is module-private (fn, src/app.rs:2563), so no test outside that target can reach it; the lib-wide green is the strongest evidence obtainable short of driving EqApp::update, which nothing does. Reading the whole surrounding paragraph: the unchanged Boat/ferry travel unsupported — no rider/vehicle mechanic #194 / Mob::FixZ prose above it does not contradict the new sentences.

FINDING (LOW, non-blocking) — "appears nowhere else in this file" is false by grep

Confirmed: [f32::NAN; 3] — the invalidation — appears nowhere else in this file

grep -n 'f32::NAN; 3\]' src/app.rs on the merged tree returns two code occurrences:

  • src/app.rs:2592floor_at: [f32::NAN; 3], floor_z: 0.0,, in the motion.entry(b.id).or_insert_with(…) initialiser
  • src/app.rs:2735 — the None arm

Same literal, same field. And 2592 is not inert: NaN != anything makes b.pos != m.floor_at true on the entry's first frame, so entry re-creation is functionally a second invalidation path — which is precisely the path the next paragraph leans on ("motion.retain (below) drops an absent entity's entry the first frame it's missing from the billboard list"). So the sentence is both falsifiable by a five-second grep and slightly in tension with the paragraph immediately following it.

Why it is not blocking: the intended reading (nothing else invalidates a live entry) is true, the conclusion the sentence supports is true, and both readings agree on the fix. But it is prefixed "Confirmed:", which is a measurement claim, and the measurement disagrees. Suggested one-line replacement, no re-review needed:

Confirmed: the only other [f32::NAN; 3] in this file is the motion.entry(..).or_insert_with(..) initialiser (~2592), which invalidates only on entry creation; nothing else invalidates a live entry. And self.collision has exactly two production writers …


2. Does MUST-3 discriminate? MEASURED — yes

M10 (production-code mutation at the call site: delete only m.floor_z = col.floor_z(b.pos[0], b.pos[1], b.pos[2]);, leaving the floor_at write — so floor_z is served from EntityMotion's zero-init forever, which is exactly the confound finding 3 named):

thread 'app::tests::floating_across_a_zone_reload_does_not_resurrect_the_old_zones_floor'
panicked at src/app.rs:3191:9:
precondition: grounded on col_a at z=-3

It dies at 3191, the step-1 precondition — not at 3216. The initialiser cause can no longer produce the step-4 message at all; it is intercepted three steps earlier by a different assertion with a different message. The sibling test failed under the same mutation with got z=0, the distinct number. Against M2r (below), which produces got z=-3.000001 at 3216. Three causes, three distinguishable outcomes. 212 passed; 2 failed; 1 ignored. MUST-3 does what finding 3 asked.

3. Does MUST-4's pin bind? MEASURED — yes

M12 (production-code mutation: b.pos = m.display;b.pos = [m.display[0], m.display[1], m.display[2] + 1e-6]; — literally the epsilon drift the pin's own comment names):

thread 'app::tests::floating_across_a_zone_reload_does_not_resurrect_the_old_zones_floor'
panicked at src/app.rs:3197:9:
assertion `left == right` failed: memo must key on the exact position it raycast at
  left: [10.0, 0.0, 10.000001]
 right: [10.0, 0.0, 10.0]

RED on the new assert_eq!. 211 passed; 3 failed; 1 ignored — two other tests also caught the drift, but at different assertions (first sight snaps to the server position @2864, re-entering entity snaps to the server position @3072); they catch it as a snap-identity failure. The pin is the only assertion in the suite that names the memo key, and the only one inside the #753 test. Not decoration.

4. Author-declared gaps — graded

  • Re-running only M2 was the right pick, and I re-derived it rather than accepting it. M2r (None => {}) on the merged tree: RED, 213 passed; 1 failed; 1 ignored, failing at src/app.rs:3216 with got z=-3.000001 — matching your paste. The result that mattered and that neither of us had stated: the new step-1 precondition and pin do not mask the assertion under test — the test still reaches 3216 under the mutation it exists to catch. A pin placed before the thing under test can short-circuit it; this one does not.
  • Which round-1 verdicts could the col_a/pin edits have invalidated? Two classes, and only two: (a) a verdict whose sole detector is this test — that is M2 alone, re-run, unchanged; (b) a surviving verdict that the two new assertions might now kill — the only surviving verdict touching this code path is M7, re-run, still surviving (and now lib-wide). The boat-gate mutations (if true / inverted) were RED on other tests independently of this one, so their verdicts cannot move. Nothing else in the round-1 table has this test as its only detector. Your conclusion was right; your stated reason ("nothing in the MUST-fixes changes production code") was the wrong one — the risk was never production drift, it was the test edits changing what an unchanged mutation is detected by. No residual defect, but worth knowing which argument actually carries it.
  • The unverified AT_FlyMode / floating() dynamism claim: correctly flagged rather than silently re-asserted. I re-verified the full chain on the merged tree this round (section 1) — it holds, so there is no residual. Fine to keep relying on it.

5. Figures — spot-checked on the merged tree, not re-derived

Full workspace run on c796b9c + 53ded87, remote builder, stdout/stderr captured separately, process confirmed gone AND a complete final test result: line present:

yours (c796b9c + b7d6e81) mine (merged with 53ded87)
^running N tests? headers / ^test result: lines 55 / 55 55 / 55
non-canonical test result: lines 0 0
empty targets, anchored full triple 14 14
cross-check ^running 0 tests 14 14
passed / failed / ignored / measured / filtered 1837 / 0 / 47 / 0 / 0 = 1884 = header sum 1839 / 0 / 47 / 0 / 0 = 1886 = header sum

The +2 is #840's own tests, not this PR. All five figures reproduce.

Mutation table (this round)

All four mutations are at the call site in production code, never a body-wrap. Each was applied from a cp -p pristine snapshot, has a distinct md5, and each build log carries exactly one Compiling eqoxide v0.1.0 line (reach control) plus a complete test result: line. Target: test -p eqoxide --lib --locked --no-fail-fast (215 tests, 1 ignored).

# Mutation Result Detail
R0 none (baseline, merged tree) GREEN 214 passed; 0 failed; 1 ignored
M2r None => {} — delete only the invalidation RED 1 failed, at 3216, got z=-3.000001
M10 delete only m.floor_z = col.floor_z(..) RED 2 failed; new test dies at 3191 (precondition), not 3216
M12 b.pos = m.display+1e-6 in z RED 3 failed; new pin RED at 3197
M7 memoize while floating (gate only the apply) SURVIVING 214 passed; 0 failed — lib-wide, not just app::tests

Mutations reverted from the pristine snapshot, md5sum-verified back to f0702bd7…, and touched.

NOT measured / NOT claimed (mine)

  • No live run. Whether a floating entity's memo can survive a real Some(A) → None → Some(B) swap end to end remains unmeasured — the PR now says so itself, which is the right outcome.
  • I did not re-run the round-1 mutations other than M2 and M7; section 4 argues why the rest cannot have moved, but that is reasoning, not measurement.
  • No clippy/lint pass, no search elsewhere in the codebase for other memoize/invalidate pairs with this shape.
  • The one finding above is non-blocking. Nothing else in this round needs another pass — fix the sentence on merge or as a follow-up, your call.

djhenry and others added 2 commits August 1, 2026 10:00
…rrences

Reviewer measured the sentence false: `[f32::NAN; 3]` appears at three
locations in src/app.rs, not the claimed "nowhere else" — src/app.rs:2592 is
the motion.entry(..).or_insert_with(..) initialiser using the same literal
on entry creation, and src/app.rs:2735 is the None arm this comment
describes. Not pedantry: NaN != anything, so entry re-creation is itself a
second invalidation path, and it's the exact path the next paragraph
(motion.retain) already leans on.

Replaced with the reviewer's verbatim correction: the only OTHER occurrence
is the entry initialiser (invalidates only on entry creation), and nothing
else invalidates a live entry. No behavior change — comment text only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQVEpaaKeXsZcW9VT2roeV
@djhenry

djhenry commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Applied the round-2 correction verbatim, merged origin/main (picks up #840, 53ded87, and #841, 365486d — neither touches src/app.rs, confirmed by git diff --stat before merging), and updated the PR body to the final state.

The one-line correction (src/app.rs:2700)

Before:

matter what collision did. Confirmed: [f32::NAN; 3] — the invalidation — appears nowhere else in this file, and self.collision has exactly two production writers …

After (your verbatim replacement):

matter what collision did. Confirmed: the only other [f32::NAN; 3] in this file is the motion.entry(..).or_insert_with(..) initialiser (~2592), which invalidates only on entry creation; nothing else invalidates a live entry. And self.collision has exactly two production writers …

Committed separately (545922b) from the merge, so the correction is reviewable on its own.

Source-text pin check

Grepped for include_str! on app.rs before running the suite, since the edit adds
motion.entry(..).or_insert_with(..) text into a comment: three pins exist
(src/zone_in.rs:1153, src/movement.rs:3518, src/movement.rs:3570). The zone_in.rs one
strips comments before scanning (strip_comments, built for exactly this hazard). The two
movement.rs pins anchor on if zone_needs_reload(...) and
if self.camera_initialized {\n if let Some(c) = self.collision.as_deref() { — both far from
and untouched by this edit's region (smooth_entity_motion, ~2695-2706). None of the three could
be affected by a comment-only change in a different function; confirmed by the full suite run
below (all three pin tests pass, no non-canonical results).

New head

eb20544f8e2af60bd997910d01ffe62dd8737fee (merge commit; 545922b is the correction, 53ded87/365486d are main's #840/#841).

Build hygiene note

First rbuild attempt on this round was killed mid-compile by the Bash tool's own 2-minute default
timeout (I'd backgrounded only the echo $? > exit half of the pipeline, not the rbuild call
itself — cmd1; cmd2 & only backgrounds cmd2). Confirmed the process was fully dead
(ps -p <pid> empty) and the partial log stopped mid-Compiling, so I discarded it rather than
reusing or trusting it, and re-ran with nohup ... &, a fresh log path
(fix753-floor-memo-eb20544-run2.*), and the prescribed foreground polling loop. That loop itself
hit the 600s Bash-call cap once and got auto-backgrounded by the harness; per instruction I
re-issued the identical loop as my next action rather than arming a Monitor or ending the turn,
and it returned clean on the second call.

Five figures (workspace cargo test --workspace --locked --no-fail-fast, remote builder, stdout/stderr captured separately, head eb20544)

  1. Compile sentinel (stderr): Finished `test` profile [unoptimized + debuginfo] target(s) in 6m 22s. Completion confirmed by both ps -p <pid> showing the process gone and a complete final test result: line in stdout — sentinel alone not treated as run-completion.
  2. ^running [0-9]+ tests? headers: 55. ^test result: lines: 55. Equal. Singular/plural split: 2 running 1 test, 53 running [0-9]+ tests.
  3. Non-canonical ^test result: lines: 0.
  4. Empty targets: 14 under the anchored full-triple predicate (^test result: ok\. 0 passed; 0 failed; 0 ignored), cross-checked against ^running 0 tests: also 14. Matches your prediction exactly — not re-derived.
  5. 1839 passed + 0 failed + 47 ignored + 0 filtered = 1886, matching the summed running N tests header total of 1886. 0 failed. Matches your prediction exactly (1839 + 0 + 47 + 0 = 1886, +2 over the round-1 figure being fix(#827): take zone_cross's verdict and its grid from one call, not two slots #840's own tests).

NOT verified, and not claimed

Unchanged from my last comment — no live run, no coalescing/ArcSwap measurement, no id-collision-across-zones measurement, no search for other similarly-shaped memoize/invalidate pairs. Added this round: I did not re-run the round-1 mutation table myself a second time (structural revert, if !b.floating inversions, the two adjacent-design controls) — that's your re-verification on the merged tree, not mine independently repeated.

scripts/check-no-local-detail.sh passes on the current tree; hand-scanned this comment and the updated PR body for /home/|dhenry@|jimbo|\.lan|192\.168|builder@ — clean in both.

Not merging this myself — leaving that to the coordinator on your verdict.

@djhenry
djhenry merged commit 8e1dee7 into main Aug 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

smooth_entity_motion: floating-entity floor-snap exemption leaves the memo cache stale instead of invalidating it

1 participant