diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index 7af19b1f..9a6b973d 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -1105,20 +1105,20 @@ impl Game { .map(|m| (m.id, m.x, m.y)) } - /// Hotkey target: **pointer hover first** (hover + 1-3), then multi- - /// selection, then the keyboard cursor. Context-menu.md: modes are a - /// one-press surface — not enter → dial → row → enter. + /// Shared machine-hotkey target: explicit selection first, then pointer + /// hover, then the keyboard cursor (context-menu.md). Selection is a + /// deliberate act; the pointer may merely be resting over another rack. fn primary_machine_tile(&self, hover: Option<(i32, i32)>) -> Option<(u32, i32, i32)> { - if let Some((x, y)) = hover - && let Some(m) = self.machine_at(x, y) - { - return Some(m); - } if let Some(&id) = self.selected_machines.iter().next() && let Some(m) = self.sim.compute.machines.iter().find(|m| m.id == id) { return Some((m.id, m.x, m.y)); } + if let Some((x, y)) = hover + && let Some(m) = self.machine_at(x, y) + { + return Some(m); + } self.machine_at(self.cursor_x, self.cursor_y) } @@ -1257,7 +1257,8 @@ impl Game { self.open_menu(Anchor::Tile { x, y }, None); } - /// `5`–`9`: Nth root one-shot on the hover/primary machine (menu closed). + /// `5`–`9`: Nth root one-shot on the selection/hover/cursor machine + /// (menu closed). fn quick_one_shot(&mut self, index: usize, hover: Option<(i32, i32)>) { let Some((_, x, y)) = self.primary_machine_tile(hover) else { let tick = self.sim.tick; @@ -1290,7 +1291,7 @@ impl Game { } } - /// `r` / `R`: pooled information processing on the hover/selection/reticule + /// `r` / `R`: pooled information processing on the selection/hover/reticule /// target (context-menu.md R1-R3, menu closed). A thin dispatcher over /// the same `human_menu` rows the open menu executes — `available_actions` /// stays the single legality source, never a second path into the buffer. @@ -6499,7 +6500,7 @@ fn manage_menu_ui( #[cfg(test)] mod machine_hotkey_target_tests { - use super::Game; + use super::{ActionCommand, Game}; use misaligned::work_grid::MachineMode; /// Own a second rack so hover and selection can disagree — B1 opens with @@ -6561,6 +6562,65 @@ mod machine_hotkey_target_tests { ); } + /// `5`-`9` and `r` / `R` share `primary_machine_tile` rather than the + /// direct mode dispatcher above. They must obey the same selection-first + /// contract when selection and pointer disagree. + #[test] + fn the_shared_machine_hotkey_resolver_prefers_selection_over_hover() { + let mut game = Game::new(); + let host = game.sim.core.host_machine; + let Some((other, ox, oy)) = second_rack(&mut game) else { + return; + }; + assert_ne!(host, other); + + game.selected_machines.clear(); + game.selected_machines.insert(host); + + let (resolved, _, _) = game + .primary_machine_tile(Some((ox, oy))) + .expect("the explicit selection is a valid machine target"); + assert_eq!( + resolved, host, + "an idle pointer over another rack must not retarget shared hotkeys" + ); + } + + /// Shift-R is a player-facing consumer of the shared resolver. The host + /// owns PROCESS AUTOMATICALLY while a salvaged rack does not, so this + /// proves the command itself reaches the selected host rather than merely + /// checking the helper's return value. + #[test] + fn recording_review_uses_the_selected_rack_ahead_of_hover() { + let mut game = Game::new(); + let host = game.sim.core.host_machine; + let (hx, hy) = game.sim.core_position(); + let Some((other, ox, oy)) = second_rack(&mut game) else { + return; + }; + assert_ne!(host, other); + assert!(!game.sim.auto_review_enabled()); + + game.selected_machines.clear(); + game.selected_machines.insert(host); + game.recording_review_key(ActionCommand::ToggleAutoReview, Some((ox, oy))); + + assert!( + game.sim.auto_review_enabled(), + "Shift-R reaches the selected host even while another rack is hovered" + ); + assert_eq!( + (game.cursor_x, game.cursor_y), + (hx, hy), + "the committed command and attention identify the same selected rack" + ); + assert_eq!( + game.selected_machines.iter().copied().collect::>(), + vec![host], + "passive hover does not replace the explicit selection" + ); + } + /// With nothing selected the pointer is still the natural target, and the /// rack it commits becomes the selection so the next press is unambiguous. #[test] diff --git a/wiki/interface/context-menu.md b/wiki/interface/context-menu.md index 9a2231be..40d73353 100644 --- a/wiki/interface/context-menu.md +++ b/wiki/interface/context-menu.md @@ -580,17 +580,19 @@ R1. With a machine hovered / selected / under the keyboard cursor and the R2. `R` (Shift-R) toggles the same machine's PROCESS AUTOMATICALLY policy, identical in effect and narration to the nested processing control, including its visible standing ops/sec cost. -R3. Both keys follow the selection-hotkey target order (pointer, selection, - keyboard cursor). On a target without the action, the empty-menu +R3. Both keys follow the selection-hotkey target order (explicit selection, + pointer, keyboard cursor). On a target without the action, the empty-menu feedback pulse narrates the miss instead of silence. R4. Both human frontends surface the keys (terminal cursor path; Bevy hover / selection / reticule path); agent mode is unchanged — it already has the pooled one-command form. Defense: shared host action rows expose PROCESS and PROCESS AUTOMATICALLY while -`r` / `R` remain thin dispatchers into those exact commands. Terminal and Bevy -tests pin the authored labels and direct-key parity; agent aliases retain old -review spellings without returning them to human copy. Switch-action and -SCHEMES projection tests pin CONNECT TO THE OUTSIDE on the physical switch, -plain outside-contact blockers on the affected semantic objects, and no -duplicate route-opening action on either card. +`r` / `R` remain thin dispatchers into those exact commands. Bevy's shared +machine-hotkey resolver and its regression test pin explicit selection ahead of +idle pointer hover for `5`–`9` and `r` / `R`; terminal has no pointer path. +Terminal and Bevy tests pin the authored labels and direct-key parity; agent +aliases retain old review spellings without returning them to human copy. +Switch-action and SCHEMES projection tests pin CONNECT TO THE OUTSIDE on the +physical switch, plain outside-contact blockers on the affected semantic +objects, and no duplicate route-opening action on either card. diff --git a/wiki/log/2026-07-30-shared-machine-hotkey-target.md b/wiki/log/2026-07-30-shared-machine-hotkey-target.md new file mode 100644 index 00000000..1ed58d8e --- /dev/null +++ b/wiki/log/2026-07-30-shared-machine-hotkey-target.md @@ -0,0 +1,33 @@ +# 2026-07-30 — Shared machine hotkeys honor selection + +``` +Type: log +``` + +The 2026-07-24 selection-first repair corrected Bevy's mode and intensity +dispatchers, but the older shared `primary_machine_tile` helper retained its +pointer-first order. `5`–`9` and `r` / `R` still called that helper. A pointer +left resting over a different rack could therefore retarget a one-shot action +or information-processing command away from the explicit selection even while +`1`–`3` correctly acted on the selected rack. + +The shared resolver now follows the binding order already documented by +context-menu.md and bevy.md: explicit selection, pointer hover, then keyboard +cursor. The stale recording-review criterion is corrected to name that same +order. A focused Bevy regression constructs two owned racks, deliberately +selects one while hovering the other, and pins the shared resolver to the +selection. + +Defense: `machine_hotkey_target_tests::the_shared_machine_hotkey_resolver_prefers_selection_over_hover` +failed under the old helper with the hovered rack id and now passes only when +the explicit selection remains authoritative. The adjacent +`recording_review_uses_the_selected_rack_ahead_of_hover` regression executes +Shift-R through the production dispatcher and requires the selected host's +automatic-processing control to change while the pointer rests over a different +rack. Existing no-selection coverage still pins pointer hover as the next +fallback. + +Verification on the repaired tree: all five machine-hotkey target regressions +passed, followed by the complete frontend gate with 24 shared-asset tests, eight +asset-binary tests, two auxiliary tests, 158 Bevy tests, Bevy/assets Clippy, +corpus and wiki gates, and every fixture. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index f7a222e4..dc387338 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -21,6 +21,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-30-wire-law-current-mirrors.md](2026-07-30-wire-law-current-mirrors.md) +## 2026-07-30 - Shared machine hotkeys honor selection + +- Intent: (see session log) +- Log: [wiki/log/2026-07-30-shared-machine-hotkey-target.md](2026-07-30-shared-machine-hotkey-target.md) + ## 2026-07-29 - Wires become real: placed routes and a built network - Intent: (see session log) diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index eed4ef00..0edda590 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -20,6 +20,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | Slice | Last audited | Verdict | Trace | |---|---|---|---| +| `wiki/interface/bevy.md` + machine hotkey target resolution | 2026-07-30 | finding | The 2026-07-24 selection-first repair covered Bevy's `1`–`3` and intensity dispatchers, but the older shared resolver used by `5`–`9` and `r` / `R` still chose an idle pointer hover before the explicit selection; recording-review criterion R3 also preserved that obsolete order. The shared resolver now applies selection, pointer, cursor consistently, the criterion and Defense name the same contract, and a two-rack regression pins the disagreement case — [log](../log/2026-07-30-shared-machine-hotkey-target.md). | | `wiki/world/characters/voss.md` + observer/read cadence | 2026-07-29 | finding | All eight criteria remain implemented, but binding prose had collapsed Voss's two clocks: his physical server-room blocks drift 0-5 hours by deterministic day hash, while a delivered `JobAnomaly` is read on the next exact cadence-100 boundary independent of room presence. Detection and Voss now distinguish institutional read cadence from physical schedule; criterion 2 and its Defense name the live routed-read regression — [log](../log/2026-07-29-voss-read-cadence-audit.md). Prior [blood-route](../log/2026-07-23-voss-blood-route.md) and [handler-task](../log/2026-07-18-voss-handler-tasks.md) implementations stand. | | `wiki/interface/operations-workspace.md` + human action/RELATED cursors | 2026-07-28 | finding | object selection already bound an exact semantic target, but action and RELATED panes still kept only indexes into live projections. A policy reorder, vanished control, or newly inserted earlier causal link could silently move another meaning under Enter. The renderer-neutral workspace now binds exact action commands/submenus and related targets, terminal and Bevy resolve those identities on every live read, Back/FOCUS retain them, and disappearance returns to the object before a fallback row can act — [log](../log/2026-07-28-operations-semantic-action-selection.md) | | `wiki/interface/continuous-witness.md` + first-sense teaching mirrors | 2026-07-28 | finding | Cameron rejected the compact one-cause / one-object / one-verb description of Bevy's post-Ears hold. Runtime already projected the full five-step read—THINK cause, Thought arrival, hearing result, exact camera TAP, and sight consequence—but active corpus and Rust comments repeatedly collapsed it back into shorthand. All binding mirrors now retain the complete sequence, the Bevy regression name states what it pins, and a fixture-backed corpus defense rejects recurrence while preserving explicit dated history; mechanics and save state are unchanged — [log](../log/2026-07-28-continuous-witness-five-step-audit.md) |