Skip to content

fix(engine): correct the answer-beat frame mechanism and harden its row (follow-up to #7005) - #7023

Merged
matthewevans merged 5 commits into
phase-rs:mainfrom
lgray:fix/loop-shortcut-answer-beat-hardening
Aug 5, 2026
Merged

fix(engine): correct the answer-beat frame mechanism and harden its row (follow-up to #7005)#7023
matthewevans merged 5 commits into
phase-rs:mainfrom
lgray:fix/loop-shortcut-answer-beat-hardening

Conversation

@lgray

@lgray lgray commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Summary

Carries the review-response work for #7005 that missed its merge. #7005 was squash-merged at 90c525eb7 from head a6d1a0e62 while three follow-on commits were being reviewed; a push of those commits raced the merge and GitHub never adopted them, so they stranded on the fork branch. This PR is exactly that content, rebased onto current main.

It corrects false mechanism claims that main now carries, replaces orphaned commit-SHA citations, closes the CodeRabbit Major on the answer-beat row, and restores a guard the row's mint detector had dropped. Comments and one test row only — zero production behaviour change. The entire non-comment crates/engine/src/ delta is a single string literal (a line-anchored census pin).

Files changed

  • crates/engine/src/game/engine.rs — the sampler comment re-cited to the correct basis; a stale ring-scan comment whose coordinates had rotted onto unrelated code, now naming symbols; census-pin literal
  • crates/engine/src/analysis/resource.rs — a scope note on ring_delta_signature stating which fields it actually reads
  • crates/engine/tests/integration/loop_shortcut.rs — answer-beat row: assertions widened over every frame a beat mints, mint detector guard restored, reach-guard asserted

Track

Developer

LLM

Model: claude-opus-5
Tier: Frontier
Thinking: high

Implementation method (required)

Method: /engine-implementer

CR references

None added, moved, or removed — git diff over the range matches no CR \d line in either direction. The CR numbers the touched hunks rely on were re-verified against the rules text regardless: CR 732.2a, CR 704.5a, CR 119.3, CR 110.1, CR 104.4b, CR 603.5, CR 122.1, CR 606.3, CR 608.2, CR 703.1/.2/.3, CR 500.8 — all resolve and describe the annotated code.

What main currently carries, and why this is worth a second PR

  1. A false mechanism, in three in-tree sites. fix(engine): commit every axis a bounded loop-shortcut offer publishes #7005 claims a ring heterogeneous in waiting_for/priority_player breaks ring_delta_signature's turn-position conjunct. Measured, that function's body reads neither field — it compares ResourceVector::snapshot(&f.normalized) and window_scope_from_cover_frames(..).phase_invariant, and phase_invariant compares only turn_number, phase, extra_phases.is_empty(). The real sensitivity is loop_states_equal_modulo_resources applied to a ring frame's normalized snapshot, which chains to impl PartialEq for GameState. One of the three sites is an assertion message, so on failure it would print a false reason.

  2. Citations of a commit that does not exist in this history. Four in-tree references named a pre-rebase SHA that no longer resolves from main; two sat in a permanent production comment. Under squash-merge no branch SHA survives, so they were pre-rotted. Replaced with the PR number.

  3. An unclosed CodeRabbit Major. The answer-beat row derived site attribution from a ring-length delta, so a beat recording at both sampler sites would leave the row inspecting a settle frame while both arms passed. Fixed structurally — the arms now run over every frame the beat minted, which makes attribution irrelevant rather than more carefully asserted.

  4. A dropped guard. The mint detector omitted the is_some() check its production counterpart has, so a ring clear would reach an unchecked usize subtraction and panic with an overflow message instead of the row's own explanation.

Verification

  • Required checks ran clean, or the exact CI-owned alternative is stated below.
  • Gate A output below is for the current committed head.
  • Final review-impl below is clean for the current committed head.
  • Both anchors cite existing analogous code at the same seam.

Measured at b90042d66e5badf3fa9837c3bd6c04488f984ad6, base a998c722413c8ec1364fd19fb8502d7f964fd0af, clean tree:

  • cargo fmt --all — clean
  • cargo clippy --workspace --all-targets -- -D warningsEXIT=0
  • cargo test -p phase-engine --lib18522 passed; 0 failed; 6 ignored
  • cargo test -p phase-engine --test integration4558 passed; 0 failed; 2 ignored

Row-count movement fully attributed. The +2 integration rows are this branch's two new regression rows — git diff over the range adds 2 and removes 0 #[test]/#[tokio::test]. The last rebase moved the base across three upstream commits (a client change and two chores) which add and remove zero engine tests in both directions, so the count is unchanged by the rebase itself. Zero unexplained movement.

Rebase is content-preserving, verified: git range-diff pairs every commit with =, and the rebase correctly dropped #7005's squash and #7020 as already upstream.

Rebase is content-preserving, verified three ways: git range-diff pairs all three commits with =; all three touched files are byte-identical by sha256 across the rebase; and the only paths differing between the pre- and post-rebase tips belong to #7020, not to this branch.

Discrimination of the answer-beat row. Clobbering priority_player after the sync fails one arm (PlayerId(3) vs PlayerId(0)); clobbering the window fails the other (GameOver vs Priority{PlayerId(0)}), with the first arm passing before it, so the arms are separately live. Clobbering priority_player at the settle sampler fails at a non-forced beat, and the same mutant with the assertion narrowed back to forced-only passes — a two-sided control showing the widened scope is real added coverage rather than a restatement. Five inspected frames against two before the widening.

Gate A

Gate A PASS head=b90042d66e5badf3fa9837c3bd6c04488f984ad6 base=a998c722413c8ec1364fd19fb8502d7f964fd0af

The gate's scope is crates/engine/src/parser, and this PR changes 0 files there. The PASS is real but uninformative for this diff — it passes because no parser code is touched, not because parser code was checked.

Anchored on

  • crates/engine/src/game/engine.rs:6050 — the settle-beat record_loop_detect_sample() call in pass_priority_once_with_pipeline, the pre-existing authority for recording a loop-detect frame. The widened assertions rest on both samplers sharing its Priority{player == active_player} conjunct and its record-after-sync_waiting_for ordering.
  • crates/engine/src/game/effects/mod.rs:5844optional_effect_is_infeasible, the pre-existing single authority for "this optional effect cannot be taken", the seam upfront_optional_gate extends rather than parallels.

Final review-impl

Final review-impl PASS head=b90042d66e5badf3fa9837c3bd6c04488f984ad6

Stated precisely, because the honest answer is not one line. Three independent passes ran on the first three commits: a review of the first (BLOCK, on the false mechanism above), a re-review of the remedy, and a verification pass returning NON-BLOCKING with three LOW diagnostic findings. Rebases since have been measured content-preserving, so those passes bind to this head for that content.

The fourth commit — the fix for this PR's review — has not had a separate independent review pass. It was self-verified with per-assertion DROP/TRIVIALIZE mutant pairs and with both broken routes driven live through production apply(), and the maintainer's re-review is the intended next check. I would rather say that than let one PASS line imply a review that did not happen.

Claimed parse impact

None. No file under crates/engine/src/parser is touched.

Scope Expansion

None. This is #7005's own review-response content, unchanged.

Validation Failures

Three LOW diagnostic findings, disclosed rather than fixed. A circuit breaker was set before the last round so further non-blocking findings would be disclosed instead of chased; these are its output. None is a correctness defect and none is reachable on the current fixture.

  1. A comment says "a cleared ring minted nothing, so the beat is skipped". Closed by the review-response commit — that comment and the code it described are gone; the clear-and-rebuild case now has its own row.
  2. The reach-guard clause labelled "Stated exactly" is the one that is not exact — it drops the minting qualifier, since the increment sits after both continue guards. Still open.
  3. The widened arms interpolate only the beat number, so on a multi-frame beat they cannot report which frame in the slice failed. Still open — measured at this head, no frame index is interpolated into either arm's message.

From the review-response commit, disclosed unprompted. The mutant harness initially reported two vacuous PASSes: --exact matches the full libtest path, so a bare function name filtered every test out and libtest still exited 0. It was fixed by qualifying the path and adding a ran() == 1 positive control that reports a setup error on zero tests, and every figure was re-measured under the corrected harness. Separately, one mutant misses its intended target — the doubled-push mutation steps the ring length past the capacity check, so it flips the eviction row's reach guard rather than its shape assertion; the shape assertion's drop arm is a 2-for-2 eviction instead. Both are recorded rather than quietly dropped. Also: the "no tracked fixture reaches clear-and-rebuild naturally" claim was surveyed over 4 dumps, not all 10, and no case was constructed in which an address-keyed snapshot actually aliases — the Arc-clone choice rests on the allocator being free to reuse an address, not on an observed collision.

Honest limits. The underlying sync fix that #7005 landed remains causally inert on the current corpus — instrumenting the pre-fix tree measured zero stale samples at either sampler, with an always-true comparison of the same shape on the same line proving the instrument live, and a pure revert of that reorder still passes. This PR hardens the row that guards it; it does not change that.

CI Failures

None.

Summary by CodeRabbit

  • Documentation

    • Clarified technical documentation for cycle detection and synchronized action behavior.
  • Tests

    • Expanded regression coverage for synchronized frames, ring eviction, and ring clearing.
    • Improved validation of waiting states, priority submissions, frame retention, and sampler attribution.
    • Added coverage for same-beat state rebuilding and priority-driven scenarios.

@lgray
lgray requested a review from matthewevans as a code owner August 5, 2026 07:14
@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 21797cc5-b114-4d1b-ba76-fe811160aa3c

📥 Commits

Reviewing files that changed from the base of the PR and between b90042d and f2df77c.

📒 Files selected for processing (1)
  • crates/engine/tests/integration/loop_shortcut.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/tests/integration/loop_shortcut.rs

📝 Walkthrough

Walkthrough

The change clarifies loop-detection semantics and updates synchronized-window regression coverage. Tests identify every frame minted during a beat, including frames affected by eviction or ring rebuilding, and validate synchronized priority state.

Changes

Loop sampling semantics

Layer / File(s) Summary
Loop detection semantics
crates/engine/src/analysis/resource.rs, crates/engine/src/game/engine.rs
Comments distinguish ring_delta_signature inputs from sampler-enforced properties, clarify Basis A comparisons and synchronization behavior, and update scan and producer references.
Synchronized frame tracking
crates/engine/tests/integration/loop_shortcut.rs
The test tracks retained frames by Arc identity and validates each frame’s synchronized waiting state and authorized priority submitter.
Ring mutation regressions
crates/engine/tests/integration/loop_shortcut.rs
New regressions cover capacity eviction and frames discarded and reminted during a same-beat clear-and-rebuild operation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the engine change to the answer-beat frame mechanism and its hardened test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/tests/integration/loop_shortcut.rs`:
- Around line 11274-11306: Replace the length-based mint detection in the beat
loop around dump_drive_one_beat with complete Arc membership tracking: snapshot
every pre-beat loop_detect_ring entry via Arc::as_ptr, then after the beat
identify and validate every remaining frame whose pointer was not present in
that snapshot. Remove the back_after/grew assertion and ensure append, capacity
eviction, and clear-and-rebuild paths all pass every newly minted frame to the
existing validation logic, preserving production failure-path coverage.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c380cd0-2eb2-4a3f-a086-7b11e3cf6250

📥 Commits

Reviewing files that changed from the base of the PR and between dd6ab09 and 483ce27.

📒 Files selected for processing (3)
  • crates/engine/src/analysis/resource.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/tests/integration/loop_shortcut.rs

Comment thread crates/engine/tests/integration/loop_shortcut.rs Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Generated for head f2df77c5d78b4b1db8bf521fa66d727a496bb267.

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans matthewevans self-assigned this Aug 5, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review head: 483ce27d3e211ad63771a750b856827144626c66

[MED] The regression test does not validate every frame it claims to validate. Evidence: crates/engine/tests/integration/loop_shortcut.rs:11283-11330 detects only a changed back pointer, derives grew from net ring length, and validates rev().take(grew); crates/engine/src/types/game_state.rs:19664-19672 evicts the front before pushing at capacity. Why it matters: an evicting push changes the back while preserving length, causing the row to fail instead of checking the newly minted frame; a clear-and-rebuild beat can also have positive net growth smaller than its minted-frame count, leaving some new frames unvalidated. Suggested fix: snapshot the complete pre-beat Arc::as_ptr membership, then validate every remaining post-beat sample whose pointer was absent before the beat; do not infer minted frames from length.

This confirms the still-current CodeRabbit finding at #7023 (comment).

@matthewevans matthewevans added the test Add tests label Aug 5, 2026
@matthewevans matthewevans removed their assignment Aug 5, 2026
lgray added 4 commits August 5, 2026 06:22
…t frame homogeneity

Comment/test-prose correction on top of `f144bb374`. NO production behaviour
change: the `sync_waiting_for(state, &wf)` hoist that commit made is correct and
is untouched. What it shipped WRONG was the justification.

`f144bb374` claimed, in its message and in three in-tree sites, that a
heterogeneous ring "breaks `ring_delta_signature`'s turn-position conjunct,
because `impl PartialEq for GameState` compares `waiting_for`/`priority_player`".
That is false at source. `ring_delta_signature`'s body reads exactly two things:
`ResourceVector::snapshot(&f.normalized)` and
`window_scope_from_cover_frames(..).phase_invariant`, and `phase_invariant` is
`turn_number` + `phase` + `extra_phases.is_empty()` on both frames. Measured:
`grep -cP "waiting_for|priority_player|loop_states_equal"` over the function body
returns 1, and that single hit is a prose comment, not code.

The real sensitivity is BASIS A. The ring scans call
`analysis::resource::loop_states_equal_modulo_resources(prior, state)` with
`prior` a ring frame's `normalized` half and `state` the LIVE board; that chains
to `types::game_state::loop_states_equal` => `impl PartialEq for GameState`,
which DOES compare both fields, and neither `normalize_for_loop` nor
`project_out_resources` neutralizes either. An un-synced answer-beat frame
therefore compares UNEQUAL against a synced live board and basis A misses the
recurrence the frame was minted to certify. Each link verified at source.

Corrected, in order of harm:
* `tests/integration/loop_shortcut.rs` arm-3 failure message — it printed the
  false reason ON FAILURE, the worst placement of the four. Now states that
  `ring_delta_signature` is INSENSITIVE to the two fields and that arm 3 is the
  blast-radius pin, not a restatement of arms (1)/(2).
* the same file's row doc, and `game/engine.rs`'s permanent comment at the
  sampler, both re-cited to basis A. The comment quotes and names the false claim
  rather than silently replacing it, because a later reader would otherwise
  re-derive it.
* `analysis/resource.rs`'s `ring_delta_signature` doc gains a minimal SCOPE note
  separating what the function READS from where its frames' homogeneity comes
  from. The pre-existing turn-position paragraph is TRUE (via `phase_invariant`)
  and is left standing.

Also corrected:
* BLAST RADIUS was justified by "`apply_action_boundary` re-syncs". Three
  production routes reach `apply_action` without that boundary:
  `inject_pinned_answer`'s three dispatches, `drive_loop_action_iteration`'s
  ten, and `apply_interaction_pre_reconciliation_for_life_safety`, which returns
  `raw.result` without ever calling `finish_action_boundary` (recorded in
  `apply_action_boundary_core`'s own comment). The claim is now an argument about
  RE-DERIVATION (`finish_action_boundary` runs the same `sync_waiting_for` over
  `result.waiting_for` and copies it back; the reorder does not change
  `ActionResult.waiting_for`) plus the measured fact that all three non-boundary
  routes drive a CLONE (`drive_one_shortcut_cycle`'s `work`, the drive's `clone`,
  `preview_candidate_life_safety`'s `preview`), never the settled board.
* "PRE-pipeline pair" was the wrong label for what the pre-fix snapshot carried:
  `run_post_action_pipeline_from` itself writes `state.waiting_for` at five sites.
  Relabelled "UN-SYNCED pair".
* the permanent comment asserted, present tense, "0 divergences over 18,486 lib +
  4,487 integration rows" — figures from a probe tree that this tree already
  exceeds. Restated as a historical measurement attributed to `f144bb374`, with
  no transcribed row count that can go stale.
* one sentence added for a consequence `f144bb374` left unmentioned: because the
  synchronizer runs `normalize_legacy_attach_waiting_for`, `state.waiting_for` can
  now differ from the returned `ActionResult.waiting_for` on this path, where the
  old raw clone made them exactly equal. Benign, and why.

ARM 2 ASSERTED THE WRONG COMPARAND — the one code-affecting fix here.
`frame.priority_player == frame.active_player` is not what `sync_waiting_for`
establishes: it sets `priority_player =
turn_control::authorized_submitter_for_player(state, wf.acting_player())`, which
re-routes to a DIFFERENT seat under a turn-decision controller (Mindslaver) or a
latched search-decision controller. A correctly-synced frame on any future
turn-control fixture would false-fail this standing pin. The comparand is now the
authority function applied to the frame's own window.

That change does NOT weaken the arm, measured rather than argued. Neither
`effective_authority_for_player` nor `search_decision_authority` reads
`priority_player`, so the recomputation cannot be tainted by the mutant it must
catch. Re-run at this tree, predicate `cargo test -p phase-engine --test
integration loop_shortcut::answer_beat_frames_carry_the_synced_window_and_the_
offer_certificate_is_exact -- --exact`, one unit = one test row:
* clean: 1 passed.
* MUTANT-A (`state.priority_player = PlayerId(3);` after the sync): FAILED at
  `loop_shortcut.rs:11335`, `left: PlayerId(3)` / `right: PlayerId(0)`, beat 5 —
  the first answer-beat mint. Arm 2 still fires.
* PURE REVERT of the reorder (drop the hoisted sync, restore the raw clone below
  the record): 1 passed — so the row's own "a pure revert leaves all three arms
  GREEN" statement remains true under the new comparand, and the arm is still a
  standing pin rather than a live catch. `engine.rs` restored byte-identically
  afterwards, verified by sha256.

Assisted-by: ClaudeCode:claude-opus-5

CR 603.5 CENSUS PIN, moved because this commit moved it. The line-exact pin in
`the_cr_603_5_prompt_census_is_pinned_so_a_sixth_producer_is_a_counted_event`
goes `engine.rs:11549 ⇒ :11583`, and the drift-log entry carries the evidence the
row's own convention demands: engine.rs's entire delta this round is ONE comment
hunk (`@@ -11177,8 +11177,42 @@ fn apply_action`, numstat +42/-8 ⇒ net +34)
sitting above the producer, 11549 + 34 = 11583 exactly, the producer line is
sha256-identical to `a6d1a0e62:engine.rs:11549` (prefix `8a544e878d3e77fb…`, the
same prefix the log already recorded for `:11549`), it is still inside
`begin_pending_trigger_target_selection` (which moved by the same +34, :11400 ⇒
:11434), the census total (37) and partition (5/7/25) asserts fired GREEN on the
run that caught this, and the other four entries did not move. Not a sixth
producer.

ONE MORE ROTTED CITATION, same class, fixed LINE-NEUTRALLY. `bounded_cycle_offer`'s
basis-A note cited the four sibling ring `find_map` scans as
`:481`/`:668`/`:710`/`:808`. Measured at this tree those lines hold `);`, `block,`,
`}` and `state,` — meaningless fragments; the real scans are the `On` reconcile,
Path B, Path C and `find_live_loop_winner`. That stale citation is what propagated
the bogus coordinates into this round's own brief, through two agents, before
anyone re-derived them — so the fix is to NAME the four and delete the numbers,
which cannot rot the same way. The block grows by one line and the sampler comment
was tightened by one to pay for it, keeping the CR 603.5 producer exactly on
`:11583`: engine.rs's hunks above it are now `@@ -1924,5 +1924,6 @@` (+1) and
`@@ -11177,8 +11178,41 @@` (+33), still 11549 + 34.

COUNT RECONCILED, and my first figure was wrong. `drive_loop_action_iteration`
dispatches `apply_action` TEN times, not nine. Predicate: `awk` the function body
(`fn drive_loop_action_iteration(` at :4062 to the next column-0 `}` at :4319) and
`grep -cP 'apply_action\('` it; one unit = one call-expression opening line. The
ten are :4087 :4118 :4150 :4173 :4200 :4222 :4231 :4256 :4271 :4303. My "nine" was
a transcription miscount off a whole-file grep that had listed :4231 — not a
different predicate. Corrected above and in the comment.
… of measuring it

CodeRabbit (Major) on `answer_beat_frames_carry_the_synced_window_and_the_offer_
certificate_is_exact`. The row is a STANDING pin — it exists to fire the first
time an answer beat diverges — but its mint detector could hand arms (1)/(2) the
wrong frame, in which case the pin goes quietly vacuous instead of firing. Two
independent ways, both closed here. No production change; test-only.

(1) ATTRIBUTION. A single beat can reach BOTH samplers — `apply_action`'s
forced-window answer site, then the settle site in
`pass_priority_once_with_pipeline`. The ring then grows by 2, `answered_forced_
window` is true, and `back()` holds the SETTLE frame; arms (1)/(2) pass while
inspecting a frame the reorder never touched. `assert_eq!(grew, 1, ..)` on the
answer path is the property that makes `back()` the answer-site frame, and
`settle_mints += grew` stops the settle path under-counting a double mint.

(2) EVICTION. `record_loop_detect_sample` pops the front before pushing once the
ring is at `LOOP_DETECT_RING_CAP`, so a full ring mints WITHOUT growing and the
old `len() == before ⇒ continue` read that as "no mint" — `answer_mints` would
then measure a smaller set than the reach-guard claims. Fixed by detecting a mint
through `Arc` identity of the ring's back, which is what
`game::engine::drive_one_shortcut_cycle` already does and for this exact reason
(its own comment: "a length delta reads 0 once it is full"). Reusing the shipped
instrument rather than adding a second one.

DELIBERATE DEVIATION, stated rather than slipped in: the brief asked for a
headroom assertion against the ring's capacity. The capacity is real and fixed —
`LOOP_DETECT_RING_CAP: usize = 16`, `types/game_state.rs:20025` — but it is
PRIVATE to that module, so an integration row can only transcribe the literal.
A transcribed cap fails in the dangerous direction: if the const ever SHRINKS
below the drive's ring length, `len() < 16` still passes while eviction silently
resumes. Identity detection has no such failure mode and makes the capacity
irrelevant to this row, so the residual `assert!(grew >= 1, ..)` needs no
constant: back-changed-but-length-did-not IS "at capacity and evicting", named as
such in the message and failing closed.

MEASURED, one unit = one emitted probe line = one beat on which the ring's back
changed. Temporary `eprintln!` on the line after `grew` is computed, whole drive,
`--nocapture`:
  beat=0  answered_forced=false grew=1 len=1
  beat=5  answered_forced=true  grew=1 len=2
  beat=9  answered_forced=false grew=1 len=3
  beat=14 answered_forced=true  grew=1 len=4
  beat=18 answered_forced=false grew=1 len=5
So `assert_eq!(grew, 1)` is REACHED on both answer beats (5 and 14) and true
there — not vacuously skipped — and the 2/3 answer/settle split the row's doc
claims is reproduced by the new counters. The probe was removed afterwards and
the file restored byte-identically (sha256 `45b33588f08cc6cf…`).

HONEST LIMIT on the second assertion: max ring length on this drive is 5 against a
cap of 16, so `assert!(grew >= 1)` never trips here and is NOT discriminating on
this fixture. That is its job — it is a fail-closed guard against a future fixture
whose drive fills the ring, which is exactly the case where the old detector would
have gone silent.

Assisted-by: ClaudeCode:claude-opus-5

ARM SEPARATION, a defect the arm-1 mutant caught in MY OWN prior round. The
`priority_player` comparand added in `e0da32fca` derived its semantic player with
`frame.waiting_for.acting_player().expect(..)` placed BEFORE arm (1). A window
with no acting player is precisely what arm (1) exists to catch, so the
`waiting_for = GameOver { winner: None }` mutant died on the unwrap at
`loop_shortcut.rs:11321:14` — arm (1)'s explanation replaced by an unwrap string,
and the row's own doc claim that the two arms are separately live silently false.
Fixed by making arm (2) an `if let`. The pair is TOTAL, so the skip opens no hole:
either arm (2) runs, or the window has no actor and arm (1) fails on that same
frame.

Both mutants re-run at the final tree, predicate `cargo test -p phase-engine
--test integration loop_shortcut::answer_beat_frames_carry_the_synced_window_and_
the_offer_certificate_is_exact -- --exact`, one unit = one test row:
* `state.priority_player = PlayerId(3);` ⇒ FAILS ARM (2), `:11327`, beat 5,
  `left: PlayerId(3)` / `right: PlayerId(0)`, arm (2)'s own message.
* `state.waiting_for = WaitingFor::GameOver { winner: None };` ⇒ FAILS ARM (1),
  `:11337`, beat 5, `left: GameOver { winner: None }` /
  `right: Priority { player: PlayerId(0) }`, arm (1)'s own message.
* clean: 1 passed. `engine.rs` restored byte-identically after both,
  sha256 `2393d6a2597c1e33…`.
A DIFFERENT arm answers each mutant, which is the property "separately live"
names; the doc bullet is corrected to say so and to record why arm (2) is an
`if let`.

CR 603.5 census pin re-derived, not assumed: this commit touches only
`tests/integration/loop_shortcut.rs`, and the producer is still `engine.rs:11583`
with sha256 prefix `8a544e878d3e77fb` unchanged.
…ts, not over `back()`

Follow-up review on `e7e50229b`. That commit fixed a vacuity in this row and
introduced a NEW false claim of the same shape as the one the branch exists to
correct — a locally true statement generalised one step too far. Test-only; no
production behaviour change.

THE FALSE CLAIM, stated before it is fixed. `e7e50229b`'s comment said `grew == 1`
"is the property that makes `back()` the answer-beat frame". It is NECESSARY, not
sufficient. The row's `answered_forced_window` is `is_forced_cascade_window()` read
BEFORE the beat, i.e. ONE conjunct of the production answer-site gate, which also
requires `!in_simulation_probe()`, `loop_detection.samples()`, `!stack.is_empty()`,
`stack.len() >= stack_len_before_action`, and `Priority{player == active_player}`.
One test beat is one `apply()`, and `apply()` reaches the SETTLE sampler after
`apply_action` returns. So on a beat where answering the forced window resolves the
last stack entry — `!stack.is_empty()` false, answer site gated OFF — and the
refill cascade mints one settle frame, `grew == 1` holds, `answer_mints`
increments, and arms (1)/(2) inspect a SETTLE frame while the row claims an answer
frame. That is precisely the class this row targets: a self-refilling drain
cascade. The predecessor paragraph "SITE ATTRIBUTION IS EXACT, not assumed" is
wrong for the same reason and is corrected in place, quoting itself.

FIXED AT THE ROOT rather than by asserting harder. Arms (1) and (2) now run over
`loop_detect_ring.iter().rev().take(grew)` — every frame the beat added, on every
minting beat — so attribution stops mattering instead of getting sharper. Sound
because BOTH samplers gate their record on `Priority{player == active_player}`
(`engine.rs` answer site; `pass_priority_once_with_pipeline` settle site) and both
record after their own `sync_waiting_for`: a frame failing either arm is a real
defect whichever site minted it. `grew == 1` is therefore no longer asserted at
all — what remains of the mint accounting is `Arc`-identity detection and
`grew >= 1`, and the comment now says only what each actually rules out.

MINT DETECTOR: the production `is_some()` guard restored. `drive_one_shortcut_cycle`
uses `ring_back_after.is_some() && ring_back_after != ring_back_before`;
`e7e50229b` kept only the inequality. The settle sampler's `else` arm calls
`loop_detect_ring.clear()`, so `back()` can go to `None` inside a beat — then
`None != Some(..)` fell through to `len() - before`, which underflows and panics
"attempt to subtract with overflow" (dev profile leaves `overflow-checks` at its
`true` default) instead of this row's own explanation. Now: `is_some()` skips a
cleared-and-not-repushed beat as the non-mint it is, `saturating_sub` cannot
underflow, and the `grew >= 1` message names BOTH causes — at-capacity eviction
AND clear-then-repush — because `e7e50229b`'s message asserted "AT CAPACITY",
which is false for the clear case.

`settle_mints` NOW FEEDS AN ASSERTION. `e7e50229b` incremented it and reached only
an interpolated message while its commit message listed it among vacuities "both
closed here" — an overclaim. It is now the reach-guard for the widening
(`settle_mints > 0`), which is the conjunct that proves the settle-frame coverage
was actually exercised. The `answer_mints` guard's message is corrected to say it
is a reach signal over ONE gate conjunct, not proof of which sampler minted.

CITATIONS DE-ROTTED, same class as this branch's `bounded_cycle_offer` fix two
hunks away. Four in-tree references named `f144bb374`, which the user's rebase
orphaned: `git merge-base --is-ancestor f144bb3 HEAD` exits 1. Two of the four
sat in a PERMANENT PRODUCTION COMMENT, and under squash-merge neither that SHA nor
`a6d1a0e62` survives into main. All four now cite `phase-rs#7005`, which is durable and was
already named alongside them.

