Skip to content

fix(engine): commit every axis a bounded loop-shortcut offer publishes - #7005

Merged
matthewevans merged 10 commits into
phase-rs:mainfrom
lgray:fix/f4-published-set-completeness
Aug 5, 2026
Merged

fix(engine): commit every axis a bounded loop-shortcut offer publishes#7005
matthewevans merged 10 commits into
phase-rs:mainfrom
lgray:fix/f4-published-set-completeness

Conversation

@lgray

@lgray lgray commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Summary

A bounded loop-shortcut offer could publish a candidate set that its own accept then failed to commit: the user's 4-player capture (MODE2) accepted a Fixed(n) grant and the board moved on no axis. Root cause is a sampler blind spot — entries announced across a forced pre-priority window never appeared in any settle frame's stack, so the announced set was incomplete and the period boundary was measured against a premise that no longer held. This widens the sampler to the forced-window answer beat, gives the period boundary and the "is this may already answered" gate one authority each, discharges a loop's replacement obligations against the live board, and pins both user captures as fixtures.

Files changed

  • crates/engine/src/game/engine.rs — forced-window answer sampling site; published_period_elapsed authority
  • crates/engine/src/analysis/resource.rs — period-boundary and homogeneity notes re-derived against the two-site premise
  • crates/engine/src/game/effects/mod.rsupfront_optional_gate, the single authority for whether a may is already answered
  • crates/engine/src/game/replacement.rs — replacement obligations discharged against the live board
  • crates/engine/src/types/game_state.rs — comment-only: record_loop_detect_sample and loop_detect_ring docs re-derived
  • crates/engine/tests/integration/fantastic_four_bounded_loop.rs — fix-bar rows and axis assertions
  • crates/engine/tests/integration/loop_shortcut.rs — rows whose premise was the sampler's blind spot
  • crates/engine/tests/fixtures/f4_user_mode2_accept_commits_nothing_4p.json.gz — the user capture this PR exists to fix
  • crates/engine/tests/fixtures/f4_user_mode1_no_offer_4p.json.gz — the sibling capture (no offer)

Track

Developer

LLM

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

Implementation method (required)

Method: /engine-implementer

CR references

CR 732.2a, CR 732.2b, CR 732.2c (shortcut conformance and the published certificate), CR 603.5 (optional-trigger gate), CR 603.3, CR 603.3b, CR 603.12a, CR 614.1, CR 616.1, CR 608.2b, CR 608.2c, CR 608.2d, CR 704.5a, CR 701.34a, CR 121.2, CR 122.1, CR 117.3a, CR 104.4b, CR 101.4.

One correction worth calling out: a pre-existing comment cited CR 614.1a for "scopes a definition to its controller's events". 614.1a is verbatim "Effects that use the word 'instead'", which cannot cover skips, enters-with, turned-face-up, or virtual replacements. Corrected to CR 614.1. The three remaining 614.1a mentions in this diff are the corrective notes themselves, not new claims.

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.

  • cargo test -p phase-engine --libtest result: ok. 18501 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out; finished in 48.96s

  • cargo test -p phase-engine --test integrationtest result: ok. 4513 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 211.14s

  • cargo clippy --workspace --all-targets -- -D warnings — EXIT=0, zero warning/error lines

  • cargo fmt --all — EXIT=0

  • fantastic_four_bounded_loop::a1_the_users_accept_committed_nothing_board_now_commits_on_every_axisok (the fix bar: the user's accepted Fixed(n) now commits on life, library, counters and tokens, each against the offer's own published per-cycle signature rather than a constant)

Baseline movement reconciled, not waved through. These totals sit above the pre-rebase baselines (lib 18487, integration 4487) purely because of upstream: added #[test]/#[tokio::test] across dcb8f3808..b654513cb is crates/engine/src +14/−0 and crates/engine/tests +26/−0 (#6999 lib+11/int+2, #6996 lib+3/int+15, #6946 int+5, #6997 int+4, #6998 and #7001 zero). lib +14 and integration +26 are fully attributed; zero unexplained movement.

Non-vacuity of the new assertions. Every new assertion and guard was revert-probed and flips alone, as a typed assertion failure rather than a harness crash: ablating the sampler widening reproduces the user's symptom exactly (left: 0, right: -1 on P0's library); bypassing the seat loop flips the recovered exact counter equality (left: 0, right: 2); zeroing each published rate fires the life, library and counter anti-vacuity guards individually.

Gate A

Gate A PASS head=d51d3b6e0055ee54e13185d4f49977e41508cac6 base=b654513cb391203fa1add4dbc797dc21aa9f429e

Stated honestly: the gate's scope is crates/engine/src/parser, and this PR changes 0 files there (9 files in range, all outside it). 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:6049 — the settle-beat record_loop_detect_sample() call, the pre-existing authority for recording a loop-detect frame. The new forced-window site (:11192) mirrors its conjunct set, differing only in that answering_forced_window replaces resolved_this_beat.
  • crates/engine/src/game/effects/mod.rs:5844optional_effect_is_infeasible, the pre-existing single authority for "this optional effect cannot be taken". upfront_optional_gate (:6013) extends that same seam rather than adding a parallel check, and preserves all three of its arms.

Final review-impl

Final review-impl PASS head=d51d3b6e0055ee54e13185d4f49977e41508cac6

Claimed parse impact

None.

Scope Expansion

None.

Validation Failures

None.

CI Failures

None.

Summary by CodeRabbit

  • Bug Fixes

    • Improved loop detection and bounded shortcut handling across more decision and replacement scenarios.
    • Optional choices are now offered only when eligible and feasible.
    • Stored accepted choices are correctly recognized during loop certification, while declines remain safely handled.
  • Tests

    • Expanded coverage for multiplayer loops, replacement effects, target selection, feasibility checks, and captured-game scenarios.
    • Updated bounded-loop expectations and published decision handling.
  • Documentation

    • Clarified loop-sampling behavior and replacement-candidate rules.

@lgray
lgray requested a review from matthewevans as a code owner August 4, 2026 23:02
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@lgray, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 483644e2-aa56-4271-92e5-e810d7903ec6

📥 Commits

Reviewing files that changed from the base of the PR and between acfeaf0 and a6d1a0e.

⛔ Files ignored due to path filters (2)
  • crates/engine/tests/fixtures/f4_user_mode1_no_offer_4p.json.gz is excluded by !**/*.gz
  • crates/engine/tests/fixtures/f4_user_mode2_accept_commits_nothing_4p.json.gz is excluded by !**/*.gz
📒 Files selected for processing (7)
  • crates/engine/src/analysis/resource.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/replacement.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/tests/integration/fantastic_four_bounded_loop.rs
  • crates/engine/tests/integration/loop_shortcut.rs
📝 Walkthrough

Walkthrough

The PR centralizes optional-gate evaluation, recognizes stored Accept choices during certification, adds forced-window loop sampling, measures candidate spans, and updates bounded-loop tests for published decision slots and declared victim data.

Changes

Loop certification and bounded shortcut behavior

