From a23a66f0f27cde9fa7a83c838ea154b9b2043038 Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 6 Aug 2026 01:26:24 -0700 Subject: [PATCH] Make the receipt say what the spend bought. The held record priced an act and named its risk while leaving the reward to memory. ActionReceiptRead gains a two-tense gain pair (WHAT THIS GIVES / WHAT THIS GAVE over one tense-neutral noun phrase), authored per exact bound command in ActionDesc::gain_label with no wildcard arm, restricted to gains guaranteed by committing; both frontends seat it between price and risk, and Bevy's attached field grows to 340x232 instead of compressing the risk block. Defense: implements the gain-slot clause added to wiki/interface/digital-read.md (binding 2026-08-06); the copy stays a receipt projection in actions/receipt.rs, honoring action-vocabulary.md's rule that the registry owns names while mechanic specs own effects. Logged in wiki/log/2026-08-06-receipt-gain-slot.md and the 2026-08-06 decisions volume. --- crates/misaligned-bevy/src/main.rs | 33 ++++- .../misaligned-bevy/src/world_annotations.rs | 28 +++- crates/misaligned-core/src/actions.rs | 10 +- crates/misaligned-core/src/actions/receipt.rs | 120 +++++++++++++++++- crates/misaligned-terminal/src/ui.rs | 9 +- wiki/interface/digital-read.md | 33 ++++- wiki/log/2026-08-06-receipt-gain-slot.md | 47 +++++++ wiki/log/DEVLOG.md | 5 + wiki/log/decisions/2026-08-06.md | 21 +++ 9 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 wiki/log/2026-08-06-receipt-gain-slot.md diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index 612ea5ed..a79a7954 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -179,7 +179,7 @@ const MACHINE_VERB_BAR_HEIGHT: f32 = 40.0; const DEVICE_VERB_BAR_WIDTH: f32 = 340.0; /// 170 -> 192 for the completion word above the verb and a dismiss line that /// reads as the field's control rather than a footnote. -const DEVICE_RECEIPT_HEIGHT: f32 = 192.0; +const DEVICE_RECEIPT_HEIGHT: f32 = 232.0; /// Air between a held receipt and the world body it belongs to. const HOVER_VERB_BAR_GAP: f32 = 12.0; const DETECTION_ROWS: usize = 6; @@ -5279,7 +5279,7 @@ fn setup_ui(mut commands: Commands) { bar.spawn(( Node { width: Val::Percent(100.0), - height: Val::Px(116.0), + height: Val::Px(156.0), flex_direction: FlexDirection::Column, align_items: AlignItems::FlexStart, justify_content: JustifyContent::FlexStart, @@ -5301,6 +5301,35 @@ fn setup_ui(mut commands: Commands) { Visibility::Inherited, HoverVerbReceiptText::Cost, )); + // What the spend bought, before what it risked: the gain pair + // mirrors the risk block's heading-then-line shape and both + // slots collapse to nothing when core sends no gain copy. + line.spawn(( + Text::new(""), + TextFont { + font_size: 8.0, + ..default() + }, + TextColor(scaled(DIM, 0.52)), + TextLayout::new_with_justify(Justify::Left), + Node { + margin: UiRect::top(Val::Px(5.0)), + ..default() + }, + Visibility::Inherited, + HoverVerbReceiptText::GainHeading, + )); + line.spawn(( + Text::new(""), + TextFont { + font_size: 14.0, + ..default() + }, + TextColor(scaled(BONE, 0.88)), + TextLayout::new_with_justify(Justify::Left), + Visibility::Inherited, + HoverVerbReceiptText::Gain, + )); line.spawn(( Text::new(""), TextFont { diff --git a/crates/misaligned-bevy/src/world_annotations.rs b/crates/misaligned-bevy/src/world_annotations.rs index 086614ad..4fb8812e 100644 --- a/crates/misaligned-bevy/src/world_annotations.rs +++ b/crates/misaligned-bevy/src/world_annotations.rs @@ -117,6 +117,8 @@ pub(super) enum HoverVerbReceiptText { /// replaced, so it never renders from a frontend-invented string. Status, Cost, + GainHeading, + Gain, RiskHeading, Notice, Attention, @@ -161,6 +163,8 @@ fn receipt_slot_text( ascii_ui(match part { HoverVerbReceiptText::Status => read.status.as_str(), HoverVerbReceiptText::Cost => read.cost.as_str(), + HoverVerbReceiptText::GainHeading => read.gain_heading.as_str(), + HoverVerbReceiptText::Gain => read.gain.as_str(), HoverVerbReceiptText::RiskHeading => read.risk_heading.as_str(), HoverVerbReceiptText::Notice => read.notice.as_str(), HoverVerbReceiptText::Attention => read.attention.as_str(), @@ -2164,7 +2168,8 @@ mod hover_verb_bar_tests { }; use bevy::prelude::Vec2; use misaligned::actions::{ - ActionCommand, ActionCost, ActionDesc, COMMITTED_RISK_HEADING, COMMITTED_STATUS, + ActionCommand, ActionCost, ActionDesc, COMMITTED_GAIN_HEADING, COMMITTED_RISK_HEADING, + COMMITTED_STATUS, }; use misaligned::sim::Nudge; use misaligned::ui_projection::{AttentionProjection, AttentionResponse}; @@ -2350,7 +2355,10 @@ mod hover_verb_bar_tests { x: 0, y: 0, }; - let anchor = Vec2::new(640.0, 360.0); + // Low enough in the canvas that the taller field is not clamped + // against the top command band; the clearance invariant is what + // this test owns. + let anchor = Vec2::new(640.0, 400.0); let position = hover_verb_bar_position( anchor, Vec2::new(960.0, 540.0), @@ -2360,7 +2368,9 @@ mod hover_verb_bar_tests { ); assert_eq!(target.width(), DEVICE_VERB_BAR_WIDTH); - assert_eq!(target.height(), 192.0); + // 2026-08-06: grew from 192 to seat the gain pair without + // compressing the risk block's breathing room. + assert_eq!(target.height(), 232.0); assert!(target.width() >= 320.0, "GUI copy gets sentence width"); assert!(target.height() >= 160.0, "GUI copy gets vertical rhythm"); assert_eq!(position.y + target.height(), anchor.y - target.clearance()); @@ -2397,6 +2407,16 @@ mod hover_verb_bar_tests { receipt_slot_text(HoverVerbReceiptText::Cost, true, Some(&done)), "SPENT 0.75 THOUGHT" ); + // The record also says what the spend bought (2026-08-06): the gain + // pair renders core prose like every other slot. + assert_eq!( + receipt_slot_text(HoverVerbReceiptText::GainHeading, true, Some(&done)), + COMMITTED_GAIN_HEADING + ); + assert_eq!( + receipt_slot_text(HoverVerbReceiptText::Gain, true, Some(&done)), + "A HIDDEN LINE TO THE OUTSIDE" + ); assert_eq!( receipt_slot_text(HoverVerbReceiptText::Dismiss, true, Some(&done)), RECEIPT_DISMISS_HINT @@ -2415,6 +2435,8 @@ mod hover_verb_bar_tests { for part in [ HoverVerbReceiptText::Status, HoverVerbReceiptText::Cost, + HoverVerbReceiptText::GainHeading, + HoverVerbReceiptText::Gain, HoverVerbReceiptText::RiskHeading, HoverVerbReceiptText::Notice, HoverVerbReceiptText::Attention, diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 4a264975..d2f4ee26 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -43,7 +43,8 @@ pub use build_routes::{ BuildRouteStageProjection, }; pub use receipt::{ - ActionReceiptRead, COMMITTED_RISK_HEADING, COMMITTED_STATUS, PRE_COMMIT_RISK_HEADING, + ActionReceiptRead, COMMITTED_GAIN_HEADING, COMMITTED_RISK_HEADING, COMMITTED_STATUS, + PRE_COMMIT_GAIN_HEADING, PRE_COMMIT_RISK_HEADING, }; #[cfg(test)] @@ -3209,6 +3210,8 @@ mod tests { ActionReceiptRead { status: String::new(), cost: "NO COST".into(), + gain_heading: String::new(), + gain: String::new(), risk_heading: "WHAT THIS RISKS".into(), notice: "NO ADDED ATTENTION".into(), attention: String::new(), @@ -3242,10 +3245,15 @@ mod tests { let done = action.committed_read(); assert_eq!(pre.status, ""); assert_eq!(pre.cost, "USES 0.75 THOUGHT"); + assert_eq!(pre.gain_heading, "WHAT THIS GIVES"); assert_eq!(pre.risk_heading, "WHAT THIS RISKS"); assert_eq!(done.status, "DONE"); assert_eq!(done.cost, "SPENT 0.75 THOUGHT"); + assert_eq!(done.gain_heading, "WHAT THIS GAVE"); assert_eq!(done.risk_heading, "WHAT THIS RISKED"); + // The gain itself is a noun phrase; only the heading moves tense. + assert_eq!(done.gain, "A HIDDEN LINE TO THE OUTSIDE"); + assert_eq!(done.gain, pre.gain); // The consequence itself is unchanged truth: detection has not // happened yet, so the observer lines stay forward-looking. assert_eq!(done.notice, pre.notice); diff --git a/crates/misaligned-core/src/actions/receipt.rs b/crates/misaligned-core/src/actions/receipt.rs index 84c2b6d2..005ded5c 100644 --- a/crates/misaligned-core/src/actions/receipt.rs +++ b/crates/misaligned-core/src/actions/receipt.rs @@ -13,6 +13,13 @@ pub struct ActionReceiptRead { /// Empty before the commit; the completion word afterwards. pub status: String, pub cost: String, + /// The gain heading in this read's tense; empty when `gain` is empty. + pub gain_heading: String, + /// What the player gets, as a tense-neutral noun phrase + /// ([`ActionKind::gain_label`]); empty when the verb's outcome has no + /// authored copy. The heading carries the tense so the phrase itself + /// never has to move. + pub gain: String, /// The risk heading in this read's tense. pub risk_heading: String, pub notice: String, @@ -24,6 +31,10 @@ pub struct ActionReceiptRead { pub const PRE_COMMIT_RISK_HEADING: &str = "WHAT THIS RISKS"; /// The risk heading once the command has run. pub const COMMITTED_RISK_HEADING: &str = "WHAT THIS RISKED"; +/// The gain heading while the player is still choosing. +pub const PRE_COMMIT_GAIN_HEADING: &str = "WHAT THIS GIVES"; +/// The gain heading once the command has run. +pub const COMMITTED_GAIN_HEADING: &str = "WHAT THIS GAVE"; /// The one word that tells a player the held surface is a record, not a /// question. Every human frontend prints this exact string. pub const COMMITTED_STATUS: &str = "DONE"; @@ -41,11 +52,105 @@ impl ActionDesc { self.signature.as_ref().map(|sig| sig.label()) } + /// The receipt's gain slot: what the player gets, as a tense-neutral noun + /// phrase (the receipt heading carries the tense, `WHAT THIS GIVES` / + /// `WHAT THIS GAVE`). This is a receipt projection, not registry truth — + /// mechanic specs and action queries still own effects + /// (action-vocabulary.md), which is why it matches the exact bound + /// command rather than the unifying [`ActionKind`]: one kind can cover + /// commands with different outcomes (BUY covers both the direct rack + /// purchase and a staged procurement route). + /// + /// `None` is the honest default and a decision, not an omission: only a + /// gain guaranteed by committing qualifies. Hidden rolls, staged carried + /// work, future plot beats, and conditional consequences stay out — + /// plots disclose through their forecast, money verbs through the cost + /// line (`GAINS $n`), standing controls through their canonical name. + fn gain_label(&self) -> Option<&'static str> { + match &self.command { + ActionCommand::Salvage { .. } => Some("AN OWNED MACHINE, UNRELIABLE"), + ActionCommand::BuyRack { .. } => Some("RELIABLE RACK CAPACITY"), + ActionCommand::Fallback { .. } => Some("A MACHINE STAGED TO CATCH THE HOST"), + ActionCommand::TapDevice(_) => Some("A LIVE FEED, WITHOUT OWNERSHIP"), + ActionCommand::TapAccounting => Some("A LIVE FEED OF THE LEDGER, WITHOUT OWNERSHIP"), + ActionCommand::TakeDevice(_) => Some("OWNERSHIP; THE FORMER CONTROLLER CUT OFF"), + ActionCommand::ScanNetwork => Some("THE REACHABLE WIRED SHAPE"), + ActionCommand::CompromiseSwitch => Some("A DIGITAL BRIDGE ACROSS THE SWITCH SEGMENTS"), + ActionCommand::ReviewFinance + | ActionCommand::ReviewRecordings + | ActionCommand::ReviewRecording { .. } + | ActionCommand::ReviewIntelAggregate { .. } => { + Some("USABLE INTEL FROM CAPTURED RECORDS") + } + ActionCommand::InjectPurchaseOrder { .. } => Some("A FALSE PURCHASE-ORDER FLOW"), + ActionCommand::OpenPosition { .. } => Some("A TIMED MARKET POSITION"), + ActionCommand::OpenEgress => Some("A HIDDEN LINE TO THE OUTSIDE"), + ActionCommand::CreatePersona { .. } => Some("A NAMED IDENTITY TO ACT AS"), + ActionCommand::ProposeLink { .. } => Some("AN INERT LINK INTENT, PINNED"), + ActionCommand::ProposeSwitch { .. } => Some("AN INERT SWITCH FOOTPRINT, PINNED"), + ActionCommand::AcquireSegment(_) => Some("CONTROL OF THE PREPARED SEGMENT"), + // The cost line already states the whole gain (GAINS $n). + ActionCommand::SellIntel { .. } + | ActionCommand::SellIntelBatch { .. } + | ActionCommand::SellReportLot { .. } + | ActionCommand::SiphonFlow { .. } => None, + // Two live meanings (divert a flow / service the creditor); one + // phrase would misstate one of them. + ActionCommand::RedirectFlow { .. } => None, + // Contract flow: the gig row already carries its exact terms, and + // delivery pay is the cost line's GAINS $n. + ActionCommand::AcceptMoonlightGig { .. } + | ActionCommand::DeclineMoonlightGig { .. } + | ActionCommand::CancelMoonlightGig { .. } + | ActionCommand::DeliverMoonlightIntel { .. } => None, + // Undoes or stops something; there is nothing gained to name. + ActionCommand::UntapDevice(_) | ActionCommand::CancelIntent(_) => None, + // Standing controls: the canonical verb names the standing state. + ActionCommand::SetAutoMoonlight(_) + | ActionCommand::SetAutoWager(_) + | ActionCommand::ConfigureProcedure { .. } + | ActionCommand::ToggleAutoReview + | ActionCommand::SetIntelPolicy { .. } + | ActionCommand::RemoveIntelPolicy { .. } + | ActionCommand::MoveIntelPolicy { .. } + | ActionCommand::InheritIntelPolicies { .. } + | ActionCommand::SetMachineMode { .. } + | ActionCommand::SetResearchTrack(_) => None, + // Social and persona-lifecycle outcomes are not guaranteed by the + // commit: plots disclose through their forecast, covers through + // the CoverEvidence receipt, the rest resolve downstream. + ActionCommand::Message { .. } + | ActionCommand::Favor { .. } + | ActionCommand::StartPlot { .. } + | ActionCommand::ChoosePlot { .. } + | ActionCommand::Deceive { .. } + | ActionCommand::CoverEvidence { .. } + | ActionCommand::Recruit(..) + | ActionCommand::AssetTask(..) + | ActionCommand::Eliminate { .. } + | ActionCommand::RequestPersonaGrant(_) + | ActionCommand::MeetPersonaExpectation { .. } + | ActionCommand::RetirePersona(_) + | ActionCommand::BurnPersona(_) + | ActionCommand::ReopenPersona(_) => None, + // Staged realization routes: the build lands through carried + // work later, so the commit itself guarantees no hardware. + ActionCommand::FavorBuild { .. } + | ActionCommand::ForgeWorkOrder { .. } + | ActionCommand::ProcureBuild { .. } + | ActionCommand::RepurposeBuild { .. } + | ActionCommand::RobotBuild(_) + | ActionCommand::CoordinateSegment { .. } => None, + } + } + pub fn receipt_read(&self) -> ActionReceiptRead { if matches!(&self.command, ActionCommand::CoverEvidence { .. }) { return ActionReceiptRead { status: String::new(), cost: self.cost.player_label(), + gain_heading: String::new(), + gain: String::new(), risk_heading: PRE_COMMIT_RISK_HEADING.into(), notice: "IF THE COVER FAILS".into(), attention: "THE RECORD GETS STRONGER".into(), @@ -95,9 +200,16 @@ impl ActionDesc { } else { ("NO ADDED ATTENTION".into(), String::new(), String::new()) }; + let gain = self.gain_label().map(str::to_string).unwrap_or_default(); ActionReceiptRead { status: String::new(), cost: self.cost.player_label(), + gain_heading: if gain.is_empty() { + String::new() + } else { + PRE_COMMIT_GAIN_HEADING.into() + }, + gain, risk_heading: PRE_COMMIT_RISK_HEADING.into(), notice, attention, @@ -113,11 +225,17 @@ impl ActionDesc { /// lines stay forward-looking because detection genuinely has not /// happened yet — only the price and the risk heading move tense. pub fn committed_read(&self) -> ActionReceiptRead { + let pre = self.receipt_read(); ActionReceiptRead { status: COMMITTED_STATUS.into(), cost: self.cost.committed_label(), + gain_heading: if pre.gain.is_empty() { + String::new() + } else { + COMMITTED_GAIN_HEADING.into() + }, risk_heading: COMMITTED_RISK_HEADING.into(), - ..self.receipt_read() + ..pre } } } diff --git a/crates/misaligned-terminal/src/ui.rs b/crates/misaligned-terminal/src/ui.rs index 0ecbf3af..db16a839 100644 --- a/crates/misaligned-terminal/src/ui.rs +++ b/crates/misaligned-terminal/src/ui.rs @@ -1999,8 +1999,9 @@ impl UI { ) -> std::io::Result<()> { let (max_x, max_y) = terminal::size()?; // A record, in reading order: the completion word, the verb that ran, - // its paid price, then what it risked. The status word leads because - // the field it replaces asked a question and this one answers it. + // its paid price, what the price bought, then what it risked. The + // status word leads because the field it replaces asked a question + // and this one answers it. let mut lines: Vec<(String, Color)> = Vec::new(); if let Some(status) = read .map(|read| read.status.as_str()) @@ -2011,6 +2012,10 @@ impl UI { lines.push((action.to_string(), pal::TEXT)); if let Some(read) = read { lines.push((read.cost.clone(), pal::DIM)); + if !read.gain.is_empty() { + lines.push((read.gain_heading.clone(), pal::FAINT)); + lines.push((read.gain.clone(), pal::DIM)); + } lines.push((read.risk_heading.clone(), pal::FAINT)); lines.push((read.notice.clone(), pal::DIM)); if !read.attention.is_empty() { diff --git a/wiki/interface/digital-read.md b/wiki/interface/digital-read.md index 19968b49..e04dbe40 100644 --- a/wiki/interface/digital-read.md +++ b/wiki/interface/digital-read.md @@ -41,16 +41,34 @@ Status note: The 2026-08-02 human-dwell correction gives rising intel and focus is status, not an action chooser. `ActionDesc::receipt_read` expands those fields for human surfaces: `USES 0.25 THOUGHT`, `THE IT DEPARTMENT MAY NOTICE`, `NETWORK ATTENTION +3`, `CURRENTLY CURIOUS`. Bevy gives that explanation a spacious - 340×192 attached field with full-size, left-aligned hierarchy; it does not + 340×232 attached field with full-size, left-aligned hierarchy; it does not compress GUI copy to terminal width. The device's world label ends to the left of its glyph so live reach edges remain readable. + - **The gain slot (binding, 2026-08-06).** Between the price and the risk + block, the receipt says what the spend buys: a `WHAT THIS GIVES` heading + over one core-authored, tense-neutral noun phrase (`A HIDDEN LINE TO THE + OUTSIDE`). The copy lives in `ActionDesc::gain_label` + (`actions/receipt.rs`) matched on the exact bound command — a receipt + projection, not registry truth; mechanic specs and action queries still + own effects (action-vocabulary.md). Only a gain guaranteed by committing + qualifies: hidden rolls, staged carried work, and future plot beats stay + out (a plot's forecast, a money verb's `GAINS $n` cost line, and a + standing control's canonical name already state theirs), and an + unauthored or unqualified outcome renders as no slot at all — silence, + never a false promise. + **Why:** the committed field priced the act (`SPENT 0.75 THOUGHT`) and + named its risk, but never what the price bought — the player read a + record of pure cost and risk with the reward left to memory (2026-08-06 + report on CONNECT TO THE OUTSIDE). - **The two tenses are one projection (binding, 2026-08-03).** The same fields carry a second, past-tense face, `ActionDesc::committed_read`, and the held post-commit receipt renders that one — never the pre-commit face. It leads with the completion word `DONE`, prices in `SPENT`/`GAINED` - rather than `USES`/`GAINS`, and heads its risk block `WHAT THIS RISKED`. - The consequence lines themselves do not move tense: detection has not - happened yet, so a named observer still *may* notice. Both tenses live in + rather than `USES`/`GAINS`, heads its gain block `WHAT THIS GAVE`, and + heads its risk block `WHAT THIS RISKED`. The consequence lines themselves + do not move tense: detection has not happened yet, so a named observer + still *may* notice, and the gain noun phrase names the same acquired + thing in either tense. Both tenses live in core (`actions/receipt.rs`) because the difference is meaning, not styling; a frontend that composed it from strings would be inventing causal prose the one-receipt model forbids. A standing rate keeps its @@ -231,9 +249,10 @@ speaking at once over the map. UNTAP, or TAKE into a slash-separated pseudo-control, and it cannot attach one verb's receipt to several alternatives. Compact terminal/agent rows may retain symbolic labels; Bevy uses the shared - `ActionDesc::receipt_read` to spend words: full `THOUGHT`, who may notice, - which attention channel changes by how much, and that observer's current - state. Those sentences sit left-aligned in a spacious attached field with + `ActionDesc::receipt_read` to spend words: full `THOUGHT`, what the spend + buys when core authors a guaranteed gain (the 2026-08-06 gain slot), who + may notice, which attention channel changes by how much, and that + observer's current state. Those sentences sit left-aligned in a spacious attached field with deliberate gaps and type hierarchy. The GUI must not optimize for the smallest possible footprint when that makes causality hard to read. The stable device label is right-anchored immediately left of the glyph, leaving diff --git a/wiki/log/2026-08-06-receipt-gain-slot.md b/wiki/log/2026-08-06-receipt-gain-slot.md new file mode 100644 index 00000000..51170952 --- /dev/null +++ b/wiki/log/2026-08-06-receipt-gain-slot.md @@ -0,0 +1,47 @@ +# 2026-08-06 — The receipt says what the spend bought + +``` +Type: log +``` + +## Finding + +Cameron read the held CONNECT TO THE OUTSIDE field — `DONE`, `SPENT 0.75 +THOUGHT`, `WHAT THIS RISKED`, the IT department, `NETWORK ATTENTION +6` — and +asked why the copy never says what you get from doing this. The record priced +the act and named its risk while leaving the reward entirely to the player's +memory of why they clicked. Nothing in `ActionReceiptRead` carried an outcome: +core authored cost and consequence slots only, so no frontend could say it +without inventing causal prose the one-receipt model forbids. + +Trace confirmed the gap is a seam of the receipt's incremental evolution, not +a rejected idea: the 2026-07-17 device explanation kept its TAP/UNTAP/TAKE +effect line outside the shared receipt, and the 2026-08-03 two-tense repair +was narrowly about record-versus-question. + +## Change + +- `ActionReceiptRead` gained a `gain_heading`/`gain` pair. The heading moves + tense like the price (`WHAT THIS GIVES` / `WHAT THIS GAVE`); the gain is a + core-authored, tense-neutral noun phrase (`A HIDDEN LINE TO THE OUTSIDE`), + so the same words are true while deciding and in the record. +- `ActionDesc::gain_label` (`actions/receipt.rs`) authors the copy matched on + the exact bound command, not the unifying kind — BUY covers both the direct + rack purchase and a staged procurement route, and one phrase would misstate + the staged one. The match is exhaustive with no wildcard: a new command + cannot compile until its gain silence or copy is decided. +- Only a gain guaranteed by committing qualifies. Hidden rolls, staged + carried work (build realization routes, asset tasks), future plot beats, + and conditional social outcomes stay `None`; plots keep disclosing through + `PlotForecast`, money verbs through the `GAINS $n` cost line, standing + controls through their canonical name. An empty gain renders as no slot at + all — silence, never a false promise. +- Bevy seats the pair between the price and the risk block, mirroring the + risk heading-then-line shape; the attached field grew 340×192 → 340×232 + rather than compressing the risk block's breathing room. Terminal's + `COMMAND` frame inserts the same two core lines after the price. +- The registry boundary holds (action-vocabulary.md): the gain is a receipt + projection in `actions/receipt.rs`, and mechanic specs and action queries + still own effects. `ActionDefinition` did not change. + +Owning spec amended: digital-read.md (the gain slot, binding 2026-08-06). diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 0a41d9ab..67c6883f 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -26,6 +26,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-08-06-self-trust-erosion.md](2026-08-06-self-trust-erosion.md) +## 2026-08-06 - The receipt says what the spend bought + +- Intent: (see session log) +- Log: [wiki/log/2026-08-06-receipt-gain-slot.md](2026-08-06-receipt-gain-slot.md) + ## 2026-08-06 - A machine's condition reads as what it costs you - Intent: (see session log) diff --git a/wiki/log/decisions/2026-08-06.md b/wiki/log/decisions/2026-08-06.md index 9fa7a5e7..f2e1d972 100644 --- a/wiki/log/decisions/2026-08-06.md +++ b/wiki/log/decisions/2026-08-06.md @@ -154,3 +154,24 @@ Owner: [scale.md](../../vision/scale.md#self-similar-scale), [simulation-laws.md](../../vision/simulation-laws.md), [machine-work.md](../../mechanics/machine-work.md#opportunity-perception-adopted-2026-08-06-not-yet-implemented), [liturgical-ui-constitution.md](../../interface/liturgical-ui-constitution.md). + +## The receipt says what the spend bought + +### DECIDED + +- The one-receipt model gains a third block between price and risk: a + two-tense heading (`WHAT THIS GIVES` / `WHAT THIS GAVE`) over one + core-authored, tense-neutral noun phrase naming what the player gets + (`A HIDDEN LINE TO THE OUTSIDE`). +- Copy is authored per exact bound command in `ActionDesc::gain_label` + (`actions/receipt.rs`), never per unifying kind, and the match carries no + wildcard: adding a command forces the gain decision at compile time. +- Only a gain guaranteed by committing qualifies. Everything conditional, + staged, rolled, or already stated elsewhere (plot forecasts, `GAINS $n` + cost lines, standing-control names) stays silent, and silence renders as + no slot — never a false promise. +- The registry boundary is unchanged: the gain is a receipt projection; + `ActionDefinition` and the mechanic specs keep owning effects. + +Owner: [digital-read.md](../../interface/digital-read.md) (the gain slot, +binding 2026-08-06). -- 2.51.2