EVIDENCE. Predicate for every row below: `cargo test -p phase-engine --test
integration loop_shortcut::answer_beat_frames_carry_the_synced_window_and_the_
offer_certificate_is_exact -- --exact`; one unit = one test row.

Reachability probe, temporary `eprintln!` after `grew`, `--nocapture`, one unit =
one minting beat:
  beat=0  forced=false grew=1 len=1 inspected=1
  beat=5  forced=true  grew=1 len=2 inspected=1
  beat=9  forced=false grew=1 len=3 inspected=1
  beat=14 forced=true  grew=1 len=4 inspected=1
  beat=18 forced=false grew=1 len=5 inspected=1
HONEST LIMIT: `grew > 1` never occurs on this fixture, so the `take(grew)` slice is
always ONE frame and the widened loop CANNOT be shown here inspecting several
frames from a single beat. What IS measurable is the other half of the widening —
arms now run on the 3 non-forced beats as well as the 2 forced ones, 5 inspected
frames against 2 before — and that half is proved two-sided below.

Mutants at the final tree, `engine.rs` restored byte-identically after each
(file sha256 `ed92a21b6715739f…`; `loop_shortcut.rs` `9684389b859345c0…`, both
re-measured against the COMMITTED content after the `saturating_sub` fix, not a WIP
tree):
* `state.priority_player = PlayerId(3);` after the answer-site sync ⇒ ARM (2),
  `loop_shortcut.rs:11352`, beat 5, `left: PlayerId(3)` / `right: PlayerId(0)`.
