From ef816f0d69aefda96a31ee89c9b2a991a8aa0e38 Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 16 Jul 2026 23:05:19 +0000 Subject: [PATCH] Make Bevy speak the read: reticle receipt and world sentences. digital-read D2. The frontend now consumes the read projection and the ActionDesc receipt it never rendered. Criterion 2: the hover verb bar gained a receipt row printing the leading device verb's cost and expected signature (observer + band) straight off ActionDesc — the read-receipt harness proves '0.25 T | Network 3 -> the IT [Cold]' at the reticle; emission-free verbs say 'no signature'. Criteria 3-4: render_read_sentences places earned-anchor sentences in world space (held choices bright, else dim) above their tile, with a rail READ block for anchor-less rising states — one frame shows the starving Ears sink and a machine's pending JobAnomaly trace at its own tile. People place only where a camera earns them. Defense: wiki/interface/digital-read.md criteria 2-4, amended here with the D2 status note; continuous-witness.md axiom 1 (the verb under the cursor now shows its shadow) and axiom 3. No sim or save change; the frontend reads an existing projection. --- wiki/interface/digital-read.md | 16 ++++++++++++++++ wiki/log/2026-07-15-digital-read-d2.md | 40 ++++++++++++++++++++++++++++++++++++++++ wiki/log/DEVLOG.md | 5 +++++ assets/reference/art-direction/b1-digital-read-receipt.png | 0 assets/reference/art-direction/b1-digital-read-world-trace.png | 0 crates/misaligned-bevy/src/main.rs | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 6 file(s) changed, 262 insertion(s)(+), 4 deletion(s)(-) diff --git a/wiki/interface/digital-read.md b/wiki/interface/digital-read.md --- a/wiki/interface/digital-read.md +++ b/wiki/interface/digital-read.md @@ -26,6 +26,22 @@ pure fixtures pin the states (`sim/tests/read.rs`); agent mode's `read` verb prints the same sentences (one producer, no frontend-composed prose). Criteria 2-8 remain open for D2-D5. + 2026-07-15 D2 landed: Bevy consumes the projection. Criterion 2 — the + hover verb bar grew a receipt row printing the leading device verb's + `ActionCost::label()` + `ExpectedSignature::label()` unmodified; the + new `read-receipt` harness proves it renders `0.25 T | Network 3 -> + the IT [Cold]` at the reticle + (assets/reference/art-direction/b1-digital-read-receipt.png). Criteria + 3-4 — `render_read_sentences` places each earned-anchor sentence in + world space (held choices BONE, else DIM) above its tile; anchor-less + sentences fall to a rail READ block (`sidebar_read_text`), so a fresh + frame shows the starving Ears sink and a machine's pending + `JobAnomaly` trace at its own tile + (b1-digital-read-world-trace.png). People place only where a camera + earns them (presence law). Frontend gate green; visually reviewed. + Criterion 5 (held-choice interrupt frame), 7 (dev two-pane), and 8 + (full cold-player sweep) remain for D3; criterion 6 (intel levels) + waits on the summary-source decision. Stage: B1 — The Basement Work order: digital-read Work priority: 27 diff --git a/wiki/log/2026-07-15-digital-read-d2.md b/wiki/log/2026-07-15-digital-read-d2.md new file mode 100644 --- /dev/null +++ b/wiki/log/2026-07-15-digital-read-d2.md @@ -0,0 +1,40 @@ +# digital-read D2: Bevy speaks the read + +``` +Type: log +``` + +Second dispatch of the digital-read work order. The Bevy frontend now +consumes `Sim::read_sentences` and the `ActionDesc` receipt it never +rendered before. + +**Criterion 2 — the reticle receipt.** The hover verb bar gained a +receipt row under the device verbs. It prints the leading tap/untap/take +verb's `ActionCost::label()` and `ExpectedSignature::label()` straight +off `ActionDesc`, unmodified — cost, channel, size, watching observer, +and that observer's band. A player weighs the shadow before casting it. +The new `read-receipt` harness (DIGITAL, reticle on the untapped +environmental monitor) freezes the proof: `0.25 T | Network 3 -> the IT +[Cold]`. Emission-free verbs read `free | no signature`, honestly. + +**Criteria 3-4 — standing anchor sentences.** `render_read_sentences` +places each read sentence whose anchor has an earned world position +(a seen tile, or a person a camera earns per the presence law) as a +world-space line above its tile — held choices bright (BONE), everything +else DIM. Sentences with no earned place (a starving reservoir, a +network-wide hum) fall to a rail READ block. One fresh frame shows both +paths at once: the starving Ears sink on the rail and a machine's pending +`JobAnomaly` trace sitting at that machine's own tile, naming the Handler +and his band. + +Evidence: assets/reference/art-direction/b1-digital-read-receipt.png, +b1-digital-read-world-trace.png. Frontend gate green; both frames +visually reviewed (not just structurally audited) per criterion 8's +standard, which the remaining held-choice and cold-player frames will +complete in D3. + +Defense: wiki/interface/digital-read.md criteria 2-4 (one receipt model, +attention economy, pending records at emitting anchors) — amended in this +commit with the D2 status note; continuous-witness.md axiom 1 ("what verb +is on the thing under my cursor" now includes its shadow) and axiom 3. +No sim or save change; the frontend reads an existing projection. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -26,6 +26,11 @@ - Intent: (see session log) - Log: [wiki/log/2026-07-15-digital-read-design-session.md](2026-07-15-digital-read-design-session.md) +## 2026-07-15 - digital-read D2: Bevy speaks the read + +- Intent: (see session log) +- Log: [wiki/log/2026-07-15-digital-read-d2.md](2026-07-15-digital-read-d2.md) + ## 2026-07-15 - digital-read D1: the read projection in core - Intent: (see session log) diff --git a/assets/reference/art-direction/b1-digital-read-receipt.png b/assets/reference/art-direction/b1-digital-read-receipt.png new file mode 100644 --- /dev/null +++ b/assets/reference/art-direction/b1-digital-read-receipt.png diff --git a/assets/reference/art-direction/b1-digital-read-world-trace.png b/assets/reference/art-direction/b1-digital-read-world-trace.png new file mode 100644 --- /dev/null +++ b/assets/reference/art-direction/b1-digital-read-world-trace.png diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -1449,6 +1449,11 @@ ] } #[derive(Component)] +struct ReadSentenceMarker { + index: usize, +} + +#[derive(Component)] struct PersonMarker { id: u8, } @@ -1470,6 +1475,9 @@ /// line only when an observer, strike, or unpaid obligation is live. Schedule, Nudge, + /// Standing read sentences without an earned world position + /// (digital-read.md): rising states only, never nominal chatter. + Read, /// Quiet one-line progressive-disclosure hint (Tab); chrome, never amber. DetailHint, Focus, @@ -1527,6 +1535,9 @@ } /// One reusable text slot in the hover verb bar. #[derive(Component, Clone, Copy)] +struct HoverVerbReceiptText; + +#[derive(Component)] struct HoverVerbWord { family: HoverVerbFamily, index: usize, @@ -1581,8 +1592,12 @@ } fn height(self) -> f32 { + // +16 carries the pre-commit receipt line under the device verbs + // (digital-read.md criterion 2). if self.machine.is_some() && self.device.is_some() { - 58.0 + 74.0 + } else if self.device.is_some() { + 48.0 } else { 32.0 } @@ -1725,7 +1740,8 @@ .set_machine_mode(game.sim.core.host_machine, MachineMode::Work); game.set_cursor(core.0, core.1); } - let hover_verb_bar_reticule_only = matches!(kind.as_str(), "hover-menu" | "first-think"); + let hover_verb_bar_reticule_only = + matches!(kind.as_str(), "hover-menu" | "first-think" | "read-receipt"); let presentation_progress = (kind == "thought-snap").then_some(0.12); ShotHarness { kind, @@ -1882,7 +1898,7 @@ render_work_production, render_thought_sinks, render_people, - render_digital_focus_label, + (render_digital_focus_label, render_read_sentences), render_people_3d, render_audio_events_flat, scroll_sidebar, @@ -2766,6 +2782,27 @@ let core = game.sim.core_position(); game.set_cursor(core.0, core.1); } + // The pre-commit receipt on the reticle (digital-read.md criterion 2): + // DIGITAL, reticule centered on the reachable-but-unowned + // environmental monitor so its leading TAP verb renders cost + + // expected signature + observer + band. No menu open, so the hover + // verb bar (and its receipt row) is visible. + "read-receipt" => { + mode.material = false; + mode.zoom = 1.0; + // Untap the monitor so its leading verb is TAP, whose receipt + // carries the Network signature (the exact string a player weighs + // before committing) rather than a free UNTAP. + if let Some((id, x, y)) = game + .sim + .reach + .device_named("environmental monitor") + .map(|d| (d.id, d.x, d.y)) + { + game.sim.reach.untap(id); + game.set_cursor(x, y); + } + } // First Eyes payoff (opening.md): freeze the two material frames that // turn the amber presence beam white, then let chassis volume resolve // out of the white source. These are visual evidence only; the sim @@ -3623,6 +3660,24 @@ Transform::from_translation(grid_to_world(0, 0, 3.2)), Visibility::Hidden, PersonWorkMarker { id: p.id }, + ChildOf(flat_root), + )); + } + + // Standing read sentences at earned anchors (digital-read.md + // criterion 3): a fixed pool, hidden while nominal; render_read_sentences + // fills them from the one core projection each sim change. + for index in 0..10 { + commands.spawn(( + Text2d::new(""), + TextFont { + font_size: 8.0, + ..default() + }, + TextColor(BONE), + Transform::from_translation(grid_to_world(0, 0, 4.5)), + Visibility::Hidden, + ReadSentenceMarker { index }, ChildOf(flat_root), )); } @@ -7519,6 +7574,50 @@ } } +/// Standing anchor sentences in world space (digital-read.md criterion 3): +/// held choices brightest, everything else DIM; one sentence per earned +/// anchor tile, offset above the row the focus label uses. The pool never +/// grows — surplus sentences stay on the rail block, honestly counted. +fn render_read_sentences( + game: Res, + mut q: Query<( + &ReadSentenceMarker, + &mut Transform, + &mut Visibility, + &mut Text2d, + &mut TextColor, + )>, +) { + if !game.is_changed() { + return; + } + let placed: Vec<(String, bool, (i32, i32))> = game + .sim + .read_sentences() + .into_iter() + .filter_map(|s| { + read_sentence_tile(&game.sim, &s).map(|tile| { + ( + s.text.clone(), + matches!(s.class, misaligned::sim::read::ReadClass::Held), + tile, + ) + }) + }) + .collect(); + for (marker, mut tf, mut vis, mut text, mut color) in q.iter_mut() { + match placed.get(marker.index) { + Some((line, held, (x, y))) => { + text.0 = ascii_ui(line); + color.0 = if *held { BONE } else { DIM }; + tf.translation = grid_to_world(*x, *y, 4.5) + Vec3::new(0.0, TILE_SIZE * 0.9, 0.0); + *vis = Visibility::Inherited; + } + None => *vis = Visibility::Hidden, + } + } +} + /// People are spatial only where a camera earns their position. Audio events /// stay attached to the subscribed instrument that captured them. fn render_people( @@ -7943,6 +8042,7 @@ spawn_sidebar_text(header, SidebarText::Header, 11.5, DIM); spawn_sidebar_text(header, SidebarText::Schedule, 10.5, DIM); spawn_sidebar_text(header, SidebarText::Nudge, 11.5, BONE); + spawn_sidebar_text(header, SidebarText::Read, 10.0, DIM); // The one labeled Operations navigation affordance on the // quiet instrument rail (operations-workspace.md entry). // A view command, not a world action. @@ -8174,6 +8274,32 @@ }, )); } + }); + // The pre-commit receipt (digital-read.md criterion 2): the + // leading device verb's cost and expected signature, straight + // off ActionDesc — you never click a shadow you haven't seen. + bar.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Px(16.0), + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + Visibility::Visible, + )) + .with_children(|line| { + line.spawn(( + Text::new(""), + TextFont { + font_size: 10.0, + ..default() + }, + TextColor(DIM), + Visibility::Visible, + HoverVerbReceiptText, + )); }); }); @@ -8469,7 +8595,20 @@ >, mut words: Query< (&HoverVerbWord, &mut Text, &mut TextColor, &mut Visibility), - (Without, Without), + ( + Without, + Without, + Without, + ), + >, + mut receipt: Query< + &mut Text, + ( + With, + Without, + Without, + Without, + ), >, ) { let Ok((mut node, mut visibility)) = grammar.single_mut() else { @@ -8525,6 +8664,33 @@ .device .map(|id| device_hover_verbs(&game.sim, id)) .unwrap_or_default(); + // The pre-commit receipt for the leading device verb: cost + expected + // signature + observer + band from ActionDesc unmodified + // (digital-read.md criterion 2); emission-free verbs say so. + if let Ok(mut receipt_text) = receipt.single_mut() { + receipt_text.0 = target + .device + .and_then(|id| { + game.sim + .available_actions(Anchor::Device(id)) + .into_iter() + .find(|a| { + matches!( + a.command.kind(), + ActionKind::Tap | ActionKind::Untap | ActionKind::Take + ) + }) + }) + .map(|a| { + let sig = a + .signature + .as_ref() + .map(|s| s.label()) + .unwrap_or_else(|| "no signature".into()); + ascii_ui(&format!("{} | {}", a.cost.label(), sig)) + }) + .unwrap_or_default(); + } let mut device_slots = Vec::new(); for verb in device_verbs { if !device_slots.is_empty() { @@ -10158,6 +10324,36 @@ format!("NEXT: {nudge}") } +/// Where a read sentence renders in world space, when its anchor has an +/// earned position (digital-read.md criterion 3). People place only where +/// a camera earns them (presence law); everything else falls to the rail. +fn read_sentence_tile(sim: &Sim, s: &misaligned::sim::read::ReadSentence) -> Option<(i32, i32)> { + match s.anchor { + Some(Anchor::Tile { x, y }) => Some((x, y)), + Some(Anchor::Person(id)) => sim + .person_pos(id) + .filter(|&(x, y)| sim.can_see_person(id) && sim.is_seen(x, y)), + _ => None, + } +} + +/// The rail's READ block: every rising sentence whose anchor has no earned +/// world position. One line each, newest classes first by projection order; +/// empty when the world already says everything (attention economy). +fn sidebar_read_text(sim: &Sim) -> String { + let lines: Vec = sim + .read_sentences() + .into_iter() + .filter(|s| read_sentence_tile(sim, s).is_none()) + .map(|s| format!("- {}", s.text)) + .collect(); + if lines.is_empty() { + String::new() + } else { + format!("READ\n{}", lines.join("\n")) + } +} + /// Frontend-local exception that outranks the Act One ladder. Until this /// buffer condition moves into the shared condition vocabulary, suppress the /// world cue so the rail and map can never point at different "now" states. @@ -10866,6 +11062,7 @@ SidebarText::Header => sidebar_header_text(&game, mode.material), SidebarText::Schedule => sidebar_schedule_text(&game.sim), SidebarText::Nudge => sidebar_nudge_text(&game.sim), + SidebarText::Read => sidebar_read_text(&game.sim), SidebarText::DetailHint => sidebar_detail_hint_text(rail.expanded).to_string(), SidebarText::Focus => sidebar_focus_text(&game), SidebarText::CycleStats => sidebar_cycle_stats_text(&game), -- tangled.sh