refactor(engine): ambiguity gate covers the full cluster — batches 1-3, 585 pairs burned to zero
Test / fmt (pull_request) Successful in 4s
Test / test (pull_request) Successful in 4m13s

Port the July 7 gate-extender branch onto current master (the You-hub,
touch action bar, glass tab bar, Phase L, and hint-ghost work all landed
since) and finish batch 3. cluster_app now includes the mode, replay,
input, radial, tooltip, cursor, touch-selection, and safe-area plugins on
top of batches 1-2; the 585 ambiguous pairs the expansion exposed are
annotated down to zero and the gate stays assert_eq!(count, 0).

New ordering machinery:
- ReplayPlayback: playback driver chain on the pre-mutation spine
  (PointerInput < ReplayPlayback < overlay chain < GameMutation)
- ModeStart: mode-start handlers between the stats abandon-recorder and
  GameMutation; ChallengeCompletion < DailyCompletion < WeeklyGoalsEval
  pinned inside ProgressUpdate so `.after(ProgressUpdate)` readers see
  settled progress
- PointerInput: public set wrapping the input chain; AbandonRecord on the
  stats abandon-recorder; PendingHint around the async hint pipeline;
  HintGhostFx for the ghost chain (after the full visual spine)
- Clubs: MoveRequestWriters, WarningToastWriters join the existing
  request/toast writer clubs; UiTextFx grows the overlay/tooltip/safe-area
  chrome painters; SettingsAccess grows the lag-tolerant settings readers

Gate: cargo test --workspace green (993 engine tests), clippy
--all-targets -D warnings clean, rustfmt applied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-16 17:42:46 -07:00
parent 099b7a263f
commit 838342649c
37 changed files with 1155 additions and 174 deletions
+83 -11
View File
@@ -46,6 +46,14 @@ pub struct StatsStoragePath(pub Option<PathBuf>);
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct StatsUpdate;
/// Set holding the abandon-recorder (`update_stats_on_new_game`). It must
/// read the outgoing game's `move_count` before any mode-start handler or
/// `GameMutation` clobbers the game state — mode plugins order their reset
/// handlers `.after(AbandonRecord)` via `crate::game_plugin::ModeStart`
/// (#143).
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct AbandonRecord;
/// Marker component on the stats overlay root node.
#[derive(Component, Debug)]
pub struct StatsScreen;
@@ -205,10 +213,34 @@ impl Plugin for StatsPlugin {
// constraints (win_summary_plugin: cache_win_data.before(StatsUpdate)),
// and a system cannot be both inside a set and individually before a
// set-level ordering constraint.
.add_systems(Update, update_stats_on_new_game.before(GameMutation))
.add_systems(
Update,
update_stats_on_win.after(GameMutation).in_set(StatsUpdate),
update_stats_on_new_game
.in_set(AbandonRecord)
.before(GameMutation)
// Reads the shared new-game request stream via its own
// cursor — append order is irrelevant, so it joins the
// self-ambiguous writer club (NewGameRequestWriters must
// not carry set-level edges; see its doc). The explicit
// edges pin it after the pause/settings/selection
// mutators and the pre-mutation game-state spine it
// reads `move_count` from (#143).
.in_set(crate::game_plugin::NewGameRequestWriters)
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
.after(crate::game_plugin::PreMutation)
.after(crate::pause_plugin::PauseMutation)
.after(crate::settings_plugin::SettingsMutation)
.after(crate::selection_plugin::SelectionKeySet),
)
.add_systems(
Update,
update_stats_on_win
.after(GameMutation)
.in_set(StatsUpdate)
// Also emits toasts (streak milestones) — join the
// self-ambiguous writer club (#143).
.in_set(crate::game_plugin::InfoToastWriters)
.ambiguous_with(crate::game_plugin::InfoToastWriters),
)
.add_systems(
Update,
@@ -216,29 +248,69 @@ impl Plugin for StatsPlugin {
// the NewGameRequestEvent it emits is not visible to
// update_stats_on_new_game in the same frame — otherwise
// record_abandoned() fires twice on every forfeit (#21).
// It drains the ForfeitEvent the pause/forfeit modal chain
// emits, and shares the new-game request stream with the
// other writers (#143).
handle_forfeit
.before(GameMutation)
.before(update_stats_on_new_game),
.before(update_stats_on_new_game)
.after(crate::pause_plugin::PauseMutation)
.after(crate::selection_plugin::SelectionKeySet)
.in_set(crate::game_plugin::NewGameRequestWriters)
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
)
// Open/close/tab handling moved to `you_hub_plugin`
// (Phase E) — this plugin now owns body content, replay
// selector behavior, and scroll.
.add_systems(Update, refresh_replay_history_on_win.after(GameMutation))
.add_systems(Update, handle_watch_replay_button)
.add_systems(Update, handle_copy_share_link_button)
//
// Stats overlay UI: one deterministic chain — every member
// touches the replay-selector state or the overlay's nodes, and
// handle_watch_replay_button resets the live game, so the chain
// sits between GameMutation and the auto-complete driver on the
// game-state timeline (#143). The toast-emitting members join
// the self-ambiguous InfoToastWriters set so the toast pipeline
// (AnimFx) drains them this frame.
.add_systems(
Update,
(
refresh_replay_history_on_win,
handle_replay_selector_buttons,
repaint_replay_selector_caption,
repaint_replay_selector_detail,
// Marker-filtered Text repaints — disjoint from the
// HUD text painters (UiTextFx club, #143).
repaint_replay_selector_caption
.in_set(crate::ui_theme::UiTextFx)
.ambiguous_with(crate::ui_theme::UiTextFx),
repaint_replay_selector_detail
.in_set(crate::ui_theme::UiTextFx)
.ambiguous_with(crate::ui_theme::UiTextFx),
handle_watch_replay_button
.in_set(crate::game_plugin::InfoToastWriters)
.ambiguous_with(crate::game_plugin::InfoToastWriters),
handle_copy_share_link_button
.in_set(crate::game_plugin::InfoToastWriters)
.ambiguous_with(crate::game_plugin::InfoToastWriters),
)
.chain(),
.chain()
.after(GameMutation)
// The overlay repaints read this frame's settled stats,
// progress, and achievement evaluation (level line,
// streak).
.after(StatsUpdate)
.after(crate::progress_plugin::ProgressUpdate)
.after(crate::achievement_plugin::AchievementEval)
.before(crate::auto_complete_plugin::AutoComplete),
)
.add_systems(Update, scroll_stats_panel)
.add_systems(
Update,
crate::ui_modal::touch_scroll_panel::<StatsScrollable>,
scroll_stats_panel
.in_set(crate::ui_modal::PanelScroll)
.ambiguous_with(crate::ui_modal::PanelScroll),
)
.add_systems(
Update,
crate::ui_modal::touch_scroll_panel::<StatsScrollable>
.in_set(crate::ui_modal::PanelScroll)
.ambiguous_with(crate::ui_modal::PanelScroll),
);
}
}