* `state.waiting_for = WaitingFor::GameOver { winner: None };` same place ⇒ ARM (1),
  `:11365`, beat 5, `left: GameOver { winner: None }` /
  `right: Priority { player: PlayerId(0) }`. A DIFFERENT arm answers each.
* NEW, the widening's own two-sided control — `state.priority_player = PlayerId(3);`
  immediately before the SETTLE sampler's `record_loop_detect_sample()` ⇒ ARM (2),
  `:11352`, BEAT 0, a beat the probe measured as `forced=false`. TRIVIALIZE arm:
  the same mutant with the arms narrowed back to forced beats only (one-line
  `continue`, the pre-widening scope) PASSES. So the widening is measured coverage,
  not decoration — it catches a settle-frame defect that the predecessor could not
  see. Both files restored byte-identically after every run.

CENSUS PIN re-derived, not assumed: `engine.rs`'s two citation edits are 1:1 line
substitutions, the producer is still `engine.rs:11583`, sha256 prefix
`8a544e878d3e77fb` unchanged, pin literal unchanged.

THREE CORRECTIONS TO `e7e50229b`'s OWN MESSAGE, since under squash-merge that text
lands. They are corrections, not restatements:
1. It cited the mutant failures at `loop_shortcut.rs:11327` and `:11337`. Those were
   measured BEFORE a subsequent doc edit inserted 6 lines above them, and the
   message was written after. The committed positions were `:11333`/`:11343`; at
   THIS tree they are `:11352`/`:11365` as measured above. The lesson is the one
   this branch keeps re-learning: a coordinate measured before a later edit is not
   a coordinate.