Layer / File(s) Summary
Optional gate authority and certification relief
crates/engine/src/game/effects/mod.rs, crates/engine/src/analysis/resource.rs, crates/engine/src/game/engine.rs, crates/engine/src/game/replacement.rs
Optional gate checks use upfront_optional_gate. Stored Accept choices provide certification relief, while Decline remains fail-closed. Resolution checks live replacement state when required.
Candidate windows and dual-site loop sampling
crates/engine/src/game/engine.rs, crates/engine/src/analysis/resource.rs, crates/engine/src/types/game_state.rs
The engine shares newest-first candidate-window traversal, uses published_period_elapsed, and records samples at settle and forced-window answer sites.
Fantastic Four bounded-loop validation
crates/engine/tests/integration/fantastic_four_bounded_loop.rs, crates/engine/src/analysis/resource.rs
Tests validate published decision points, declared victim damage, exact fixed-count commits, replacement definitions, and measured cycle scaling.
Published decision-slot shortcut tests
crates/engine/tests/integration/loop_shortcut.rs
Tests use engine-published DecisionSlot values and distinguish bounded offers with non-empty target schemas from explicitly staged empty schemas.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GameEngine
  participant GameState
  participant ResourceAnalysis
  participant IntegrationTests
  GameEngine->>GameState: record settle or forced-window samples
  GameState-->>GameEngine: provide ring frames and period state
  GameEngine->>ResourceAnalysis: provide measured candidate windows
  ResourceAnalysis-->>GameEngine: certify bounded loop
  GameEngine->>IntegrationTests: publish decision slots and cycle signature
  IntegrationTests-->>GameEngine: submit accepted fixed-count grant
Loading

Possibly related PRs

Suggested labels: needs-maintainer

Suggested reviewers: matthewevans

🚥 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 describes the main fix: committing every axis published by a bounded loop-shortcut offer.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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: 4

🧹 Nitpick comments (3)
crates/engine/tests/integration/fantastic_four_bounded_loop.rs (1)

764-773: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse accept_all_opponents in r2a instead of the inline Accept loop.

Lines 764-773 duplicate the body of the new accept_all_opponents helper at lines 169-184. The helper also returns the responder count, which lets this row assert that the CR 732.2c window really opened for at least one seat. Replacing the inline loop removes the duplicate and keeps the local use engine::analysis::loop_check::ShortcutResponse; import unnecessary here.

♻️ Proposed refactor
-        while let WaitingFor::RespondToShortcut { player, .. } = state.waiting_for.clone() {
-            apply(
-                &mut state,
-                player,
-                GameAction::RespondToShortcut {
-                    response: ShortcutResponse::Accept,
-                },
-            )
-            .expect("each living opponent accepts (CR 732.2c)");
-        }
+        let responders = accept_all_opponents(&mut state);
+        assert!(
+            responders > 0,
+            "n={n}: at least one living opponent must have answered the CR 732.2c window"
+        );
🤖 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/fantastic_four_bounded_loop.rs` around lines
764 - 773, In the r2a setup, replace the inline WaitingFor::RespondToShortcut
Accept loop with the existing accept_all_opponents helper, capture its returned
responder count, and assert that at least one opponent responded to confirm the
CR 732.2c window opened. Remove the now-unused local ShortcutResponse import if
it is only referenced by this loop.
crates/engine/tests/integration/loop_shortcut.rs (2)

10939-10957: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

A board with a naturally empty live schema already exists in this file.

The doc discloses the reachability downgrade correctly and names the remedy: "a fixture whose live offer publishes nothing". drain_offer_schema_is_empty_until_lethal at line 4091 already asserts exactly that on reach_2p_optional_drain_offer — its live schema's points is empty with no staging at all.

Consider building this row on that board instead of staging points: vec![] onto the R5 offer. That restores natural reachability for the !offer.schema.points.is_empty() skipped path and removes the disclosed downgrade. The owner firewall does not depend on R5's 4-player seat set, so the smaller board should serve.

If you prefer to keep the staged form, no change is needed — the disclosure and the matched positive already make the row honest about what it proves.

As per path instructions: test adequacy is the highest-frequency contributor finding, and a row must drive the engine through its production pipeline.

🤖 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 10939 - 10957,
Update r28_a_the_owner_firewall_is_reached_on_an_empty_schema_offer_too to use
the naturally empty live-schema fixture reach_2p_optional_drain_offer, following
the drain_offer_schema_is_empty_until_lethal setup, instead of staging an empty
schema through r28_empty_schema_offer. Preserve the matched honest-declaration
assertion while driving the owner firewall through the production pipeline with
no schema-point staging.

Source: Path instructions


11048-11056: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Re-derive this comment: the mutation it justifies is gone, but the decode dependency is real.

r5_reach_offer() produces a per_cycle: None ShortcutProposal, so the decode still depends on that precondition. Update the comment to say the row does not mutate per_cycle because the fixture already carries none, and assert proposal.per_cycle.is_none() before round-tripping if this row must guard against future populated proposals.

🤖 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 11048 - 11056,
The comment above the ingress fixture is stale and must reflect that
r5_reach_offer() already creates a ShortcutProposal with per_cycle set to None.
Update it to explain that the row preserves the existing per_cycle value rather
than nulling it, and add an assertion on proposal.per_cycle.is_none() before
round-tripping if this fixture should reject future populated proposals.
🤖 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/src/game/effects/mod.rs`:
- Around line 6006-6007: Update the documentation comment for the
UpfrontOptionalGate prompt_player field to replace the incorrect CR 117.3a
citation with the governing optional-effect resolution rule, such as CR 608.2d,
while preserving the clarification that optional_prompt_player identifies the
player making the choice rather than necessarily the controller.

In `@crates/engine/src/game/engine.rs`:
- Around line 2644-2668: Review entry_publishes_pin_slots and its
upfront_optional_gate Probe path to determine whether current fixtures contain
optional, non-repeat, non-optional_for CastFromZone entries. If they do, avoid
repeated deep-clone dry-runs from optional_effect_is_infeasible by memoizing
results per beat using stack-entry identity, or expose the cost separately as
appropriate; otherwise document or validate the fixture assumption without
changing unrelated behavior.

In `@crates/engine/tests/integration/fantastic_four_bounded_loop.rs`:
- Around line 1775-1782: Make the source-name resolution in the match handling
`YieldTarget::ThisObject` panic when `state.objects.get(source_id)` returns
`None` instead of synthesizing an `obj<id>` name. Preserve the existing
cloned-name behavior for resolvable objects and the panic for unexpected yield
targets, so negative assertions cannot pass with an unresolvable source.
- Around line 777-787: Update the assertion in the bounded-loop test to cast
each library count to i64 before subtraction, matching the safe ordering used by
accept_a_fixed_grant. Also retain and use the bound _certificate’s published
per-cycle delta to compute the expected life and library changes for
i64::from(n), instead of hard-coding one per repetition.

---

