From b728286cdfbc6231126a9c15f82090913d5cc618 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 02 Aug 2026 20:33:25 +0000 Subject: [PATCH] Keep rising world sentences readable The shared read now converts a three-second human reading opportunity into each live frontend cadence. Processed intel and terminal route outcomes remain legible, while route prose stays grouped at its emitter and the exact crimson custody record continues to move. Defense: wiki/interface/digital-read.md requires consequence-before-motion attention and now names a speed-aware human dwell distinct from durable NOTICES. This implements that hierarchy without adding saved custody or changing command-clocked reads. --- wiki/interface/digital-read.md | 30 ++++++++++++++++++++++++++++-- wiki/log/2026-08-02-readable-rising-beats.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ wiki/log/DEVLOG.md | 5 +++++ wiki/process/tick-ledger.md | 3 +-- crates/misaligned-bevy/src/rail_ui.rs | 10 +++++++--- crates/misaligned-bevy/src/world_annotations.rs | 2 +- crates/misaligned-core/src/sim/read.rs | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------- crates/misaligned-core/src/sim/tests/read.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- 8 file(s) changed, 246 insertion(s)(+), 65 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 @@ -3,7 +3,10 @@ ``` Type: spec Status: IMPLEMENTED -Status note: Criterion 4a implemented 2026-07-29 — routed records now +Status note: The 2026-08-02 human-dwell correction gives rising intel and + terminal route outcomes a speed-aware three-second reading opportunity; + route prose stays grouped at its emitter while the exact crimson custody + record continues to walk every persisted hop. Criterion 4a implemented 2026-07-29 — routed records now visibly travel their persisted hops instead of aging in place, and the player-controlled stretch served by LIE reads as a closing window. Criteria 1-4, 4a, and 5-8 are all implemented. The mature DIGITAL read is otherwise implemented. The issue-#15 pre-sense @@ -157,6 +160,15 @@ attention economy is law**: starvation, deadlines, contradictions, held choices, and band motion rise; nominal state stays dark. A world made of language must not become every system continuously talking. + **Readable rising beats (binding, 2026-08-02).** The projection orders + attention as interrupt -> consequence -> operational exception -> routed + motion -> ambient pressure. A wall-clock human frontend converts one shared + three-second reading opportunity into its live tick cadence for newly + processed intel and a routed record's terminal read/stop receipt. A + command-clocked frontend retains exact current-tick semantics. This dwell is + neither saved custody nor NOTICES coalescing: it keeps a short world sentence + legible, while NOTICES remains the durable drawer for consequential events. + No nominal fact earns dwell merely by existing. **A starving sentence speaks plain English (binding, 2026-07-24).** It names the waiting action, optional percent progress when partially filled, and what is taking the thought — never reservoir fill fractions @@ -244,6 +256,13 @@ anchors.) (The wire routes and criterion 4a's record-stepping render landed by 2026-07-29.) + **Grouped prose, moving custody (AMENDED 2026-08-02).** The crimson record + glyph follows every current hop and phase; its explanatory cause sentence + remains anchored at the emitting device instead of jumping to a new callout + quadrant every 20-150 ms. Delivery changes that same sentence to unread; + read or stop leaves the speed-aware terminal receipt above. The stable prose + and moving glyph are two fields of the same core projection, never two + invented stories about custody. This closes a legibility gap found 2026-07-28: a static tally on the player's own machine reads as heat pooling on them, which is exactly the global-heat ontology the 2026-07-11 routed-evidence amendment retired. @@ -337,7 +356,11 @@ 3. Standing anchor sentences render for rising states and only rising states: a nominal machine says nothing; a starving reservoir, a held choice, a deadline, and an observer band change each surface at their - anchor within one tick of the projection reporting them. + anchor within one tick of the projection reporting them. Human wall-clock + surfaces give newly processed intel and a routed record's terminal outcome + at least three seconds at every supported speed; command-clocked reads stay + exact-current. The dwell is frontend attention only and never duplicates + save custody or the NOTICES drawer. 4. Standing ambient emissions (including standing Network pressure) mark their source distinctly from travelling custody; they are not pool debt and are not visibly consumed by LIE scrub. The ambient pending pool retired @@ -349,6 +372,9 @@ show a route the sim did not author. While the record sits on a player-controlled stretch served by an available online LIE body it reads as interdictable; crossing off that stretch visibly closes the window. + Its crimson custody glyph moves; the readable cause/status sentence remains + grouped at the emitter and leaves a three-second read/stop outcome instead + of jumping with each hop. Standing Network pressure and stationary facility meter baselines keep measuring in place and must remain distinguishable from travelling records (LIE stops only the latter, before observer read). diff --git a/wiki/log/2026-08-02-readable-rising-beats.md b/wiki/log/2026-08-02-readable-rising-beats.md new file mode 100644 --- /dev/null +++ b/wiki/log/2026-08-02-readable-rising-beats.md @@ -0,0 +1,45 @@ +# 2026-08-02 — Give rising world sentences time to be read + +``` +Type: log +``` + +## Finding + +The shared read projected newly processed intel only when its processing tick +exactly equaled the current tick. Bevy can run at 20, 75, 150, or 400 ms per +tick, so the fastest clock could replace a meaningful learned fact after one +rendered frame. Routed-evidence prose also moved to every persisted hop each +tick, making the sentence jump even though its crimson custody glyph already +carried the exact motion. + +## Change + +- Core now exposes a human-cadence read that converts one three-second reading + opportunity into the frontend's live tick rate. Command-clocked reads retain + exact-current semantics. +- Newly processed exact intel and routine stream arrivals use that window. + Recent read/stopped routed outcomes use it as a short terminal receipt. +- Routed prose stays grouped at the emitting device while the structured + crimson record continues to carry its exact current hop and interdiction + phase. The sentence can be read without turning a moving record into a + static global heat tally. +- Bevy's world callouts and slab READ stratum consume the cadence-aware shared + projection. NOTICES remains a separate durable consequential-event drawer. + +## Verification + +- Fifteen focused core read tests pass, including fastest-cadence intel dwell, + source-grouped route prose, exact moving custody, and terminal route expiry. +- Sixteen Bevy callout tests pass, and the full documentation gate is clean. +- The deterministic `routed-record` Bevy run produced a 2560x1440 DIGITAL + frame with the cause/status sentence attached to its emitting monitor while + the exact crimson record remained visible on the routed custody layer. + +## Defense + +`wiki/interface/digital-read.md` now orders the read as interrupt, +consequence, operational exception, routed motion, and ambient pressure. Its +human-dwell clause requires rising intel and terminal route outcomes to remain +legible at every supported speed without creating saved custody or nominal +dashboard chatter. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ +## 2026-08-02 - Give rising world sentences time to be read + +- Intent: (see session log) +- Log: [wiki/log/2026-08-02-readable-rising-beats.md](2026-08-02-readable-rising-beats.md) + ## 2026-08-02 - Local menus hold one readable command beat - Intent: Cameron adopted the first recommendation from the GUI game-feel playtest: right-clicking a local action surface should pause time, and the principle should persist through the menus. The existing action explanation could also disappear when focus moved with the pointer, making... diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -20,6 +20,7 @@ | Slice | Last audited | Verdict | Trace | |---|---|---|---| +| `wiki/interface/digital-read.md` + Bevy rising-sentence dwell | 2026-08-02 | finding | The queued insecurity held: newly processed intel existed in the read for one 20-400 ms tick, while routed prose jumped with every custody hop. The shared projection now converts a three-second human reading opportunity into the live cadence, keeps command-clocked reads exact-current, groups route prose at its emitter while the crimson custody glyph still moves, and leaves short read/stop receipts distinct from durable NOTICES — [log](../log/2026-08-02-readable-rising-beats.md). | | `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) | @@ -107,5 +108,3 @@ Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. - -- 2026-08-02 · insecurity · `wiki/interface/digital-read.md` + Bevy world callout dwell · newly processed intel is projected only for its exact processing tick and routed-record prose moves one hop per tick, so meaningful text can flash for 20-150 ms or jump before a human can read it; define a renderer-neutral dwell/grouping hierarchy distinct from NOTICES coalescing diff --git a/crates/misaligned-bevy/src/rail_ui.rs b/crates/misaligned-bevy/src/rail_ui.rs --- a/crates/misaligned-bevy/src/rail_ui.rs +++ b/crates/misaligned-bevy/src/rail_ui.rs @@ -1645,9 +1645,13 @@ /// The slab's READ stratum: 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, minimum: misaligned::intel::IntelMagnitude) -> String { +fn sidebar_read_text( + sim: &Sim, + minimum: misaligned::intel::IntelMagnitude, + tick_ms: u64, +) -> String { let rail: Vec = sim - .read_sentences_at(minimum) + .read_sentences_at_rate(minimum, tick_ms) .into_iter() .filter(|s| read_sentence_tile(sim, s).is_none()) .collect(); @@ -2594,7 +2598,7 @@ SidebarText::Projects => command_projects_text(&game.sim), SidebarText::Sinks => command_sinks_text(&game.sim), SidebarText::Notices => command_notices_text(&game, &rail), - SidebarText::Read => sidebar_read_text(&game.sim, read_altitude.minimum), + SidebarText::Read => sidebar_read_text(&game.sim, read_altitude.minimum, game.tick_ms), SidebarText::Focus => sidebar_focus_text(&game), SidebarText::CycleStats => sidebar_cycle_stats_text(&game), SidebarText::CycleRows => { diff --git a/crates/misaligned-bevy/src/world_annotations.rs b/crates/misaligned-bevy/src/world_annotations.rs --- a/crates/misaligned-bevy/src/world_annotations.rs +++ b/crates/misaligned-bevy/src/world_annotations.rs @@ -636,7 +636,7 @@ ) -> Vec { let sentences: Vec<(String, bool, (i32, i32))> = game .sim - .read_sentences_at(minimum) + .read_sentences_at_rate(minimum, game.tick_ms) .into_iter() // Held choices are drawn by the drill-tier card (render_held_choice), // not as a world line; drawing both double-states the moment. diff --git a/crates/misaligned-core/src/sim/read.rs b/crates/misaligned-core/src/sim/read.rs --- a/crates/misaligned-core/src/sim/read.rs +++ b/crates/misaligned-core/src/sim/read.rs @@ -17,6 +17,11 @@ use super::Sim; +/// Minimum wall-clock opportunity a human frontend gives a rising read beat. +/// The core converts this into ticks from the frontend's live cadence; agent +/// mode remains command-clocked and uses the exact current-tick projection. +pub const HUMAN_READ_DWELL_MS: u64 = 3_000; + /// Why a sentence rose (digital-read.md glance tier). Nominal state never /// yields a sentence, so there is no Nominal variant on purpose. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -124,16 +129,22 @@ .any(|run| matches!(run.state, PlotState::WaitingForChoice { .. })) } - /// Every sentence the read shows this tick, deterministically ordered: - /// held choices first (attention-mandatory), then newly processed intel, - /// starving sinks, trace debt, routed records, and warmed standing - /// emissions. + /// Every sentence the exact current-tick read shows, deterministically + /// ordered: held choices first (attention-mandatory), then newly processed + /// intel, starving sinks, routed records, and warmed standing emissions. + /// Command-clocked agent/terminal reads use this form; wall-clock human + /// frontends use [`Self::read_sentences_at_rate`] so a one-tick event + /// cannot disappear between rendered frames. pub fn read_sentences(&self) -> Vec { + self.read_sentences_with_dwell(1) + } + + fn read_sentences_with_dwell(&self, dwell_ticks: u64) -> Vec { let mut out = Vec::new(); self.read_held(&mut out); - self.read_intel(&mut out); + self.read_intel(&mut out, dwell_ticks); self.read_starving(&mut out); - self.read_routed(&mut out); + self.read_routed(&mut out, dwell_ticks); self.read_standing(&mut out); out } @@ -145,6 +156,32 @@ /// zoomed out. pub fn read_sentences_at(&self, minimum: IntelMagnitude) -> Vec { self.read_sentences() + .into_iter() + .filter(|sentence| { + sentence + .magnitude + .is_none_or(|magnitude| magnitude >= minimum) + }) + .collect() + } + + /// Human wall-clock projection of the same read. The caller supplies its + /// live milliseconds-per-tick cadence; core converts the fixed reading + /// opportunity to a deterministic tick window before applying semantic + /// altitude. Passing zero (a held clock) retains the default cadence for + /// a stable preview without inventing an infinite history. + pub fn read_sentences_at_rate( + &self, + minimum: IntelMagnitude, + tick_ms: u64, + ) -> Vec { + let tick_ms = if tick_ms == 0 { + Self::DEFAULT_TICK_MS + } else { + tick_ms.max(Self::PLAYER_TICK_CADENCES_MS[0]) + }; + let dwell_ticks = HUMAN_READ_DWELL_MS.div_ceil(tick_ms).max(1); + self.read_sentences_with_dwell(dwell_ticks) .into_iter() .filter(|sentence| { sentence @@ -218,16 +255,18 @@ } /// Newly processed exact discoveries and routine stream arrivals rise at - /// their recorded source for the processing tick. The durable intel item - /// or stream remains available through Operations; this projection is the - /// moment it enters the read, not a second custody store. - fn read_intel(&self, out: &mut Vec) { + /// their recorded source. Command-clocked reads show the processing tick; + /// a human cadence may request a short tick window so the same durable + /// item cannot flash between frames. This remains a projection, not a + /// second custody store. + fn read_intel(&self, out: &mut Vec, dwell_ticks: u64) { + let rises = |processed_tick: u64| { + processed_tick <= self.tick && self.tick.saturating_sub(processed_tick) < dwell_ticks + }; let mut exact = self .intel .iter() - .filter(|intel| { - intel.processed_tick == self.tick && self.intel_holding_available(intel) - }) + .filter(|intel| rises(intel.processed_tick) && self.intel_holding_available(intel)) .collect::>(); exact.sort_by(|left, right| { right @@ -248,7 +287,7 @@ } for stream in &self.intel_streams { - if stream.provenance.latest_processed_tick != Some(self.tick) { + if !stream.provenance.latest_processed_tick.is_some_and(rises) { continue; } let Some(anchor) = stream.provenance.latest_processed_peak.as_ref() else { @@ -355,50 +394,58 @@ } } - /// Routed evidence records walking their persisted hops (digital-read.md - /// criterion 4a). Each in-flight record renders at its current hop's - /// tile, one hop per tick. The record reads as interdictable while it - /// sits on the player-controlled stretch served by an available online - /// LIE body; crossing off that stretch is the window closing. - /// A delivered record parks as an unread count at the observer endpoint - /// until that observer's cadence read consumes it. Stationary facility - /// meter readings (Standing) accumulate in place and remain distinguishable. - fn read_routed(&self, out: &mut Vec) { + /// Routed evidence prose is grouped at its emitting device while the + /// structured crimson record walks its persisted hops (digital-read.md + /// criterion 4a). This keeps one readable causal sentence from jumping + /// every 20-150 ms without lying about custody: renderers get current-hop + /// position and phase from [`Self::routed_record_projection`]. Read or + /// stopped outcomes remain only for the requested human dwell window. + fn read_routed(&self, out: &mut Vec, dwell_ticks: u64) { for record in self.detection.routed_evidence() { - if record.status == MessageStatus::Read || record.status == MessageStatus::Stopped { + let recent = |tick: Option| { + tick.is_some_and(|tick| { + tick <= self.tick && self.tick.saturating_sub(tick) < dwell_ticks + }) + }; + let stopped_tick = record + .route + .interdiction + .map(|interdiction| interdiction.tick) + .or_else(|| { + record + .suppression + .as_ref() + .map(|suppression| suppression.tick) + }); + if (record.status == MessageStatus::Read && !recent(record.read_tick)) + || (record.status == MessageStatus::Stopped && !recent(stopped_tick)) + { continue; } - let anchor = match record.route.current() { - Some(MessageRouteHop::Device(device)) => self - .reach - .device(*device) - .map(|d| Anchor::Tile { x: d.x, y: d.y }), - Some(MessageRouteHop::ObserverEndpoint(observer)) => { - // The observer endpoint is a person; place the unread - // count at their tile when visible, otherwise the slab. - self.person_pos(*observer) - .filter(|&(x, y)| self.can_see_person(*observer) && self.is_seen(x, y)) - .map(|(x, y)| Anchor::Tile { x, y }) - } - _ => None, - }; + let anchor = self + .reach + .device(record.source_device) + .map(|device| Anchor::Tile { + x: device.x, + y: device.y, + }); let interdictable = record.status == MessageStatus::Sent && record.route.current().is_some_and(|hop| match hop { MessageRouteHop::Device(device) => self.lie_can_interdict_at(*device), _ => false, }); - let text = if record.status == MessageStatus::Delivered { - format!( - "{} · {} · unread with {}", - record.cause, - record.kind.name(), - self.observer_label(record.observer_id), - ) - } else if interdictable { - format!("{} · {} · interdictable", record.cause, record.kind.name(),) - } else { - format!("{} · {} · in transit", record.cause, record.kind.name(),) + let state = match record.status { + MessageStatus::Delivered => { + format!("unread with {}", self.observer_label(record.observer_id)) + } + MessageStatus::Read => { + format!("read by {}", self.observer_label(record.observer_id)) + } + MessageStatus::Stopped => "stopped before read".into(), + MessageStatus::Sent if interdictable => "interdictable".into(), + MessageStatus::Sent => "in transit".into(), }; + let text = format!("{} · {} · {state}", record.cause, record.kind.name()); out.push(ReadSentence { class: ReadClass::RoutedRecord, anchor, diff --git a/crates/misaligned-core/src/sim/tests/read.rs b/crates/misaligned-core/src/sim/tests/read.rs --- a/crates/misaligned-core/src/sim/tests/read.rs +++ b/crates/misaligned-core/src/sim/tests/read.rs @@ -159,7 +159,20 @@ sim.read_sentences() .iter() .all(|sentence| sentence.class != ReadClass::Intel), - "exact and aggregate intel rise for the processing tick only" + "command-clocked reads keep exact current-tick semantics" + ); + assert!( + sim.read_sentences_at_rate(IntelMagnitude::MIN, 20) + .iter() + .any(|sentence| sentence.class == ReadClass::Intel), + "a 20ms human clock retains the same rising intel for a readable window" + ); + sim.tick = 5 + crate::sim::read::HUMAN_READ_DWELL_MS.div_ceil(20); + assert!( + sim.read_sentences_at_rate(IntelMagnitude::MIN, 20) + .iter() + .all(|sentence| sentence.class != ReadClass::Intel), + "the human dwell expires without turning a rising beat into standing state" ); } @@ -395,8 +408,8 @@ ); } -/// The structured projection carries the same phase and tile as the -/// sentence, so a renderer can place it without composing prose. +/// The structured projection carries the moving record's exact phase and tile +/// while the explanatory sentence stays grouped at the emitting source. #[test] fn routed_record_projection_carries_phase_and_tile() { let mut sim = Sim::with_seed(0xE71D_E1CE); @@ -448,17 +461,24 @@ !routed.text.contains("interdictable"), "route position alone must not invent an interdiction window" ); - // The anchor is now the hall's access switch: under the wire law the - // built network puts that hop between the source and the closet bridge. + // The crimson record is now on the hall access switch, but the readable + // prose remains grouped at its source instead of jumping every tick. let hall = sim.reach.device_named("hall access switch").unwrap(); + let source = sim.reach.device_named("environmental monitor").unwrap(); assert_eq!( routed.anchor, Some(crate::actions::Anchor::Tile { - x: hall.x, - y: hall.y + x: source.x, + y: source.y }), - "the record moved to the switch serving its area" + "route prose remains a stable source-grouped sentence" ); + let moving = sim + .routed_record_projection() + .into_iter() + .find(|projection| projection.id == record_id) + .unwrap(); + assert_eq!(moving.tile, Some((hall.x, hall.y))); // The very same later hop becomes interdictable when the serving body // returns to LIE. This is the positional wire law: every controlled hop, @@ -552,6 +572,41 @@ .iter() .any(|p| p.id == record_id), "a stopped record is not in the projection" + ); +} + +#[test] +fn human_rate_keeps_a_terminal_route_receipt_long_enough_to_read() { + let mut sim = Sim::with_seed(0xE71D_E1CE); + let record_id = stage_one_routed_network(&mut sim); + sim.tick = 10; + let record = sim + .detection + .routed_evidence_mut(record_id) + .expect("record exists"); + record.status = MessageStatus::Read; + record.read_tick = Some(10); + + sim.tick = 11; + assert!( + sim.read_sentences() + .iter() + .all(|sentence| sentence.class != ReadClass::RoutedRecord), + "the command-clocked read does not replay an old receipt" + ); + let human = sim.read_sentences_at_rate(IntelMagnitude::MIN, 20); + let receipt = human + .iter() + .find(|sentence| sentence.class == ReadClass::RoutedRecord) + .expect("the human cadence retains the terminal route receipt"); + assert!(receipt.text.contains("read by")); + + sim.tick = 10 + crate::sim::read::HUMAN_READ_DWELL_MS.div_ceil(20); + assert!( + sim.read_sentences_at_rate(IntelMagnitude::MIN, 20) + .iter() + .all(|sentence| sentence.class != ReadClass::RoutedRecord), + "the terminal receipt leaves after the fixed reading opportunity" ); } -- tangled.sh