2. It listed `drive_loop_action_iteration`'s ten dispatches as `:4087 :4118 :4150
   :4173 :4200 :4222 :4231 :4256 :4271 :4303`. Each is exactly 1 low — the same
   commit's `bounded_cycle_offer` fix added one line above the function. Re-derived
   at this tree (`fn` spans `:4063`..`:4320`): `:4088 :4119 :4151 :4174 :4201 :4223
   :4232 :4257 :4272 :4304`. The COUNT, 10, re-derives exactly; only the
   coordinates were wrong.
3. It gave the restored `loop_shortcut.rs` as sha256 `45b33588f08cc6cf…` without
   saying what that digest was of. `git cat-file -t` reports it is not a valid
   object, and it never could be: it is `sha256sum` of the working-tree FILE, not a
   git blob id (git hashes a header plus content). It also described a WIP tree —
   the file was edited again before the commit — so it corresponds to no committed
   state either. Restoration digests in this message are labelled as file digests
   and were taken against the committed content.

CLIPPY CAUGHT ONE OF MINE, recorded because the process point matters more than
the token. The first cut of the underflow guard was
`after_len.checked_sub(before).unwrap_or(0)`, which `cargo test` compiles happily
and `clippy::manual_saturating_arithmetic` rejects under `-D warnings`
(`loop_shortcut.rs:11297`). I had verified the change with targeted `cargo test`
runs before committing and let the battery find it — the battery did its job, but
the cheap gate belonged before the commit, not after. Now `saturating_sub`, which
is what the guard meant: 0 on underflow, and the `grew >= 1` assertion below turns
that 0 into a named failure.

Assisted-by: ClaudeCode:claude-opus-5
…ch both routes a scalar cannot name

Maintainer review on phase-rs#7023 (matthewevans, CHANGES_REQUESTED at `483ce27d3`), confirming
CodeRabbit's still-open Major. Test-only; no production behaviour change. Both claims were
re-verified at source before any edit, and both are true.

THE DEFECT, stated before it is fixed. The row inferred a SET of minted frames from a SCALAR:
it detected a changed `Arc::as_ptr` on the ring's back, computed `grew = after_len - before_len`,
and validated `rev().take(grew)`. Two production routes break that inference.

  (1) EVICTION AT CAPACITY. `GameState::record_loop_detect_sample` calls `pop_front()` and THEN
      `push_back()` once `loop_detect_ring.len() == LOOP_DETECT_RING_CAP`. The back changes while
      the length stays equal, so `grew == 0` and the row's `assert!(grew >= 1, ..)` FAILED THE
      TEST on a legitimate beat. That guard was labelled "fail closed"; the label was wrong about
      which side of the line the beat is on, and it has gone with the code.
  (2) CLEAR-AND-REBUILD. `apply_action` clears the ring at the TOP of the beat for any action
      that is neither `PassPriority` nor `OrderTriggers` answering a non-forced window, and a
      sampler then mints LATER in the same `apply()`. Net growth is `minted - cleared`, so
      `take(grew)` silently skips frames the row claims to validate. The sentence "a cleared ring
      minted nothing, so the beat is skipped" was untrue for exactly this beat and is also gone.

FIXED BY DELETING THE SCALAR, not by re-deriving it more carefully. The row snapshots the
COMPLETE pre-beat membership of `loop_detect_ring` and validates every remaining frame whose
identity was absent from that snapshot. Append, eviction and clear-and-rebuild are then covered
uniformly, the ring's capacity stops mattering to the row, and `grew` appears in no assertion.
The per-frame validation logic (arms (1)/(2)) is untouched — only how its frame set is obtained.

The snapshot holds `Arc` CLONES, not raw addresses, and that is load-bearing rather than
defensive: `pop_front` DROPS the evicted allocation before `Arc::new` claims a new one of
identical layout, so an address-keyed snapshot can be aliased by the allocator handing the freed
block straight back, and a genuinely new frame would then read as an old one. A retained strong
reference makes every snapshotted address un-reusable for the beat, so `ptr_eq` is exact by
construction. (Aliasing was not observed on these drives — 0 over the `dellian` drive — which is
why the reason is construction and not a measured near-miss.)

REACHABILITY SHIPS WITH THE FIX rather than after it. The row's own `dina_conqueror_4p` drive
reaches NEITHER route — measured: max ring 5 against a capacity of 16, 0 evicting beats, 0
clearing beats — so correcting the detector alone would have re-opened the evidential hole that
sent the PR back. Two new rows put each route on a board where it is live, both driven through
production `apply()`:

  * `an_evicting_beat_mints_without_growing_the_ring` — `dellian_emblem_conqueror_4p`, ordinary
    pass-the-drain drive. MEASURED: beat 72, ring 16 -> 16, 1 minted, 1 dropped, net growth 0;
    three more evicting beats inside the first 90.
  * `a_clearing_beat_rebuilds_the_ring_inside_the_same_beat` — `dina_conqueror_4p`, the same
    board plus one `GameAction::SetAutoPass { UntilStackEmpty }`, which is the exact payload the
    client's Arena-style "Resolve All" control dispatches. MEASURED: beat 6, ring 2 -> 1, 2
    dropped, 1 minted, net growth 0 against a real mint.

`SetAutoPass` has to be dispatched by name because it is absent from
`ai_support::legal_actions_for_viewer` (`classify_flat_priority_action` files it with the
preference-propagation actions), so the generic dump driver never picks it. A survey of the
tracked 4p dumps under the driver's own policy found no natural clear-and-rebuild beat: on
`fantastic_four_bounded_loop_4p` every non-`Priority` window met with a non-empty ring is a
FORCED one, which is precisely the class `apply_action`'s clear exempts.

Both new rows SEARCH ON A STRUCTURAL PREDICATE AND ASSERT THE CONSEQUENCE. The witness is the
first beat that lost a pre-beat frame without the ring shrinking (ⓔ) or that lost every pre-beat
frame (ⓒ) — statements about membership alone — and what is then asserted is what the beat minted
and what its length did. A witness selected on "minted while the length stood still" would have
carried its own conclusion into the arm that claims to test it.

ALSO ADDED, and it is a reach guard rather than decoration: `frames_validated == ring_at_offer`
in the answer-beat row. Every frame the offer's ring holds must have reached arms (1)/(2) as a
member of some beat's minted set, so a detector that silently returned the EMPTY set on a minting
beat — exactly what a length delta returns at capacity — can no longer pass quietly.

DRIVE COST. `drive_one_beat_passing_fast` takes `dump_drive_one_beat`'s `Priority` arm directly:
that policy is unconditionally "pass", and routing it through the per-viewer enumerator is the
dominant cost of a long drive on the 152-entry `dellian` stack. `apply` performs the real
legality check itself, so nothing is skipped but the enumeration. Measured equivalence, not
assumed: the eviction row's own mutant panics name beat 72 — the same first evicting beat the
enumerator-driven probe measured. The 120-beat dellian drive fell from ~9 min to ~2 min.

MUTANTS, per assertion, two-sided, each pair flipping its own named assertion. PRISTINE controls
(comment-only rebuild) pass for all three rows; the harness reports SETUP-ERROR unless libtest
executed exactly one test, after a first run produced two vacuous PASSes from `--exact` against
an unqualified test name.

  frames_validated  DROP `.skip(1)` on the post-beat ring -> FAIL 4 vs 5
                    TRIV minted filter `true`             -> FAIL 49 vs 5
  arm (1)           prod `waiting_for = GameOver`         -> FAIL beat 5, GameOver vs Priority{P0}
  arm (2)           prod `priority_player = PlayerId(3)`  -> FAIL beat 5, PlayerId(3) vs PlayerId(0)
  ⓔ reach           prod no pop at capacity               -> FAIL reach-guard, max ring 28
                    prod `LOOP_DETECT_RING_CAP = 4096`    -> FAIL reach-guard
  ⓔ shape           prod eviction made 2-for-2            -> FAIL beat 67, (2,16) vs (1,16)
                    TRIV minted filter `false`            -> FAIL beat 72, (0,16) vs (1,16)
  ⓒ reach           prod top-of-beat clear neutered       -> FAIL reach-guard
                    prod `SetAutoPass` added to exemption -> FAIL reach-guard
  ⓒ shape           prod sampler pushes twice             -> FAIL beat 1, (0,0) vs (1,1)
                    TRIV minted filter `false`            -> FAIL beat 6, (0,1) vs (1,1)

ONE MUTANT MISSED ITS TARGET AND IS RECORDED RATHER THAN DROPPED: making the sampler push twice
flips ⓔ's REACH guard, not its shape assertion, because the doubled push steps the length
15 -> 17 so `len == LOOP_DETECT_RING_CAP` never holds and the ring never evicts at all. ⓔ's DROP
arm is the 2-for-2 eviction instead.

Assisted-by: ClaudeCode:claude-opus-5
@lgray
lgray force-pushed the fix/loop-shortcut-answer-beat-hardening branch from 483ce27 to b90042d Compare August 5, 2026 11:35
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@lgray

lgray commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

@matthewevans — both mechanisms confirmed at source and fixed. Pushed as b90042d66 (rebased onto current main; CI is running).

Your [MED] was right, and understated in one respect. At capacity record_loop_detect_sample pops the front then pushes, so the back changes while the length does not — which meant assert!(grew >= 1) failed the test on a legitimate beat rather than leaving the frame unvalidated. I had labelled that guard "fail closed"; it was not. A safety label is earned by a census of the production paths that reach the trip condition, and mine trips on an ordinary drive once the ring fills.

Fix. back(), grew, saturating_sub and the assertion are deleted rather than re-derived. The minted set is now the ring's pre/post membership difference, snapshotted as Arc clones rather than addressespop_front drops the evicted allocation before Arc::new can claim one of identical layout, so an address-keyed snapshot can alias. A new reach guard requires the difference to be non-empty on every minting beat.

Both routes are now live on real fixtures through production apply(), not argued:

  • Eviction at capacitydellian_emblem_conqueror_4p, ordinary drive. First evicting beat is 72: ring 16→16, minted 1, dropped 1, net 0. Three more inside 90 beats.
  • Clear-and-rebuilddina_conqueror_4p plus one SetAutoPass{UntilStackEmpty} at the first priority window with two accumulated frames. Beat 6: ring 2→1, dropped 2, minted 1, net 0. That payload is the client's "Resolve All"; it is absent from legal_actions_for_viewer, which is why it has to be dispatched by name.
  • The pre-existing row's own board reaches neither — max ring 5 against a capacity of 16, zero evicting and zero clearing beats. That is now stated in its doc instead of left implicit, which is the omission that let the original defect through.

Each assertion has a drop arm and a trivialize arm flipping that assertion, including reach witnesses for both new rows.

Two things worth your attention rather than buried in the body. The mutant harness first reported two vacuous passes — --exact matches the full libtest path, so a bare function name filtered every test out and libtest exited 0. Fixed with a qualified path plus a ran() == 1 positive control, and all figures re-measured. And one mutant misses its intended target: the doubled-push mutation steps the length past the capacity check, so it flips the eviction row's reach guard rather than its shape assertion; the shape arm is a 2-for-2 eviction instead. Recorded in the body rather than dropped.

Two of the three LOW diagnostic findings I disclosed earlier are still open and still disclosed; the third is closed by this commit. The PR body is re-measured and re-bound to this head.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/engine/tests/integration/loop_shortcut.rs (2)

11514-11527: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse ring_membership_delta in the answer-beat loop.

ring_membership_delta re-implements the same Arc::ptr_eq membership filter that the answer-beat loop already writes inline at Lines 11312-11316. The two copies can drift, and only one of them is exercised by the two new regressions.

Make the helper return the minted frames and derive the counts from it, so all three call sites share one definition.

♻️ Proposed shape
-fn ring_membership_delta<T>(
+fn minted_frames<'a, T>(
+    before: &[std::sync::Arc<T>],
+    after: &'a std::collections::VecDeque<std::sync::Arc<T>>,
+) -> Vec<&'a std::sync::Arc<T>> {
+    after
+        .iter()
+        .filter(|f| !before.iter().any(|b| std::sync::Arc::ptr_eq(b, f)))
+        .collect()
+}
+
+fn ring_membership_delta<T>(
     before: &[std::sync::Arc<T>],
     after: &std::collections::VecDeque<std::sync::Arc<T>>,
 ) -> (usize, usize) {
-    let minted = after
-        .iter()
-        .filter(|f| !before.iter().any(|b| std::sync::Arc::ptr_eq(b, f)))
-        .count();
+    let minted = minted_frames(before, after).len();
     let dropped = before
         .iter()
         .filter(|b| !after.iter().any(|f| std::sync::Arc::ptr_eq(b, f)))
         .count();
     (minted, dropped)
 }

As per coding guidelines, "reuse existing helpers instead of writing duplicate string or collection logic".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/loop_shortcut.rs` around lines 11514 - 11527,
Update ring_membership_delta to return the minted Arc frames rather than only
counts, then derive minted and dropped counts from that shared result as needed.
Replace the answer-beat loop’s inline Arc::ptr_eq membership filtering around
the existing call sites with this helper, ensuring all three call sites use one
membership definition.

Source: Coding guidelines


11693-11730: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind the clear-and-rebuild witness to the dispatched beat.

The loop dispatches SetAutoPass on one beat, but the witness predicate at Line 11719 accepts any beat that lost every pre-beat frame. A future fixture change or an unrelated clearing beat can satisfy the predicate, and the assertion message at Lines 11743-11750 would then attribute the clear to a SetAutoPass press that did not happen on that beat.

Record whether the current beat is the dispatched one, and require that flag in the witness predicate. The row then proves the route it names instead of relying on the measured "0 clearing beats" property of this dump.

♻️ Proposed fix
-        let outcome = if !fired
+        let dispatched_here = !fired
             && before.len() >= 2
-            && matches!(state.waiting_for, WaitingFor::Priority { .. })
-        {
+            && matches!(state.waiting_for, WaitingFor::Priority { .. });
+        let outcome = if dispatched_here {
             fired = true;
@@
         let (minted, dropped) = ring_membership_delta(&before, &state.loop_detect_ring);
-        if before.is_empty() || dropped != before.len() {
+        if !dispatched_here || before.is_empty() || dropped != before.len() {
             continue;
         }

As per path instructions, flag constructor or witness shortcuts that can silently mask the very bug a regression test claims to catch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/loop_shortcut.rs` around lines 11693 - 11730,
Track whether the current loop beat is the one that dispatches SetAutoPass in
the outcome branch, and require that flag alongside the existing ring-clearing
conditions before assigning clearing. Update the beat loop around the
SetAutoPass dispatch and the clearing witness predicate so the recorded witness
can only refer to that dispatched beat, preserving the existing outcome handling
and assertions.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/engine/tests/integration/loop_shortcut.rs`:
- Around line 11514-11527: Update ring_membership_delta to return the minted Arc
frames rather than only counts, then derive minted and dropped counts from that
shared result as needed. Replace the answer-beat loop’s inline Arc::ptr_eq
membership filtering around the existing call sites with this helper, ensuring
all three call sites use one membership definition.
- Around line 11693-11730: Track whether the current loop beat is the one that
dispatches SetAutoPass in the outcome branch, and require that flag alongside
the existing ring-clearing conditions before assigning clearing. Update the beat
loop around the SetAutoPass dispatch and the clearing witness predicate so the
recorded witness can only refer to that dispatched beat, preserving the existing
outcome handling and assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 044aebe8-8074-4ffc-bc4c-2a9c36dd31bd

📥 Commits

Reviewing files that changed from the base of the PR and between 152b368 and b90042d.

📒 Files selected for processing (3)
  • crates/engine/src/analysis/resource.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/tests/integration/loop_shortcut.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/engine/src/analysis/resource.rs
  • crates/engine/src/game/engine.rs

@matthewevans matthewevans self-assigned this Aug 5, 2026
Use the shared membership helper for the answer-beat validator and bind the clear-and-rebuild witness to the SetAutoPass dispatch beat.

Co-authored-by: lgray <lgray@users.noreply.github.com>

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review head: f2df77c

The prior membership-delta finding and both current-head test-witness comments are resolved. The answer-beat validator, eviction row, and clear/rebuild row now share the Arc-identity minted-frame definition; the clear/rebuild witness additionally requires the exact beat that dispatched SetAutoPass UntilStackEmpty.

Re-review found no remaining implementation findings.

@matthewevans matthewevans added the bug Bug fix label Aug 5, 2026
@matthewevans
matthewevans enabled auto-merge August 5, 2026 12:09
@matthewevans matthewevans removed their assignment Aug 5, 2026
@matthewevans
matthewevans added this pull request to the merge queue Aug 5, 2026
Merged via the queue into phase-rs:main with commit 562e7b5 Aug 5, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) test Add tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants