From 9de6da24185f5e1a53e1460bf7b4e8033b51cbef Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 3 Aug 2026 20:33:25 -0700 Subject: [PATCH] Say only what the resting band can support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The command band's NEXT strip read "Thought is reaching Eyes. Keep THINK running until sight lands." on day 1 — prose in a table of terse conditions, naming two systems before either was taught, setting a completion condition with no meter in the UI to measure it against, and claiming present-tense motion while the clock was paused. The strip is retired. A recommendation is advice, not standing state, and it already had two homes that fit it: the world-space NOW mark and the OPERATIONS witness. COMMAND_BAND_HEIGHT drops 132 to 106 and the world takes the height back. The crown was the worse offender. Both frontends zeroed it on a stopped clock, so pausing drove the largest glyphs in the frame to 0.0 OPS/SEC while YIELDS one panel over still reported THINK 4.0/T — one frame making two contradictory claims, and the eye believes the big number. It is a rate at the current configuration; whether time is running is the clock field's fact to state, and the clock field states it. Its label becomes OUTPUT, because WORK was already both a machine mode and a yield channel on that same row. W0 / T1 / L0 needed a legend the band never carried and restated what YIELDS already showed. The per-mode counts move onto the yield rows that produce them, so a mode and the number it moves read as one fact; MACHINES keeps availability and names IDLE only when it is not zero. The schedule now authors its own two lines instead of running past a shrinking slot and letting the 960px floor choose the break, which had stranded a bare 0/4 under a clipped label. Eyes copy joins the register every other nudge already used — lowercase, condition then act. EyesNudgeState::Filling carries the reservoir percent it describes, so the rung reads "eyes 40% - hold machine 1 on THINK" and the player can see how far off sight is. Defense: implements liturgical-ui-constitution.md's amended band composition — the band is the standing causal chain and the witness, and advice does not rent permanent height — together with its new clause that the crown is a rate at the current configuration which a stopped clock never zeroes, since the clock field already owns that fact and a zeroed crown beside live per-tick yields reads as a stalled machine rather than a stopped watch. Amends clinical-frame.md for the 106px band, the OUTPUT crown, yields carrying their machine counts, and the NEXT condition's move to the world cue and OPERATIONS. Sim gains no wall-clock or I/O dependency: the filling percent is projected from the reservoir the frontends were already reading. ActionCommand and the save schema are unchanged. # Conflicts: # wiki/log/DEVLOG.md --- crates/misaligned-bevy/src/main.rs | 51 +++--- crates/misaligned-bevy/src/rail_ui.rs | 168 ++++++++++++++---- .../misaligned-bevy/src/world_annotations.rs | 5 +- crates/misaligned-core/src/sim/economy.rs | 11 +- crates/misaligned-core/src/sim/mod.rs | 12 +- crates/misaligned-core/src/sim/tests/work.rs | 20 ++- crates/misaligned-terminal/src/agent.rs | 8 +- crates/misaligned-terminal/src/ui.rs | 37 ++-- tools/corpus_engine.py | 7 +- wiki/interface/clinical-frame.md | 34 ++-- wiki/interface/liturgical-ui-constitution.md | 32 ++-- .../log/2026-08-03-command-band-copy-audit.md | 62 +++++++ wiki/log/DEVLOG.md | 5 + 13 files changed, 337 insertions(+), 115 deletions(-) create mode 100644 wiki/log/2026-08-03-command-band-copy-audit.md diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index 255b659b..310ceb17 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -125,10 +125,12 @@ use world_annotations::{ }; const TILE_SIZE: f32 = 16.0; -/// Persistent top command band: witness, machine-to-sink resource chain, and -/// the one current NEXT condition. It spends height rather than deleting one -/// quarter to one third of the world width. -const COMMAND_BAND_HEIGHT: f32 = 132.0; +/// Persistent top command band: witness plus the machine-to-sink resource +/// chain. It spends height rather than deleting one quarter to one third of +/// the world width, and it carries only standing state — the current NEXT +/// condition is advice, so it lives in the OPERATIONS drawer instead of +/// renting a permanent strip. +const COMMAND_BAND_HEIGHT: f32 = 106.0; /// On-demand SYSTEMS and NOTICES drawers share one width and never coexist. const DRAWER_WIDTH: f32 = 360.0; /// The dev two-pane's agent-read column width (digital-read.md criterion 7). @@ -4658,10 +4660,14 @@ fn setup_ui(mut commands: Commands) { BorderColor::all(scaled(GUNMETAL, 0.54)), )) .with_children(|witness| { + // Identity and clock hold their measured width; the schedule + // takes whatever the window has left. At MIN_WINDOW_WIDTH the + // three fixed slots plus the buttons already spend the row, so + // shrinking these two is what used to wrap the schedule into an + // orphaned strike count. for (kind, width, size) in [ (SidebarText::Header, 170.0, 11.0), - (SidebarText::Clock, 255.0, 10.0), - (SidebarText::Schedule, 260.0, 10.0), + (SidebarText::Clock, 245.0, 10.0), ] { witness .spawn(Node { @@ -4669,11 +4675,24 @@ fn setup_ui(mut commands: Commands) { height: Val::Percent(100.0), align_items: AlignItems::Center, padding: UiRect::horizontal(Val::Px(10.0)), - flex_shrink: 1.0, + flex_shrink: 0.0, ..default() }) .with_children(|slot| spawn_sidebar_text(slot, kind, size, DIM)); } + witness + .spawn(Node { + flex_grow: 1.0, + flex_basis: Val::Px(0.0), + min_width: Val::Px(0.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + padding: UiRect::horizontal(Val::Px(10.0)), + ..default() + }) + .with_children(|slot| { + spawn_sidebar_text(slot, SidebarText::Schedule, 10.0, DIM) + }); witness .spawn(( Button, @@ -4754,26 +4773,16 @@ fn setup_ui(mut commands: Commands) { ..default() },)) .with_children(|metrics| { - spawn_command_metric(metrics, "WORK RATE", SidebarText::WorkRate, 17.0); + // "OUTPUT", not "WORK RATE": WORK is already a machine mode + // and a yield channel on this same row, and this number is + // neither of them. + spawn_command_metric(metrics, "OUTPUT", SidebarText::WorkRate, 17.0); spawn_command_metric(metrics, "MACHINES", SidebarText::Machines, 11.0); spawn_command_metric(metrics, "YIELDS", SidebarText::Yields, 10.5); spawn_command_metric(metrics, "THOUGHT", SidebarText::Thought, 10.5); spawn_command_metric(metrics, "PROJECTS", SidebarText::Projects, 10.5); spawn_command_metric(metrics, "SINKS", SidebarText::Sinks, 10.5); }); - - band.spawn(( - Node { - width: Val::Percent(100.0), - height: Val::Px(26.0), - align_items: AlignItems::Center, - padding: UiRect::horizontal(Val::Px(10.0)), - border: UiRect::top(Val::Px(1.0)), - ..default() - }, - BorderColor::all(scaled(GUNMETAL, 0.54)), - )) - .with_children(|next| spawn_sidebar_text(next, SidebarText::Nudge, 11.0, BONE)); }); // Former slab detail survives as an on-demand SYSTEMS drawer. It is not diff --git a/crates/misaligned-bevy/src/rail_ui.rs b/crates/misaligned-bevy/src/rail_ui.rs index 01fec234..710b382b 100644 --- a/crates/misaligned-bevy/src/rail_ui.rs +++ b/crates/misaligned-bevy/src/rail_ui.rs @@ -108,7 +108,6 @@ pub(super) enum SidebarText { /// Quiet schedule in ordinary operation; promoted to a crimson THREAT /// line only when an observer, strike, or unpaid obligation is live. Schedule, - Nudge, WorkRate, Machines, Yields, @@ -1092,8 +1091,9 @@ pub(super) fn ascii_ui(text: &str) -> String { mod ascii_ui_tests { use super::Game; use super::{ - ascii_ui, detection_rows, eyes_nudge_copy, sidebar_clock_text, sidebar_header_text, - sidebar_nudge, sidebar_nudge_for, sidebar_nudge_text, sidebar_schedule_text, + ascii_ui, command_work_rate_text, detection_rows, eyes_nudge_copy, sidebar_clock_text, + sidebar_header_text, sidebar_nudge, sidebar_nudge_for, sidebar_nudge_text, + sidebar_schedule_text, }; use crate::material_view::sensor_signal_visible; use misaligned::actions::ActionCommand; @@ -1111,6 +1111,58 @@ mod ascii_ui_tests { assert!(folded.contains(" - no active job")); } + /// The crown is a rate at the current configuration. Zeroing it on pause + /// put the largest number in the band into direct contradiction with the + /// per-tick yields one panel over. + #[test] + fn output_crown_survives_a_stopped_clock() { + let mut game = Game::new(); + let host = game.sim.core.host_machine; + game.sim + .set_machine_mode(host, misaligned::work_grid::MachineMode::Think); + for _ in 0..Sim::DAY_TICKS / 24 { + game.sim.advance(); + } + assert!( + game.sim.command_band_projection().work_rate_per_tick > 0.0, + "a THINK host must actually be producing for this test to mean anything" + ); + + game.paused = false; + let running = command_work_rate_text(&game); + game.paused = true; + assert!(game.clock_stopped()); + let paused = command_work_rate_text(&game); + + assert_eq!( + running, paused, + "the clock field states the pause; the crown states the rate" + ); + assert!(!paused.starts_with("0.0 "), "{paused}"); + } + + /// The schedule breaks where this code says it breaks. Left to the layout, + /// a narrow window stranded a bare strike count under a clipped label. + #[test] + fn schedule_chooses_its_own_line_break() { + let mut sim = Sim::with_seed(1); + for text in [sidebar_schedule_text(&sim), { + sim.dayjob.strikes = 1; + sidebar_schedule_text(&sim) + }] { + let lines: Vec<&str> = text.lines().collect(); + assert_eq!(lines.len(), 2, "{text}"); + assert!( + lines.iter().all(|l| l.chars().count() <= 32), + "each line must fit the narrowest schedule slot: {text}" + ); + assert!( + lines[1].contains('/'), + "the second line is never bare: {text}" + ); + } + } + #[test] fn pinned_header_carries_the_story_spine() { let mut sim = Sim::with_seed(1); @@ -1191,18 +1243,48 @@ mod ascii_ui_tests { fn eyes_copy_distinguishes_action_starvation_and_live_fill() { assert_eq!( eyes_nudge_copy(EyesNudgeState::TapAvailable, 3), - "Hearing is live. TAP CAMERA on the environmental monitor to open Eyes." + "hearing is live - TAP CAMERA on the environmental monitor" ); assert_eq!( eyes_nudge_copy(EyesNudgeState::WaitingForThought, 3), - "Eyes is waiting for Thought. Set machine 3 to THINK." + "eyes unfed - set machine 3 to THINK" ); assert_eq!( - eyes_nudge_copy(EyesNudgeState::Filling, 3), - "Thought is reaching Eyes. Keep THINK running until sight lands." + eyes_nudge_copy(EyesNudgeState::Filling { percent: 40 }, 3), + "eyes 40% - hold machine 3 on THINK" ); } + /// Guidance shares one voice, so no rung reads like a different author + /// wrote it. Every nudge opens lowercase and states a condition before an + /// act; none of them are prose with terminal punctuation. + #[test] + fn every_nudge_holds_the_same_terse_register() { + for state in [ + EyesNudgeState::TapAvailable, + EyesNudgeState::WaitingForThought, + EyesNudgeState::Filling { percent: 40 }, + ] { + let copy = eyes_nudge_copy(state, 3); + assert!( + copy.starts_with(|c: char| c.is_lowercase()), + "nudges open lowercase: {copy}" + ); + assert!(!copy.ends_with('.'), "nudges are not prose: {copy}"); + assert!(copy.contains(" - "), "condition then act: {copy}"); + } + } + + /// The filling rung must quantify. Asking a player to wait for "sight" with + /// no meter anywhere in the UI is what made the old copy useless. + #[test] + fn filling_copy_reports_reservoir_progress() { + for percent in [0u8, 40, 100] { + let copy = eyes_nudge_copy(EyesNudgeState::Filling { percent }, 1); + assert!(copy.contains(&format!("{percent}%")), "{copy}"); + } + } + #[test] fn known_reachable_sensor_pings_until_seen() { let mut sim = Sim::with_seed(1); @@ -1395,7 +1477,9 @@ mod rail_detail_tests { #[test] fn command_band_preserves_the_full_resting_world_width() { assert_eq!(DRAWER_WIDTH, 360.0); - assert_eq!(COMMAND_BAND_HEIGHT, 132.0); + // 34 witness + 72 metrics. The NEXT strip is gone: standing state earns + // permanent height, advice does not. + assert_eq!(COMMAND_BAND_HEIGHT, 106.0); const { assert!(COMMAND_BAND_HEIGHT < MIN_WINDOW_HEIGHT / 3.0); assert!(COMMAND_BAND_HEIGHT < DEFAULT_WINDOW_HEIGHT / 3.0); @@ -1408,11 +1492,12 @@ mod rail_detail_tests { let host = sim.core.host_machine; sim.set_machine_mode(host, misaligned::work_grid::MachineMode::Think); let tap = sim.thought_sinks.open_tap(host, "standing test", 1.0, 0.25); - assert_eq!( - command_machines_text(&sim), - "1 ONLINE / 1 OWNED\nW0 / T1 / L0" - ); - assert!(command_yields_text(&sim).contains("THINK")); + assert_eq!(command_machines_text(&sim), "1 ONLINE / 1 OWNED"); + // The mode split rides its yield row, spelled out, with no legend owed. + let yields = command_yields_text(&sim); + assert!(yields.contains("THINK"), "{yields}"); + assert!(yields.contains("x1"), "{yields}"); + assert!(!command_machines_text(&sim).contains("W0")); assert!(command_thought_text(&sim).contains("MADE")); assert!(command_thought_text(&sim).contains("UPKEEP")); assert!(command_thought_text(&sim).contains("AFTER")); @@ -1526,17 +1611,18 @@ pub(super) fn wrap(text: &str, width: usize) -> Vec { /// Effective ops/sec at the current tick clock. The top command band gives /// this growth number its own breathing room without sacrificing world width. +/// +/// This is a rate at the current configuration, so a stopped clock does not +/// zero it. Pausing used to drive the largest number on screen to 0.0 while +/// YIELDS in the next panel still reported live per-tick production — one +/// frame, two contradictory claims. Whether time is running is the clock +/// field's fact to state, and it states it. fn sidebar_ops_crown_text(game: &Game) -> String { - let tick_ms = if game.clock_stopped() { - 0 - } else { - game.tick_ms - }; let per_tick = game.sim.command_band_projection().work_rate_per_tick; - let ops = if tick_ms == 0 { + let ops = if game.tick_ms == 0 { 0.0 } else { - per_tick * (1000.0 / tick_ms as f32) + per_tick * (1000.0 / game.tick_ms as f32) }; Sim::format_ops_per_sec(ops) } @@ -1575,6 +1661,10 @@ fn command_work_rate_text(game: &Game) -> String { format!("{} OPS/SEC", sidebar_ops_crown_text(game)) } +/// Fleet availability only. The per-mode split used to render here as +/// `W0 / T1 / L0`, which needs a legend the band never had and restated what +/// YIELDS was already showing; the counts now sit on the yield rows they +/// actually produce. fn command_machines_text(sim: &Sim) -> String { let band = sim.command_band_projection(); let availability = if band.machines_offline == 0 { @@ -1588,19 +1678,23 @@ fn command_machines_text(sim: &Sim) -> String { let idle = if band.machines_idle == 0 { String::new() } else { - format!(" / I{}", band.machines_idle) + format!("\nIDLE {}", band.machines_idle) }; - format!( - "{availability} / {} OWNED\nW{} / T{} / L{}{}", - band.machines_owned, band.machines_work, band.machines_think, band.machines_lie, idle - ) + format!("{availability} / {} OWNED{idle}", band.machines_owned) } +/// Each channel names its rate and the machine count behind it, so the mode a +/// player assigns and the number it moves read as one fact. fn command_yields_text(sim: &Sim) -> String { let band = sim.command_band_projection(); format!( - "WORK {:.1}/T\nTHINK {:.1}/T\nLIE {:.1}/T", - band.work_yield_per_tick, band.think_yield_per_tick, band.lie_yield_per_tick + "WORK {:.1}/T x{}\nTHINK {:.1}/T x{}\nLIE {:.1}/T x{}", + band.work_yield_per_tick, + band.machines_work, + band.think_yield_per_tick, + band.machines_think, + band.lie_yield_per_tick, + band.machines_lie ) } @@ -1646,6 +1740,9 @@ pub(super) fn sidebar_schedule_text(sim: &Sim) -> String { .detection .next_audit_tick(sim.tick) .saturating_sub(sim.tick); + // Two deliberate lines. Packed onto one, this ran past its slot and let + // the layout choose the break, which stranded a bare `0/4` under a + // half-visible label. let schedule = if let Some(person) = sim.known_unpaid_debt_person() { let short = person .name @@ -1657,10 +1754,10 @@ pub(super) fn sidebar_schedule_text(sim: &Sim) -> String { .debt_payment_amount_for(person.id) .map(|amount| format!("{short} ${amount} unpaid")) .unwrap_or_else(|| format!("{short} debt unpaid")); - format!("{debt} | {} {audit}t", sim.institutional_review_label()) + format!("{} {audit}t\n{debt}", sim.institutional_review_label()) } else { format!( - "{} {audit}t | pilot {}/{}", + "{} {audit}t\npilot {}/{}", sim.institutional_review_label(), sim.dayjob.strikes, misaligned::dayjob::DayJob::PILOT_STRIKES @@ -1902,16 +1999,20 @@ fn sidebar_nudge_for(sim: &Sim, nudge: misaligned::sim::Nudge) -> Option } } +/// One shape for every rung: the standing condition, then the exact next act. +/// These three were the only nudges written as capitalized prose, and the +/// filling rung claimed present-tense motion the player could not measure and +/// that a stopped clock made false outright. State first, then a percentage. fn eyes_nudge_copy(state: misaligned::sim::EyesNudgeState, host_machine: u32) -> String { match state { misaligned::sim::EyesNudgeState::TapAvailable => { - "Hearing is live. TAP CAMERA on the environmental monitor to open Eyes.".into() + "hearing is live - TAP CAMERA on the environmental monitor".into() } misaligned::sim::EyesNudgeState::WaitingForThought => { - format!("Eyes is waiting for Thought. Set machine {host_machine} to THINK.") + format!("eyes unfed - set machine {host_machine} to THINK") } - misaligned::sim::EyesNudgeState::Filling => { - "Thought is reaching Eyes. Keep THINK running until sight lands.".into() + misaligned::sim::EyesNudgeState::Filling { percent } => { + format!("eyes {percent}% - hold machine {host_machine} on THINK") } } } @@ -2685,7 +2786,6 @@ pub(super) fn render_ui( SidebarText::Header => sidebar_header_text(&game, mode.material), SidebarText::Clock => sidebar_clock_text(&game, mode.material), SidebarText::Schedule => sidebar_schedule_text(&game.sim), - SidebarText::Nudge => sidebar_nudge_text(&game.sim), SidebarText::WorkRate => command_work_rate_text(&game), SidebarText::Machines => command_machines_text(&game.sim), SidebarText::Yields => command_yields_text(&game.sim), diff --git a/crates/misaligned-bevy/src/world_annotations.rs b/crates/misaligned-bevy/src/world_annotations.rs index 3936ae5f..62873264 100644 --- a/crates/misaligned-bevy/src/world_annotations.rs +++ b/crates/misaligned-bevy/src/world_annotations.rs @@ -1554,8 +1554,9 @@ pub(super) fn render_operator_cue( return; }; // Selection already carries a bright world frame and a stable action - // strip. Repeating NOW on that same machine produces a third competing - // annotation without adding information; NEXT remains in the slab. + // strip — the exact act is under the player's eyes. Repeating NOW on that + // same machine adds a competing annotation and no information; the worded + // condition stays available in the OPERATIONS witness. if selection_already_carries_attention_target(&game, target) { hide(&mut visibility); return; diff --git a/crates/misaligned-core/src/sim/economy.rs b/crates/misaligned-core/src/sim/economy.rs index 900a73a4..35558792 100644 --- a/crates/misaligned-core/src/sim/economy.rs +++ b/crates/misaligned-core/src/sim/economy.rs @@ -872,9 +872,14 @@ impl Sim { self.last_think_rate } - /// Wall-clock ops/sec from a tick duration in milliseconds. Zero tick - /// duration (or pause presentation) reports 0 so the number never lies - /// about a frozen clock. + /// Wall-clock ops/sec from a tick duration in milliseconds. A zero tick + /// duration has no rate to report, so it reports 0. + /// + /// A paused clock is not that case. This is the rate at the current + /// configuration, and frontends must not zero it to signal a pause: the + /// clock field already states that, and a zeroed crown standing beside + /// live per-tick yields reads as a stalled machine rather than a + /// stopped watch. pub fn effective_ops_per_sec(&self, tick_ms: u64) -> f32 { if tick_ms == 0 { return 0.0; diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index 02884a47..67736b3b 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -763,7 +763,9 @@ pub enum EyesNudgeState { /// The TAP opened Eyes, but no Thought reached its reservoir last tick. WaitingForThought, /// Thought reached the open Eyes reservoir on the last simulation tick. - Filling, + /// `percent` is how full that reservoir is, so a frontend can show the + /// player how far off sight is instead of asking them to wait blind. + Filling { percent: u8 }, } impl Sim { @@ -861,7 +863,13 @@ impl Sim { }; let effect = crate::sinks::SinkFireEffect::TapDormantCamera(device_id); match self.thought_sinks.open_with_effect(&effect) { - Some(sink) if sink.fed_last_tick => EyesNudgeState::Filling, + Some(sink) if sink.fed_last_tick => EyesNudgeState::Filling { + percent: if sink.threshold > 0.0 { + ((sink.fill / sink.threshold).clamp(0.0, 1.0) * 100.0).round() as u8 + } else { + 100 + }, + }, Some(_) => EyesNudgeState::WaitingForThought, None => EyesNudgeState::TapAvailable, } diff --git a/crates/misaligned-core/src/sim/tests/work.rs b/crates/misaligned-core/src/sim/tests/work.rs index 3c208f16..2d4fcaee 100644 --- a/crates/misaligned-core/src/sim/tests/work.rs +++ b/crates/misaligned-core/src/sim/tests/work.rs @@ -270,10 +270,22 @@ fn eyes_nudge_phase_follows_the_camera_action_and_live_thought_feed() { ); sim.pour_thought_into_sinks(node, 1.0); - assert_eq!( - sim.eyes_nudge_state(), - EyesNudgeState::Filling, - "only actual Thought delivery licenses a filling claim" + let EyesNudgeState::Filling { percent: first } = sim.eyes_nudge_state() else { + panic!( + "only actual Thought delivery licenses a filling claim: {:?}", + sim.eyes_nudge_state() + ); + }; + + // A filling claim carries its own measure. Without one a frontend can only + // tell the player to wait, with no way to say how long for. + sim.pour_thought_into_sinks(node, 1.0); + let EyesNudgeState::Filling { percent: second } = sim.eyes_nudge_state() else { + panic!("a still-fed reservoir keeps reporting its fill"); + }; + assert!( + first < second && second <= 100, + "reservoir progress rises and stays bounded: {first}% -> {second}%" ); sim.thought_sinks.begin_tick(); diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index 82840abd..f673afaf 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -3142,7 +3142,9 @@ fn eyes_nudge_copy(state: EyesNudgeState, host_machine: u32) -> String { EyesNudgeState::WaitingForThought => { format!("now: Eyes waiting — delegate M{} think", host_machine) } - EyesNudgeState::Filling => "now: Eyes filling — keep THINK running".into(), + EyesNudgeState::Filling { percent } => { + format!("now: Eyes {percent}% — keep THINK running") + } } } @@ -3709,8 +3711,8 @@ mod narration_tests { "now: Eyes waiting — delegate M3 think" ); assert_eq!( - eyes_nudge_copy(EyesNudgeState::Filling, 3), - "now: Eyes filling — keep THINK running" + eyes_nudge_copy(EyesNudgeState::Filling { percent: 40 }, 3), + "now: Eyes 40% — keep THINK running" ); } diff --git a/crates/misaligned-terminal/src/ui.rs b/crates/misaligned-terminal/src/ui.rs index 01026471..163b152d 100644 --- a/crates/misaligned-terminal/src/ui.rs +++ b/crates/misaligned-terminal/src/ui.rs @@ -48,12 +48,6 @@ pub enum ClockState { Held, } -impl ClockState { - fn is_stopped(self) -> bool { - !matches!(self, Self::Running) - } -} - /// The sterile palette. One meaning per color, everywhere. mod pal { use crossterm::style::Color; @@ -300,7 +294,7 @@ fn nudge_text(sim: &Sim, nudge: Nudge) -> String { "now: REVIEW COOLING · QUIET".into() } } - Nudge::Eyes => eyes_nudge_text(sim.eyes_nudge_state()).into(), + Nudge::Eyes => eyes_nudge_text(sim.eyes_nudge_state()), Nudge::NeedCompute => "now: band > compute — salvage/buy".into(), Nudge::Underfed => "now: job underfed — 1 feeds it".into(), Nudge::Ears => "now: no ears — think; thought fills the tap".into(), @@ -350,11 +344,11 @@ fn nudge_text(sim: &Sim, nudge: Nudge) -> String { } } -fn eyes_nudge_text(state: EyesNudgeState) -> &'static str { +fn eyes_nudge_text(state: EyesNudgeState) -> String { match state { - EyesNudgeState::TapAvailable => "now: audio live · TAP CAMERA", - EyesNudgeState::WaitingForThought => "now: Eyes waits · 2 THINK", - EyesNudgeState::Filling => "now: Eyes filling · keep THINK", + EyesNudgeState::TapAvailable => "now: audio live · TAP CAMERA".into(), + EyesNudgeState::WaitingForThought => "now: Eyes waits · 2 THINK".into(), + EyesNudgeState::Filling { percent } => format!("now: Eyes {percent}% · keep THINK"), } } @@ -1233,11 +1227,11 @@ impl UI { // Crown metric first (clinical-frame.md): effective ops/sec — how // fast the brain is going. Large bold number, quiet unit. - let ops = if clock_state.is_stopped() { - 0.0 - } else { - sim.effective_ops_per_sec(tick_ms) - }; + // + // A rate at the current configuration, so a stopped clock does not + // zero it; the clock field owns the fact that time is paused, and + // zeroing here contradicted the per-tick yields shown below. + let ops = sim.effective_ops_per_sec(tick_ms); put_attr( stdout, sx, @@ -1247,7 +1241,7 @@ impl UI { Attribute::Bold, )?; row += 1; - line(stdout, &mut row, "work rate", pal::DIM)?; + line(stdout, &mut row, "output", pal::DIM)?; // Identity block. The clock is always on screen. put_attr(stdout, sx, row, "MISALIGNED", pal::AMBER, Attribute::Bold)?; row += 1; @@ -2860,7 +2854,14 @@ mod eyes_nudge_tests { EyesNudgeState::WaitingForThought, "now: Eyes waits · 2 THINK", ), - (EyesNudgeState::Filling, "now: Eyes filling · keep THINK"), + ( + EyesNudgeState::Filling { percent: 40 }, + "now: Eyes 40% · keep THINK", + ), + ( + EyesNudgeState::Filling { percent: 100 }, + "now: Eyes 100% · keep THINK", + ), ]; for (state, expected) in copies { diff --git a/tools/corpus_engine.py b/tools/corpus_engine.py index a8d694bc..9243ad57 100755 --- a/tools/corpus_engine.py +++ b/tools/corpus_engine.py @@ -1480,7 +1480,12 @@ class Engine: if not path.is_file(): continue path_rel = path.relative_to(self.root).as_posix() - if path_rel.startswith("site/src/content/docs/"): + # Generated mirrors of the corpus, not source authority. Both are + # built from wiki/log/, which this scan already skips so archived + # history can keep naming what it named at the time; scanning the + # derivative reintroduces exactly those failures. site/.astro/ is + # Astro's gitignored build cache. + if path_rel.startswith(("site/src/content/docs/", "site/.astro/")): continue current = self.read(path) for retired_path in ( diff --git a/wiki/interface/clinical-frame.md b/wiki/interface/clinical-frame.md index 6dc4e5ed..65d28f48 100644 --- a/wiki/interface/clinical-frame.md +++ b/wiki/interface/clinical-frame.md @@ -4,10 +4,13 @@ Type: spec Status: IMPLEMENTED Status note: IMPLEMENTED. Current state: - - **The command band.** One 132px near-black band spans the top while the + - **The command band.** One 106px near-black band spans the top while the world keeps the full resting width below it. It carries the thin witness, - one Work Rate crown, and one causal chain: Machines/allocation -> Yields -> - Thought after standing upkeep -> open Projects -> standing Sinks. Full + one Output crown, and one causal chain: Machines -> Yields (each carrying + its machine count) -> Thought after standing upkeep -> open Projects -> + standing Sinks. Standing state only: the `NEXT` condition is advice, so it + rides the world cue and the OPERATIONS witness rather than a permanent + strip. Full amber remains reserved for current selection, explicit agency, and active causality; dim amber carries subordinate agency and owned-machine presence. - **On-demand drawers.** Tab opens SYSTEMS and N opens NOTICES in one shared @@ -18,8 +21,8 @@ Status note: IMPLEMENTED. Current state: coalesces recurrence into one latest row and unread priority per logical observer, filing, project, standing sink, or current deadline. - **Progressive disclosure.** A genuine exception promotes the schedule to - crimson THREAT; one `NEXT` condition remains bone and projects to an exact - world target when placeable. Operations still takes the whole window as a + crimson THREAT; the one `NEXT` condition projects to an exact world target + when placeable, and is worded in full in OPERATIONS. Operations still takes the whole window as a full-frame chamber. - **Semantic material zoom.** Close lowers the oblique toward machine/floor, far raises toward facility survey; monotonic distance, no free orbit, @@ -124,7 +127,7 @@ the edge of attention, not a second competing game board. ### The world owns the frame At the default gameplay window, the world canvas occupies the complete resting -width beneath the 132px command band. The 360px right drawer exists only on +width beneath the 106px command band. The 360px right drawer exists only on demand and overlays rather than permanently cropping that canvas; type does not shrink at the 1280px default or 960px minimum. A player should first perceive the room, the focused machine, and the active flow, then consult the band. @@ -308,11 +311,10 @@ The Bevy command band uses three quiet rows: - a witness line for DIGITAL/REAL, process revision, the objective name after the opening/teaching boundary, day/tick/held state, schedule-or-threat, and entries to Operations, SYSTEMS, and NOTICES; -- a causal aggregate chain: effective Work Rate; owned/online machines plus - WORK/THINK/LIE allocation; per-tick channel yields; Thought made, standing - upkeep, and remainder; open project count/remaining/blocked; standing sink - count/cost/starved; -- one bone-bright `NEXT` condition. +- a causal aggregate chain: effective Output; owned/online machines plus idle + count; per-tick channel yields, each row carrying the machine count behind + it; Thought made, standing upkeep, and remainder; open project + count/remaining/blocked; standing sink count/cost/starved. `Sim::command_band_projection` is the only authority for that resource chain. It divides economy-cadence yields to honest per-tick values and classifies open @@ -363,8 +365,8 @@ In the perceived world, Bevy renders a small breathing `NOW / ` mark at the placeable target. That separate world mark carries the recommendation unless the exact target tile is already carried by an explicit machine selection; in that case the -selection frame and stable mode control are enough, and the command band's `NEXT` -retains the condition. The marker never recolors the selection-level machine +selection frame and stable mode control are enough, and the OPERATIONS +witness retains the worded condition. The marker never recolors the selection-level machine control. The current mode stays bone-white and every other word, key, and separator stays gray, whether the projection recommends that mode or not. THINK likewise never promotes WORK as a panic exit. Person targets still @@ -462,9 +464,9 @@ and action surfaces. owned-machine presence, agency, or causal signal, never as a recommendation inside an attached verb menu. Crimson pixels can each be justified as actual danger/consequence. Neutral chrome never borrows either color for decoration. -5. The Bevy command band contains the thin witness, one Work Rate crown, and - the exact Machines/allocation -> Yields -> Thought after upkeep -> Projects - -> Sinks chain plus one `NEXT`. Objective, FOCUS, observer detail, history, +5. The Bevy command band contains the thin witness, one Output crown, and + the exact Machines -> Yields -> Thought after upkeep -> Projects -> Sinks + chain, and no advice. Objective, FOCUS, observer detail, history, and controls remain exact in SYSTEMS; type stays readable at both default and minimum supported window sizes. 6. The material camera remains fixed-oblique and spatially stable; the pass diff --git a/wiki/interface/liturgical-ui-constitution.md b/wiki/interface/liturgical-ui-constitution.md index 0821cc6f..ce3913c3 100644 --- a/wiki/interface/liturgical-ui-constitution.md +++ b/wiki/interface/liturgical-ui-constitution.md @@ -47,12 +47,12 @@ can scroll, collapse, or move behind focus. **Implementation:** `Flat2dRoot` (DIGITAL) and `Real3dRoot` (REAL) are the world roots. `DEFAULT_WINDOW_WIDTH = 1280`, `MIN_WINDOW_WIDTH = 960`. There is no resting width subtraction. `command_band_preserves_the_full_resting_world_width` -pins the 132px top-band height and the fact that the 360px drawer is closed at +pins the 106px top-band height and the fact that the 360px drawer is closed at rest. ### 2. The top command band -A bounded 132px near-black band spans the resting window above the world. It +A bounded 106px near-black band spans the resting window above the world. It is one instrument, not a dashboard grid. Hairlines divide a fixed causal read; cells do not become independent floating cards. @@ -60,10 +60,15 @@ cells do not become independent floating cards. the opening/teaching boundary releases; - time/run state; - the nearest ordinary `SCHEDULE`, promoted to crimson `THREAT` only for a - real strike, concerned/convinced observer, or known unpaid obligation; -- one bone-bright `NEXT` condition; + real strike, concerned/convinced observer, or known unpaid obligation. It + authors its own two lines rather than letting a narrow window pick the + break; - the Operations, SYSTEMS, and NOTICES entries. +The band carries standing state only. The current `NEXT` condition is advice, +not state, so it does not rent a permanent strip: it rides the OPERATIONS +witness, which is where a player goes when they want to be told what to do. + Objective progress, focus facts, observer meters, and the full input legend do not earn permanent weight here. They remain exact in expanded detail. The objective name is a second quiet line inside the Identity @@ -74,9 +79,13 @@ spine — the thirty-second bar — and nothing else competes at its weight. Between them, the command chain is always legible: -1. current effective **WORK RATE** as the compact crown; -2. owned/online **MACHINES** and their WORK / THINK / LIE allocation; -3. resulting per-tick channel **YIELDS**; +1. current effective **OUTPUT** as the compact crown. It is a rate at the + current configuration, so a stopped clock never zeroes it — the clock field + owns that fact, and WORK is already both a machine mode and a yield channel + on this same row; +2. owned/online **MACHINES**, plus idle count when any machine is unassigned; +3. resulting per-tick channel **YIELDS**, each row carrying the machine count + behind it, so a mode and the number it moves read as one fact; 4. **THOUGHT** made, standing upkeep, and remainder after upkeep; 5. open one-shot **PROJECTS**, remaining Thought, and blocked count; 6. open standing **SINKS**, Thought/tick, and starved count. @@ -86,10 +95,11 @@ state. The frontend does not reconstruct economy cadence, standing procedure upkeep, sink kind, or starvation. Stable objective detail, focus facts, observer meters, and the complete log remain out of the persistent band. -**Implementation:** `COMMAND_BAND_HEIGHT = 132`. `SidebarText::WorkRate`, -`Machines`, `Yields`, `Thought`, `Projects`, and `Sinks` consume -`Sim::command_band_projection`; `Header`, `Clock`, `Schedule`, `Nudge`, and -`Notices` retain the witness. `OperationsRailButton`, `SystemsDrawerButton`, +**Implementation:** `COMMAND_BAND_HEIGHT = 106` (34 witness + 72 metrics). +`SidebarText::WorkRate`, `Machines`, `Yields`, `Thought`, `Projects`, and +`Sinks` consume `Sim::command_band_projection`; `Header`, `Clock`, `Schedule`, +and `Notices` retain the witness. Header and clock hold measured widths and do +not shrink; the schedule takes the remaining row. `OperationsRailButton`, `SystemsDrawerButton`, and `NoticeDrawerButton` are view commands, not world acts. ### 3. The on-demand right drawers diff --git a/wiki/log/2026-08-03-command-band-copy-audit.md b/wiki/log/2026-08-03-command-band-copy-audit.md new file mode 100644 index 00000000..ab4714dc --- /dev/null +++ b/wiki/log/2026-08-03-command-band-copy-audit.md @@ -0,0 +1,62 @@ +# 2026-08-03 — Command band: what the resting frame actually claims + +``` +Type: log +``` + +## Intent + +Audit every readout in the resting Bevy command band as a player meets it, not +as its author remembers it. The trigger was the `NEXT` strip reading +`Thought is reaching Eyes. Keep THINK running until sight lands.` on day 1 — +prose in a table of terse conditions, naming two systems the player has not +been taught, setting a completion condition with no meter anywhere in the UI +to measure it against, and asserting present-tense motion while the clock was +paused. + +Pulling that thread found the band making claims it could not support. + +## Changed + +- **The `NEXT` strip is gone.** The band carries standing state; a + recommendation is advice. It survives where it belongs — the world-space + `NOW /` mark and the OPERATIONS witness — and `COMMAND_BAND_HEIGHT` drops + 132 -> 106, returning the strip's height to the world. +- **The crown no longer zeroes on a stopped clock.** Both frontends did this. + Paused, the largest glyphs in the frame read `0.0 OPS/SEC` while YIELDS one + panel over reported `THINK 4.0/T`: one frame, two contradictory claims, and + the eye believes the big number. It is a rate at the current configuration; + whether time is running is the clock field's fact, and the clock field + states it. +- **`WORK RATE` -> `OUTPUT`.** WORK was already a machine mode and a yield + channel on the same row, and the crown is neither. +- **`W0 / T1 / L0` retired.** It needed a legend the band never had and + restated what YIELDS already showed. The counts moved onto the yield rows + that produce them (`THINK 4.0/T x1`), so a mode and the number it moves read + as one fact. MACHINES keeps availability, and names IDLE when it is not + zero. +- **The schedule authors its own line break.** Packed onto one line it ran + past a slot that shrinks, and at the 960px floor the layout chose the break + — stranding a bare `0/4` under a clipped label. Header and clock now hold + measured widths; the schedule takes the rest of the row. +- **Eyes copy joins the register every other nudge already used** — lowercase, + condition then act. `EyesNudgeState::Filling` now carries `percent` from the + reservoir it is describing, so the rung that used to say "until sight lands" + says `eyes 40% - hold machine 1 on THINK`. + +## Spec + +Amends liturgical-ui-constitution.md (band composition, the crown's pause +contract, the causal chain) and clinical-frame.md (band height, crown name, +chain, and where the `NEXT` condition lives). + +## Verification + +`./tools/check.sh --full`: fmt, 952 tests, clippy, agent smoke, wiki/corpus/env +gates. Seven new regression tests, including one pinning the whole nudge table +to a single register so the prose cannot drift back. Observed against rendered +`command-band` frames at the 1280px default and the 960x540 floor, plus the +`operations` frame to confirm the worded condition still lands there. + +Pre-existing and untouched: the visual-identity scan fails on +`site/.astro/content-assets.mjs`, an untracked generated mirror. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index e2ab8104..e8a64ed6 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -36,6 +36,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-08-03-committed-receipt-reads-as-a-record.md](2026-08-03-committed-receipt-reads-as-a-record.md) +## 2026-08-03 - Command band: what the resting frame actually claims + +- Intent: Audit every readout in the resting Bevy command band as a player meets it, not as its author remembers it. The trigger was the `NEXT` strip reading `Thought is reaching Eyes. Keep THINK running until sight lands.` on day 1 — prose in a table of terse conditions, naming two sys... +- Log: [wiki/log/2026-08-03-command-band-copy-audit.md](2026-08-03-command-band-copy-audit.md) + ## 2026-08-03 - The annotation layer stopped overprinting itself - Intent: A Bevy frame showed `thinking wrote noise logs | Network | unread with the IT` printing straight through the teal `INFO` flag on the same machine the sentence was about. The 2026-07-21 callout grammar had already banned annotations overprinting matter, so the layer was avoidin... -- 2.51.2