Nitpick comments:
In `@crates/engine/tests/integration/fantastic_four_bounded_loop.rs`:
- Around line 764-773: In the r2a setup, replace the inline
WaitingFor::RespondToShortcut Accept loop with the existing accept_all_opponents
helper, capture its returned responder count, and assert that at least one
opponent responded to confirm the CR 732.2c window opened. Remove the now-unused
local ShortcutResponse import if it is only referenced by this loop.

In `@crates/engine/tests/integration/loop_shortcut.rs`:
- Around line 10939-10957: Update
r28_a_the_owner_firewall_is_reached_on_an_empty_schema_offer_too to use the
naturally empty live-schema fixture reach_2p_optional_drain_offer, following the
drain_offer_schema_is_empty_until_lethal setup, instead of staging an empty
schema through r28_empty_schema_offer. Preserve the matched honest-declaration
assertion while driving the owner firewall through the production pipeline with
no schema-point staging.
- Around line 11048-11056: The comment above the ingress fixture is stale and
must reflect that r5_reach_offer() already creates a ShortcutProposal with
per_cycle set to None. Update it to explain that the row preserves the existing
per_cycle value rather than nulling it, and add an assertion on
proposal.per_cycle.is_none() before round-tripping if this fixture should reject
future populated proposals.
🪄 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: e1eaea0e-2895-4961-9936-ef3f0300ef87

📥 Commits

Reviewing files that changed from the base of the PR and between b654513 and d51d3b6.

⛔ Files ignored due to path filters (2)
  • crates/engine/tests/fixtures/f4_user_mode1_no_offer_4p.json.gz is excluded by !**/*.gz
  • crates/engine/tests/fixtures/f4_user_mode2_accept_commits_nothing_4p.json.gz is excluded by !**/*.gz
📒 Files selected for processing (7)
  • crates/engine/src/analysis/resource.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/replacement.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/tests/integration/fantastic_four_bounded_loop.rs
  • crates/engine/tests/integration/loop_shortcut.rs

Comment thread crates/engine/src/game/effects/mod.rs Outdated
Comment thread crates/engine/src/game/engine.rs
Comment thread crates/engine/tests/integration/fantastic_four_bounded_loop.rs
Comment thread crates/engine/tests/integration/fantastic_four_bounded_loop.rs
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Generated for head a6d1a0e62ad24dd2eb3447a88f6c7643f9e3c644.

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans matthewevans added the bug Bug fix label 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.

Changes requested

  1. High — forced-window certificate records the wrong state. In game/engine.rs:11185-11194, the sampled frame is recorded before state.waiting_for = wf. When wf is WaitingFor::Priority, is_forced_cascade_window only checks the non-priority side and the equality check preserves the prior waiting_for, so the certificate can record a forced window even though the returned state is priority. Route the returned wf and paired priority state through the existing synchronization authority before recording the sample, then assert both that the sampled frame is Priority and that the immediately following certificate is the exact expected state.

  2. Medium — the optional-choice prompt cites the wrong rule. effects/mod.rs:6006-6007 (and the matching optional_prompt_player documentation) cites CR 117.3a, which covers receiving priority at the start of a step or phase. An optional choice while resolving an effect is governed by CR 608.2d; the local rules text at docs/MagicCompRules.txt:2795 is the relevant authority. Please correct both annotations.

  3. High — absence test can pass vacuously. The helper in fantastic_four_bounded_loop.rs:1775-1782 substitutes obj<ID> when the expected source is absent. That makes the negative Sue-absence assertion at :2035-2040 green without proving that the expected source was present. Require the expected source with expect/panic before constructing the assertion.

These are current-head findings; no unrelated clone-cost or rate concerns are part of this review.

lgray added a commit to lgray/phase that referenced this pull request Aug 5, 2026
…ates from the certificate, and make an unresolvable point source fatal

CodeRabbit left four inline findings on phase-rs#7005. Three hold; the fourth's premise
does not, and the difference is a measurement rather than an argument.

F-A - `UpfrontOptionalGate::prompt_player` cited CR 117.3a ("The active player
receives priority at the beginning of most steps and phases..."), which is
priority timing. The field names the player who ANNOUNCES the choice, which is
CR 608.2d ("...the player announces these while applying the effect."). Both
quotes verified against the rules text before the edit. `optional_prompt_player`'s
own doc carried the identical wrong citation and is fixed with it. Both edits are
line-for-line so the CR 603.5 prompt census keeps its exact pins.

F-B - CodeRabbit asked for memoization "if the fixtures produce optional,
non-repeat, non-`optional_for` `CastFromZone` entries". They do not. Instrumenting
the clone-bearing arm and both `state.clone()` sites, with a thread-local marking
the `Probe` path, over a full `--test integration` run (reproduced bit-for-bit
across two runs): 16402 raw mint calls => 4573 `Probe`-mode feasibility calls => 0
arm entries and 0 clones on that path. The 59 arm entries and 50 clones that do
occur are production's own `Known` path, which pays them once per resolution. The
zero's positive control is in-band: `P` and `K` are two labels from the same
statement, and `K` returned 59. The memo itself already exists for the other
caller - `PeriodVerdicts` is a `(FrameIx, ObjectId)`-keyed compute-on-miss memo
whose `published` field IS `entry_publishes_pin_slots`. Recorded the measured
number at the seam; added no memoization for a cost of zero.

F-C - `(libs_before[0] - libs_after[0]) as i64` subtracted two `usize` before the
cast, so the zero-commit regression the row exists to catch aborted on an
arithmetic overflow instead of printing the row's own diagnostic. Demonstrated
both ways: the old form under the underflow condition panics `attempt to subtract
with overflow` with the diagnostic suppressed; the new form fails as `assertion
left == right` and prints it. The row also asserted `(i64::from(n), i64::from(n))`
- two literals - while its message claimed the published per-cycle delta. Both
rates now come from `certificate.per_cycle.delta`, negated because the axes are
measured as losses, with an anti-vacuity guard so the equality cannot degenerate
to `0 == 0 * n`, and seat ids read positionally so a rate belongs to the seat
whose movement is measured.

F-D - `published_point_names` synthesised `obj<id>` when a point's source was
absent from `state.objects`. Every caller compares that string to the
SUE/REED/TORCH constants, so an unresolvable source read as "not that card" and
silently satisfied m1's negative owner-firewall assertion. Now a panic, matching
the treatment the adjacent `other =>` arm already gave the same class of failure.
The new guard is proven able to fire:
`published_point_names_panics_when_a_points_source_is_absent` deletes the first
published point's source and requires the panic, and reports `should panic ...
FAILED` when the synthetic fallback is restored.

Gates at this tip: lib 18501 passed / 0 failed / 6 ignored; integration 4514
passed / 0 failed / 2 ignored (4513 + the new row); clippy --workspace
--all-targets -D warnings exit 0. The CR 603.5 prompt census and
`a1_the_users_accept_committed_nothing_board_now_commits_on_every_axis` are both
green.

Assisted-by: ClaudeCode:claude-opus-5

@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.

Changes requested

[HIGH] Synchronize the forced-window state before recording the loop sample. Evidence: crates/engine/src/game/engine.rs:11185-11194 receives wf as WaitingFor::Priority but records a loop sample before synchronizing it into state.waiting_for; record_loop_detect_sample snapshots that stale state in crates/engine/src/types/game_state.rs:19527-19535, despite the canonical synchronizer in crates/engine/src/game/public_state.rs:40-44. Why it matters: the loop certificate/ring can claim a forced window while the returned state is actually priority. Suggested fix: call sync_waiting_for(state, &wf) before sampling, and add production-fixture assertions for the newest sampled state, its priority player, and the resulting certificate.

[MED] Refresh the parse-diff sticky for this exact head. Evidence: the current <!-- coverage-parse-diff --> comment identifies d51d3b6e0055ee54e13185d4f49977e41508cac6, while this PR head is 8c5261740aefab7faad1d2f64de85d05a3f0cb6c. Why it matters: the engine/effects changes after that artifact have no current-head parse-surface evidence. Suggested fix: refresh the sticky and ensure it explicitly binds to 8c5261740aefab7faad1d2f64de85d05a3f0cb6c.

@matthewevans

Copy link
Copy Markdown
Member

Current-head correction for 8c5261740aefab7faad1d2f64de85d05a3f0cb6c: the <!-- coverage-parse-diff --> artifact is explicitly generated for this head and reports no card-parse changes, so the parse-artifact requirement is satisfied. The only remaining requested change is the existing HIGH forced-window issue: synchronize sync_waiting_for before recording the forced-window frame/certificate so the sample reflects the returned priority state.

@lgray

lgray commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

Withdrawal: three verification figures in the PR body are stale for the current head

Self-caught while applying a push-boundary integrity check. Withdrawing explicitly rather than editing silently, so the record shows what was wrong and why.

Current head is 8c5261740aefab7faad1d2f64de85d05a3f0cb6c. The PR body's Verification section is bound to d51d3b6e0055ee54e13185d4f49977e41508cac6, two commits back.

body claim measured at true at 8c5261740
--test integration4513 passed … 211.14s 7841e1e93 (.item1repin-int.log, 21:28 UTC) 4514 passed; 0 failed; 2 ignored … 186.40s (.crfix-gates.log, 00:10 UTC)
--lib18501 passed … 48.96s 7841e1e93 count still 18501, but the run is a different one (… 46.34s)
Gate A PASS head=d51d3b6e0… / Final review-impl PASS head=d51d3b6e0… d51d3b6e0 not re-derived at 8c5261740

Cause. The integration count moved because the review-response commit 8c5261740 added a regression row (published_point_names_panics_when_a_points_source_is_absent, the fix for the vacuous by-name absence assertion). So 4513 → 4514 is the expected consequence of that fix, not a regression — but the body still advertises the pre-fix number under the post-fix SHA, and the two head-bound attestations name a SHA that is no longer the head.

Nothing here is a test failure. Every gate is green at the current head; the defect is that published figures outlived the head they were measured at.

Correction plan. The forced-window synchronization change requested in review is in progress. Rather than bind figures to a head that is about to move again, I will re-run the full battery, Gate A, and a fresh review-impl at the new head, and replace the Verification, Gate A and Final review-impl sections wholesale — each explicitly bound to that SHA. This comment stands as the withdrawal of the three claims above.

Process note, since it generalises. The check I was applying compares a gate log's timestamp against the base commit's committer date, which catches pre-rebase numbers published under a post-rebase SHA. It does not catch this case: these numbers postdate the base correctly and are stale anyway, because the branch head moved forward twice after they were taken. A verification figure has to be bound to the head it was measured at and re-measured whenever that head moves — a rebase is only one of the ways it moves.

@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 11287-11304: Update the loop-detection test around
dump_drive_one_beat to require exactly one ring-frame addition per beat, and
fail rather than continue when the ring length does not increase. Capture and
validate the newly added frame directly instead of relying only on
loop_detect_ring.back(), while preserving the existing answer_mints and
settle_mints attribution and reach-guard expectations. Avoid constructor
shortcuts or fallback setup that could mask failures in the frame-recording
behavior.
🪄 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: 48c41dbd-d1a5-4fa2-9844-a59659a87c82

📥 Commits

Reviewing files that changed from the base of the PR and between d51d3b6 and f144bb3.

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

Comment thread crates/engine/tests/integration/loop_shortcut.rs
@matthewevans

matthewevans commented Aug 5, 2026

Copy link
Copy Markdown
Member

Maintainer hold for current head a6d1a0e: re-review found no code finding. Required CI is still running (Rust lint and test shards), and the coverage-parse-diff sticky has not yet been generated for this head. Approval/enqueue consideration resumes after both settle.

lgray added 10 commits August 4, 2026 22:54
…e stack

CR 732.2a's ring sampler had exactly one site: `pass_priority_once_with_pipeline`,
which fires only at an active-player `Priority` settle. Any stack entry that resolves
ACROSS a forced pre-priority window — a CR 608.2b `TriggerTargetSelection`, a CR 603.5
`OptionalEffectChoice`, a CR 603.3b `OrderTriggers` — was therefore never present in
two consecutive retained frames, so `certified_period_touch`'s announced set
("entries in a frame's stack absent from the previous frame's") could not see it and
`bounded_cycle_pin_slots_for_window` could not publish its choice. The shortcut then
described a sequence with unpublished per-iteration choices in it.

This adds the SECOND sampling site, in `apply_action`, keyed on the forced-window flag
captured BEFORE the reducer consumed it. Its conjuncts are the settle sampler's, plus
a non-shrinking-stack guard measured against the pre-action depth.

Consequences carried in this commit rather than left to be discovered:

* the structural pin `arc_as_ptr_beat_identity_is_the_sample_not_one_of_its_halves`
  moves 2 -> 3 `as *const` reads (the shared per-beat `before` plus an `after` read in
  each arm that can advance the ring), so a re-basing onto a field address still flips
  it;
* two doc comments claiming `victim_slot` is "empty on every trajectory that offers
  today" are FALSIFIED by the widening and are replaced, not softened — a `Targets`
  declaration is announced now, so `worst_seat_life_loss` reaches `elimination_bounds`
  in production;
* B5f rows that consequence two-sided on the user's own MODE1 capture (tracked here as
  `f4_user_mode1_no_offer_4p.json.gz`, 860,451 B, derived `jq -c '{gameState}' | gzip
  -9 -n` from the 20.5 MB envelope): with P1 seeded at 7 and 6 the offer FIRES with
  `max_iterations == 1`; at 5 and 4 the drive reaches the same beat, raises nothing,
  and the typed verdict is `NoNarrowedLegalCount`. The arms are ONE life point apart,
  which is what makes the row about the divisor rather than about the board.

Landing FIRST of the five commits is load-bearing: relief without the widening turns a
silent no-offer into a treadmill that offers and commits nothing.

Assisted-by: ClaudeCode:claude-opus-5
`drive_one_shortcut_cycle` delimits a committed repetition two ways: board recurrence,
and — for the certification basis that consults no board predicate at all — the
published `frames_per_period` count. The frame-count arm existed at exactly one beat
kind, the active-player settle, because that was the ring's only sampling site.

With the answer-beat sampler in place that premise is gone: a period whose extra frames
are recorded while a player answers a forced pre-priority window would never reach `k`,
so `frames_per_period` becomes unreachable on precisely the boards the widening was
for, and such a drive can only end at its runaway beat cap having committed nothing.
The injector arm therefore advances the same counter, under the same `Arc`-identity
frame detector the settle arm uses.

Both arms now ask ONE function. `published_period_elapsed(frames_this_cycle,
frames_per_period)` carries the two properties neither call site can state: `None` NEVER
elapses (an offer that published no signature must not have one invented for it, because
ending a cycle early commits a fraction of the published delta — the conditional action
CR 732.2a forbids), and the comparison is `>=` rather than `==` (one beat may retain more
than one frame, and an `==` would drive past its own boundary).

Two regression rows, one on each surface:

* `published_period_elapsed_is_total_over_the_axes_that_delimit_a_cycle` asserts the
  whole truth table, including the `k - 1` and `k + 1` arms that discriminate the
  off-by-one and the `>=`/`==` choice — the anti-vacuity control the structural row
  cannot supply;
* `the_period_delimiter_has_one_authority_and_both_frame_recording_arms_ask_it` censuses
  `drive_one_shortcut_cycle`'s extent with the tree's own comment-excluding extractor:
  exactly two delimiter calls, exactly two counter advances, and ZERO inlined raw
  comparisons, with a proven-live instrument on both sides of the zero census.

Also restores `drive_one_shortcut_cycle`'s doc block, which the delimiter extraction had
silently re-attached to the new function, and corrects its "the single
`record_loop_detect_sample` call site" sentence — there are two sampling sites now.

Assisted-by: ClaudeCode:claude-opus-5
…ive board

Conjunct (6) classifies each announced stack entry on its CARRYING FRAME — a retained
ring sample, and therefore a board from the past. It then discharged the resulting
`FreeUnlessReplacements` obligation against that same frame, which answers the wrong
question: a shortcut is a claim about the FUTURE, and every remaining repetition
resolves under the board that exists NOW. A replacement definition that entered the
battlefield after the sample was taken is invisible to the frame-side check, so the
described sequence could contain exactly the CR 616.1 resolution-time choice CR 732.2a
forbids.

The gate now discharges a second time against `state`, guarded by `!ptr::eq(*frame,
state)` — a de-duplication, not an exemption: when the pair is carried by `current`
itself the first call already ran on that very board.

Two rows, each with its own paired positive:

* `n3_a_replacement_installed_after_the_frame_was_captured_refuses_certification`
  builds a ring whose frames are all cloned BEFORE the definition is installed, so the
  def exists on the live board and nowhere else, and runs four arms — no def
  (certifies), live-only OPTIONAL (refused, the arm this change exists for), live-only
  MANDATORY (certifies, which keys the previous arm to optionality rather than to "a
  definition exists"), and present-everywhere OPTIONAL (refused, proving the frame-side
  discharge still does its own job so this is an ADDED refusal, not a relocated one).
  `announced_from_retained_sample` runs on every arm as the reach-guard that the pair is
  carried by a frame that is not `current`.
* `n3_b_a_live_carried_pair_is_still_discharged_by_the_first_call` exhibits the
  short-circuited shape and shows the optional definition is still refused there.

CR anchors corrected in the same change, because they are about this seam. CR 614.1a is
"effects that use the word instead" — a sub-rule cited for its parent's job. The
prompt-cause authority in `replacement.rs` classifies EVERY applicable replacement,
including skips, enters-with, turned-face-up and virtual candidates that carry no
`ReplacementDefinition` at all, so its anchor is the definitional head CR 614.1; and
what makes an optional replacement disqualify a shortcut is CR 732.2a's ban on
conditional actions, not CR 614.1a. Both `replacement.rs` sites and four r9 sites now
read `CR 732.2a + CR 614.1`, in that order. CR 616.1 stays on the two-or-more ORDERING
branch, where it belongs.

Assisted-by: ClaudeCode:claude-opus-5
…nd answer the shortcut's gate with it

Three places answered "does this ability open ONE up-front optional gate, and to whom,
under which key?" — production's own branch in `resolve_chain_body`, the loop-shortcut
mint's guard (b), and `analysis::resource::auto_may_answer_for`. The latter two asked
the same four predicates and OMITTED two conjuncts the first has: `optional_for` and
the CR 608.2d feasibility probe. Measured, that made them return a different answer on
two ability shapes, and each was defended only incidentally — the fan-out case by a
disjunct in `resolution_prompt.rs` answering a different question, and the infeasible
case by membership of a fail-closed list from which three variants have already been
promoted out.

`effects::upfront_optional_gate` is now the assembler, and production's own branch IS
that function rather than a fourth copy of it. `stored_may_answer` is its consumer half.

`OptionalFeasibility::{Known, Probe}` exists because a naive adoption would have made
production probe TWICE: `resolve_chain_body` has already run
`optional_effect_is_infeasible` for the `CastFromZone` decline early-return that
precedes the gate, and that arm clones the whole `GameState` per bound object to run a
dry-run cast. Adoption A hands its answer over as `Known`; every other caller passes
`Probe`, which the authority evaluates LAST so the clone-bearing arm is reached only for
`optional AND NOT optional_for AND NOT repeat` entries. It is deliberately not charged
against `PROBE_BUDGET`: that counter bounds CR 732.2a certification asks at the verdict
door, and mixing wall-clock cost into it would re-base every metered row's pinned spend.

Guard (b) adopting the authority is a BEHAVIOUR CHANGE and it is rules-correct in the
fail-closed direction. It now withholds the `MayChoice` slot for an `optional_for`
ability — CR 608.2d + CR 101.4 make that an APNAP cascade of up to one window per living
player, and one published slot standing for N prompts is the cardinality defect group
(c) already argues against — and for an infeasible optional, which opens no window at
all, so a slot for it is a pin the gate can never spend. Direction: strictly FEWER
offers, never more.

N0 rides on the same authority: gate (6) now takes relief from a stored auto-choice as
well as from a published pin, and the two bases are disjoint by construction because
guard (b) publishes a slot only for a may with NO stored answer. Reading an
auto-answered may's slotless mint as "unspecified" was the defect. Only `Accept` is
relieved — a stored `Decline` is equally prompt-free but produces the OPPOSITE board, so
its optional-cleared residual would describe events the shortcut never proposes.

Rows, each with its own paired positive:

* `a5_a_stored_accept_relieves_gate_six_and_a_stored_decline_does_not` — one board, one
  key, one value different; the arm the user's MODE1 capture rides on.
* `f2a_the_upfront_gate_authority_answers_the_two_shapes_the_third_copy_omitted` — every
  arm seeds a stored `Accept` under exactly the key the old copy built, so an omitted
  conjunct is a WRONG answer rather than an absent one. Includes the feasibility control
  (the same `RemoveCounter` ability on a board ONE counter different) and the pair that
  proves `Known` overrides the probe instead of re-running it.
* `f2b_guard_b_withholds_a_pin_the_cr_603_5_gate_can_never_spend` — deliberately
  UNSEEDED, so guard (b)'s store conjunct is vacuously true on every arm and the only
  thing that can move `may` is the axis under test. A seeded variant is rejected: it
  cannot fail for the reason the row exists.
* `f2c_the_cr_603_5_conjunct_set_has_one_production_assembler` — MEASURED per-predicate
  production call-site counts 2/2/2/1/2, plus the stronger statement that ZERO production
  consumers live outside `game/effects/`, with a proven-live instrument on the zero
  census. The three surviving non-authority sites select a DRIVER rather than opening an
  up-front window, so folding them in would be wrong, not cleaner — and the guarantee is
  stated honestly: an inline re-derivation from `ability.repeat_for` is NOT caught, and
  no census over these five tokens can catch it.

`cargo clippy -p phase-engine --all-targets -- -D warnings` is clean under the shipped
enum, with zero `is never constructed` (both variants are constructed on production
paths, and F2a exercises `Probe` to opposite outcomes).

Assisted-by: ClaudeCode:claude-opus-5
…nd spot, and pin both user captures

The answer-beat sampling site (C4) widened what a CR 732.2a offer can publish, and
several rows had encoded the OLD blind spot as if it were a property of the board.
Each is re-derived from measurement rather than relaxed, and both of the user's own
captures are tracked and driven end to end.

THE FIX BAR. `a1_the_users_accept_committed_nothing_board_now_commits_on_every_axis`
drives the user's MODE2 capture — the board where the offer fired, the declaration was
accepted, and the drive then committed nothing and re-offered. It now publishes all
three per-iteration choices and the accepted `Fixed(n)` grant commits EXACTLY n
repetitions of the offer's own published per-cycle signature on life and library, with
counters and tokens non-zero at n=1 and exactly 3x at n=3. Revert-probe run: with the
answer-beat site ablated every axis collapses to 0, reproducing the captured symptom.
`m1_...` is its one-field-apart sibling on MODE1 (a STORED CR 603.5 answer, so guard (b)
withholds Sue's slot and the auto-answer relief discharges gate (6) instead).

`--lib` rows re-keyed to production's own walk. `newest_item4_window` consumes
`game::engine::candidate_windows`, so R21(b-placement-B)'s window IS production's rather
than a hard-coded `len - 2` that silently asserted `span == 1`; its reach-guard now
states what it needs (no denied answer; a gate that ASKED must have completed) with the
load-bearing exemption equality byte-identical. R16(ii-b) searches its real construction
requirement (`meter.spent > 0`, never `denied`, which would assert itself) and ships its
own revert-probe as a `RaisedTwiceLinks` positive control. The CR 603.5 prompt census is
re-pinned from the failure's own left side, every producer sha256-identical at its new
coordinate and still inside the function the row names, and its authority count is made
comment-insensitive so prose cannot trip a call-site pin.

Attribution repairs: `r2` is renamed `r2a` to state what its body now asserts; r1 keeps
its over-charge follow-up pointer instead of claiming discharge; the `r5_declare_is_accepted`
citation is replaced with the per-caller measurement that actually exists; the r28
empty-schema arm DISCLOSES that its path is now reached by staging rather than naturally;
a pre-existing `CR 614.1a` comment that described no rule is corrected to CR 614.1.

`ai1_the_bounded_declare_candidate_withdraws_when_the_offer_publishes_a_pin` pins the
generator's `Fixed` candidate to the published pin set in both directions on one board,
and is deliberately not `#[ignore]`d.

Assisted-by: ClaudeCode:claude-opus-5
…unter assertion a false premise cost

Five ACCEPT-WITH-FIXES findings, plus one sibling swept by the same defect
mechanism. `crates/engine/src/` is COMMENT-ONLY this round — proved by
`git diff -U0 crates/engine/src/` having no non-comment +/- line — so the only
executable change is in the F4 test file.

F1. Four docs still asserted the pre-C4 premise that `record_loop_detect_sample`
has ONE call site, and this branch's own policy is to REPLACE a falsified note
rather than soften it. The measured truth is TWO production sites, both after
`run_post_action_pipeline` (CR 603.3): the settle sampler in
`pass_priority_once_with_pipeline` and the forced-window answer site in
`apply_action`. Rewritten at the fn doc and the `loop_detect_ring` field doc
(`game_state.rs`), at `frames_per_period` (`resource.rs` — the justification C2
had already repaired in code), and at `ring_delta_signature`, whose homogeneity
argument now rests on the `Priority{active_player}` conjunct the two sites
SHARE rather than on there being one site.

F1e (swept sibling). `drive_one_shortcut_cycle`'s "the frame counter is advanced
here and nowhere else" was falsified on this same branch by the forced-window
ANSWER arm's own advance. Both arms key the advance on the ring's back
allocation changing, which is what keeps drive and mint one-to-one.

F2. The second site's comment claimed "the same conjuncts as the settle sampler,
PLUS the window flag". Measured, the sets are the same size one member apart:
`answering_forced_window` REPLACES `resolved_this_beat`, and the settle site's
`else { ring.clear() }` has no counterpart here. The comment now says that,
names the consequence (an answer that resolves nothing but leaves the stack
non-shrinking records a duplicate frame), and says why it is acceptable —
`ring_delta_signature` refuses a zero smallest-period delta, and mint/drive are
symmetric because `inject_pinned_answer`'s arms all dispatch `apply_action`.
It also documents the latent ordering asymmetry: this site records BEFORE
`state.waiting_for = wf` while the settle sampler records after
`sync_waiting_for`, and `GameState::eq` compares both `waiting_for` and
`priority_player` while `normalize_for_loop` neutralizes neither. Latent, not
live: a `debug_assert_eq!` census reported 0 failures across 18,486 lib and
4,487 integration rows, with a `debug_assert!(false)` positive control that
fired on the A1 board. Deliberately NOT reordered — that would be a behavioural
change for a non-live defect.

F3. The fix bar's life and library equalities had no anti-vacuity guard, so an
all-zero certificate would satisfy `moved == rate * n` on a board that never
moved. Added, in the existing `assert_axis_scales` idiom.

F4. The counter assertion had been weakened on a false premise. Measured, the
published vector is `counters {(Plus1Plus1, Creature): 2}` — non-zero and
state-readable; only `tokens_created: 0` is event-fed. The real obstacle was the
accessor: `commit_axes` reads ONE object's counters against an AGGREGATE key.
Re-cut against `ResourceVector::snapshot`/`delta`, the accessor the certificate
is minted from, plus a "nothing unpublished may move" arm. The aggregate moves 2
at n=1 and 6 at n=3, i.e. exactly 2n. The token axis keeps the scaling arm alone,
now for its real measured reason.

F5. `has_frozen_window`'s residual was declared as "four authored-ring rows".
Measured: two call sites, and NEITHER is an authored ring — both drive the real
tracked dumps. Overstated in count, understated in kind. The disclosure now
names both rows and the loud floor that lets them keep a hard-coded `span == 1`.

The CR 603.5 prompt census went red on F2's line drift and was re-pinned by its
own protocol, not by matching the tree: `engine.rs:11515 => :11549`, +34 which is
engine.rs's entire (comment-only) delta above the producer, the line
sha256-identical at the new coordinate and still inside
`begin_pending_trigger_target_selection`; total 37 and partition 5/7/25
unchanged.

Every new assertion and guard is proven to flip. Ablating the answer-beat
sampling site fails the library equality at `left: 0 / right: -1` (the prior
probe's signature) and, with the seat loop bypassed so control reaches it, the
recovered counter equality at `left: 0 / right: 2`. Zeroing each published rate
in turn fires each guard alone. All five are typed assertion failures, not
harness crashes.

lib 18487 passed / 0 failed, integration 4487 passed / 0 failed / 2 ignored,
clippy --workspace --all-targets -D warnings exit 0.

Assisted-by: ClaudeCode:claude-opus-5
…ated

The rebase onto upstream `b654513cb` (phase-rs#6996, phase-rs#6999, phase-rs#6998, phase-rs#7001, phase-rs#6997,
phase-rs#6946) resolved the census row's conflict to upstream's three
`effects/mod.rs` literals, which are correct for the upstream tree but not
for this branch replayed on top of it. Re-derived from the row's own failure
output — never predicted by arithmetic — and re-pinned:

  `:6065/:6142/:9324 ⇒ :6175/:6252/:9456`

The shift is NOT uniform (`+110/+110/+132`), and the asymmetry is the
measurement: C1's `upfront_optional_gate` authority is one 110-line hunk
above all three producers, and `resolve_chain_body` takes a further `+22`
from two hunks inside itself and above its own gate (the `optional_for`
coupling note, and adoption A replacing the inline conjunct chain with the
authority call plus its `debug_assert!`). The file's whole-file delta is also
`+132`, so nothing lands below the third producer, and `6065+110`,
`6142+110`, `9324+132` equal the observed coordinates exactly.

Identity re-established rather than assumed. Each producer at its new
coordinate is sha256-identical to `b654513cb:effects/mod.rs` at its old one
and to the pre-rebase tip `117baa6a1` at `:6109/:6186/:9183`, and each is
still inside the enclosing function this row NAMES —
`drive_sequential_repeated_optional_payment`,
`resolve_repeated_optional_payment_choice`, `resolve_chain_body` — which is
stronger evidence than the coordinate. The diff instrument discriminates: in
the new tree the three old coordinates hold a `may_trigger_auto_choice`
lookup, a blank line, and a bare `//`.

`engine.rs:11549` was re-derived too, not carried over, and is UNMOVED:
upstream's six commits net ZERO above it (`:11420` in both the old base
`dcb8f3808` and the new base `b654513cb`), so this branch's own `+95`/`+34`
still land it on `:11549`, byte-identical and still inside
`begin_pending_trigger_target_selection`. `scoped_library_search.rs:452` is
unmoved as well. Two entries holding still while three move is the
set-preservation evidence: the row's first two asserts fired GREEN on the
run that caught this, so the total stays 37 and the partition stays 5/7/25 —
no producer was gained or lost.

Assisted-by: ClaudeCode:claude-opus-5
…d, and re-measure the span==1 residual

The final review at 7841e1e found three survivors of the F1 falsified-doc class plus one
unproven mechanism. A mechanical sweep of the same class found two more the review did not
name, both in the test file the previous round never searched. Comment-only; no behaviour.

r1's doc said the offer "publishes ONE point and commits ZERO cycles (see r1b and r2)". All
three clauses are dead: r1b's own assert_eq! pins THREE points [Sue MayChoice, Reed MayChoice,
Torch Targets]; r2a commits exactly n at n=1 and n=3; and `r2` names a row this branch renamed
(fn-name diff b654513..HEAD: exactly one name disappeared,
r2_an_accepted_declaration_commits_zero_cycles_because_reeds_may_is_unannounced, and zero
references to it survive). r1's second paragraph was falsified too and went unreported: the
in-tree form is the ADDITIVE one (resource.rs observed_life_loss.max(0) +
declared_life_magnitude), not the MAX form, and victim_slot is NON-EMPTY on this board, so the
two forms do not coincide.

r1b's OWN doc block was the sharpest instance and neither round had caught it — it said "403
and 401 are never announced ... publishes exactly ONE point" while the same function's body
asserts three and its message says all four sources are announced. The U6 header's "F4
publishes ONE point, not three" and the Fixed-gate bullet's "F4 publishes one point" are
corrected with the reason preserved: the AI still declines, but on the emptiness gate, never
on the count. resource.rs:10713 is brought into line with the two siblings this branch already
replaced at resource.rs:1062-70 and engine.rs:2257-64, reusing their wording.

has_frozen_window's span==1 residual was justified by an unproven mechanism ("both fail LOUD
on a half period"). A half period is non-degenerate, so those guards do not fire, and both
rows' assertions are span-independent — they would PASS. Re-measured here rather than
transcribed: at the beat drive_dump_until(gz, 80, has_frozen_window) selects, dina beat=6
ring=2 and dellian beat=5 ring=2, and candidate_windows yields exactly one candidate
(idx=0, span=1, len=2) on each, so &live[len-2..] is the whole ring and span==1 is exact. The
residual is restated as "exact today, silent if the sampling rate grows this ring past two".

inject_pinned_answer's "arms all dispatch apply_action" (two sites) is corrected for precision:
four arms, three dispatch, the fourth Err()s before any frame advance. The mint/drive symmetry
conclusion survives and is now stated in the stronger form the code supports.

The engine.rs edit is deliberately line-neutral (3 for 3) so the CR 603.5 census pin at
engine.rs:11549 does not move; verified still on the OptionalEffectChoice producer and the
census row green.

--lib: 18501 passed; 0 failed; 6 ignored.
--test integration: 4513 passed; 0 failed; 2 ignored.
clippy --workspace --all-targets -D warnings: exit 0.

Assisted-by: ClaudeCode:claude-opus-5
…ates from the certificate, and make an unresolvable point source fatal

CodeRabbit left four inline findings on phase-rs#7005. Three hold; the fourth's premise
does not, and the difference is a measurement rather than an argument.

F-A - `UpfrontOptionalGate::prompt_player` cited CR 117.3a ("The active player
receives priority at the beginning of most steps and phases..."), which is
priority timing. The field names the player who ANNOUNCES the choice, which is
CR 608.2d ("...the player announces these while applying the effect."). Both
quotes verified against the rules text before the edit. `optional_prompt_player`'s
own doc carried the identical wrong citation and is fixed with it. Both edits are
line-for-line so the CR 603.5 prompt census keeps its exact pins.

F-B - CodeRabbit asked for memoization "if the fixtures produce optional,
non-repeat, non-`optional_for` `CastFromZone` entries". They do not. Instrumenting
the clone-bearing arm and both `state.clone()` sites, with a thread-local marking
the `Probe` path, over a full `--test integration` run (reproduced bit-for-bit
across two runs): 16402 raw mint calls => 4573 `Probe`-mode feasibility calls => 0
arm entries and 0 clones on that path. The 59 arm entries and 50 clones that do
occur are production's own `Known` path, which pays them once per resolution. The
zero's positive control is in-band: `P` and `K` are two labels from the same
statement, and `K` returned 59. The memo itself already exists for the other
caller - `PeriodVerdicts` is a `(FrameIx, ObjectId)`-keyed compute-on-miss memo
whose `published` field IS `entry_publishes_pin_slots`. Recorded the measured
number at the seam; added no memoization for a cost of zero.

F-C - `(libs_before[0] - libs_after[0]) as i64` subtracted two `usize` before the
cast, so the zero-commit regression the row exists to catch aborted on an
arithmetic overflow instead of printing the row's own diagnostic. Demonstrated
both ways: the old form under the underflow condition panics `attempt to subtract
with overflow` with the diagnostic suppressed; the new form fails as `assertion
left == right` and prints it. The row also asserted `(i64::from(n), i64::from(n))`
- two literals - while its message claimed the published per-cycle delta. Both
rates now come from `certificate.per_cycle.delta`, negated because the axes are
measured as losses, with an anti-vacuity guard so the equality cannot degenerate
to `0 == 0 * n`, and seat ids read positionally so a rate belongs to the seat
whose movement is measured.

F-D - `published_point_names` synthesised `obj<id>` when a point's source was
absent from `state.objects`. Every caller compares that string to the
SUE/REED/TORCH constants, so an unresolvable source read as "not that card" and
silently satisfied m1's negative owner-firewall assertion. Now a panic, matching
the treatment the adjacent `other =>` arm already gave the same class of failure.
The new guard is proven able to fire:
`published_point_names_panics_when_a_points_source_is_absent` deletes the first
published point's source and requires the panic, and reports `should panic ...
FAILED` when the synthetic fallback is restored.

Gates at this tip: lib 18501 passed / 0 failed / 6 ignored; integration 4514
passed / 0 failed / 2 ignored (4513 + the new row); clippy --workspace
--all-targets -D warnings exit 0. The CR 603.5 prompt census and
`a1_the_users_accept_committed_nothing_board_now_commits_on_every_axis` are both
green.

Assisted-by: ClaudeCode:claude-opus-5
… loop sample

`apply_action`'s answer-beat sampler called `record_loop_detect_sample` BEFORE
installing the pipeline's returned `wf`, while the settle sampler in
`pass_priority_once_with_pipeline` records AFTER its `sync_waiting_for`. A frame
minted at the answer site therefore snapshotted the PRE-pipeline
`waiting_for`/`priority_player` pair and a settle frame the synced one. That is a
detection hazard, not cosmetics: `impl PartialEq for GameState` compares both
fields and `normalize_for_loop` neutralizes neither, so a heterogeneous ring
breaks `ring_delta_signature`'s turn-position conjunct.

Route `wf` through `game::public_state::sync_waiting_for` — the canonical
synchronizer, which also recomputes `priority_player` — before the record, and
drop the raw assignment below it. Blast radius is the ring only:
`apply_action_boundary` already re-syncs the returned `wf` before the result
leaves the engine, so the settled state is unchanged. The edit is line-neutral so
the CR 603.5 prompt census keeps its line-exact `engine.rs:11549` pin (verified
byte-identical by sha256, still inside `begin_pending_trigger_target_selection`).

Mechanism verified at source: `is_forced_cascade_window` is a `matches!` over 13
non-`Priority` variants with a fail-closed fall-through, and the sampler's gate
reads the returned `wf` while the snapshot preserved `state.waiting_for`.

Evidence — instrumented probe on the pre-fix tree, `--test-threads=1`, full lib +
integration; one unit = one emitted probe line = one `record_loop_detect_sample`
invocation at that site. Settle site: 996 samples, 0 that the sync changed on
either field. Answer site: 285 samples, 0 stale on either field. An always-true
comparison of the same shape emitted on the same line is true on 996/996 and
285/285, so the zeros are a verdict rather than a dead instrument. The defect is
consequently structural and latent, and this replaces a coincidence with a
guarantee.

New production-fixture row on the tracked `dina_conqueror_4p` dump, driven
through production `apply()`: the newest answer-beat frame is
`Priority{active_player}`, its `priority_player` is that seat, and the published
`LoopCertificate` is exact under an exhaustive destructure. Revert-probes,
measured: clobbering `priority_player` after the sync fails arm 2
(`PlayerId(3)` vs `PlayerId(0)`); clobbering the window fails arm 1 (`GameOver`
vs `Priority`), with arm 2 passing first, so the arms are separately live. A pure
revert of the reorder PASSES, which is the honest statement that no current
fixture reaches the divergence.

Gate at this tree: fmt 0, clippy --workspace --all-targets -D warnings 0, lib
18501 passed / 0 failed / 6 ignored, integration 4515 passed / 0 failed / 2
ignored (4514 -> 4515 is exactly the one new row).

Assisted-by: ClaudeCode:claude-opus-5
@lgray
lgray force-pushed the fix/f4-published-set-completeness branch from f144bb3 to a6d1a0e Compare August 5, 2026 03:55
@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.

Approved current head a6d1a0e: the forced-window sample uses canonical sync_waiting_for state before recording; the current-head parse artifact reports no card-parse changes; required checks are green.

@matthewevans
matthewevans added this pull request to the merge queue Aug 5, 2026
@matthewevans matthewevans removed their assignment Aug 5, 2026
Merged via the queue into phase-rs:main with commit 90c525e Aug 5, 2026
13 checks passed
lgray added a commit to lgray/phase that referenced this pull request Aug 5, 2026
…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
@lgray

lgray commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

Record-keeping pointer, no action needed here.

Three review-response commits for this PR were still in review when it merged. A push of them raced the merge — the branch ref moved, but GitHub never adopted them because the PR was already closing, so they did not enter 90c525eb7. They are now open as #7023 against current main, byte-identical content, rebased.

They matter because main currently carries what they fix: the mechanism this PR cites for answer-beat frame homogeneity is wrong in three in-tree sites (ring_delta_signature reads neither waiting_for nor priority_player — the real sensitivity is loop_states_equal_modulo_resources via impl PartialEq for GameState), one of those sites being an assertion message that would print a false reason on failure; four citations name a pre-rebase SHA that no longer resolves; CodeRabbit's Major on the answer-beat row's frame attribution is unclosed; and the row's mint detector is missing the is_some() guard its production counterpart has.

Also worth recording against this thread rather than only in the follow-up: the sync fix landed here is causally inert on the current corpus. Instrumenting the pre-fix tree measured zero stale samples at either sampler site, with an always-true comparison of the same shape emitted on the same line to prove the instrument was live, and a pure revert of the reorder passes the row added with it. It is latent-hazard hardening, correct and worth having, but this PR could not demonstrate the defect failing and no fixture reaching the divergence was constructed.

Earlier in this thread I withdrew the Verification, Gate A and Final review-impl figures as detached from the head they described. They are re-measured and re-bound in #7023 rather than restated here, since the head they belong to is that one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants