From 17b327433b53b09a0ef6213d02e102f253ab3463 Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 8 Jul 2026 13:26:07 -0700 Subject: [PATCH] Close economy.md: in-panel risk bands and signature scale tests. Finance panels now preview observer-band risk for inject/siphon/redirect/wager before commit; tests assert Financial signature sizes and Priya (not Dana). Defense: economy.md player surface and criterion 8 require risk as an observer band before commit, not only in the context menu; criteria 3-4 require scaled Financial signatures on inject/redirect. Closing those acceptance gaps flips the B1 money-as-flows spec to IMPLEMENTED without expanding into markets.md. Co-authored-by: Cursor --- src/actions.rs | 79 ++++++++++++++++++++++++++++++ src/bin/bevy.rs | 9 ++++ src/bin/terminal/agent.rs | 5 ++ src/bin/terminal/ui.rs | 23 ++++++++- src/sim.rs | 88 +++++++++++++++++++++++++++++++++- wiki/log/2026-07-08-economy.md | 32 +++++++++++++ wiki/log/DEVLOG.md | 13 +++++ wiki/mechanics/economy.md | 16 +++---- wiki/process/ROADMAP.md | 30 +++++------- wiki/process/specs.md | 2 +- 10 files changed, 269 insertions(+), 28 deletions(-) create mode 100644 wiki/log/2026-07-08-economy.md diff --git a/src/actions.rs b/src/actions.rs index 89d4034d..d9a87ed5 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1201,6 +1201,54 @@ impl Sim { band: Band::of(o.suspicion), }) } + + /// Renderer-neutral risk preview lines for the finance panel + /// (economy.md player surface / criterion 8): money verbs show the + /// observer band they feed before commit. Amounts match the default + /// menu verbs so the panel and the menu agree. + pub fn finance_risk_preview_lines(&self) -> Vec { + let mut lines = Vec::new(); + if self.accounts.known_flows().next().is_none() { + lines.push("risk: locked until the books are read".into()); + return lines; + } + let inject = 300; + if let Some(sig) = + self.signature_note(SignatureKind::Financial, Self::financial_sig_size(inject)) + { + lines.push(format!("inject ${inject} · {}", sig.label())); + } + let siphon = 50; + if let Some(sig) = + self.signature_note(SignatureKind::Financial, Self::financial_sig_size(siphon)) + { + lines.push(format!("siphon ${siphon} · {}", sig.label())); + } + let redirect = 25; + if let Some(sig) = self.signature_note( + SignatureKind::Financial, + Self::financial_sig_size(redirect) + 1, + ) { + lines.push(format!("redirect ${redirect} · {}", sig.label())); + } + let stake = 100; + if let Some(sig) = self.signature_note(SignatureKind::Network, 1) { + lines.push(format!("wager ${stake} · {}", sig.label())); + } + lines + } + + /// Per-flow risk lines for the selected known flow (siphon/redirect + /// amounts match `flow_actions`). + pub fn flow_risk_preview_lines(&self, flow_id: AccountFlowId) -> Vec { + self.available_actions(Anchor::Flow(flow_id)) + .into_iter() + .filter_map(|a| { + let sig = a.signature.as_ref()?; + Some(format!("{} · {}", a.verb, sig.label())) + }) + .collect() + } } #[cfg(test)] @@ -1325,6 +1373,37 @@ mod tests { assert!(s.income.moonlight.active); } + #[test] + fn finance_risk_preview_lines_stage_with_books() { + let mut s = sim(); + let locked = s.finance_risk_preview_lines(); + assert!( + locked.iter().any(|l| l.contains("locked")), + "risk preview locks before the books are read: {locked:?}" + ); + + let sw = switch(&s); + s.tap_device(sw); + assert!(s.review_financial_records()); + let lines = s.finance_risk_preview_lines(); + for (verb, kind) in [ + ("inject", "Financial"), + ("siphon", "Financial"), + ("redirect", "Financial"), + ("wager", "Network"), + ] { + let line = lines + .iter() + .find(|l| l.contains(verb)) + .unwrap_or_else(|| panic!("{verb} line present in {lines:?}")); + assert!(line.contains(kind), "{verb} feeds {kind}: {line}"); + assert!( + line.contains("[") && line.contains("]"), + "{verb} shows an observer band: {line}" + ); + } + } + /// Criterion 2 (person): a person with no sighting, no staged /// knowledge, and no recordings exposes nothing. #[test] diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index 7aaf3403..03108ea8 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -3807,6 +3807,15 @@ fn finance_panel_text(sim: &Sim, selected: usize) -> String { if !positions { s.push_str(" no open positions\n"); } + s.push_str("\nRISK\n"); + for line in sim.finance_risk_preview_lines() { + s.push_str(&format!(" {}\n", trunc(&line, 72))); + } + if let Some(f) = sim.accounts.known_flows().nth(selected) { + for line in sim.flow_risk_preview_lines(f.id) { + s.push_str(&format!(" {}\n", trunc(&line, 72))); + } + } s.push_str("\nenter / right-click: open actions menu on selected flow\n"); s.push_str( "(ledger + scheme verbs tap/process/inject/wager/moonlight/egress live on the switch)\n", diff --git a/src/bin/terminal/agent.rs b/src/bin/terminal/agent.rs index f63dc3f9..6b12fc53 100644 --- a/src/bin/terminal/agent.rs +++ b/src/bin/terminal/agent.rs @@ -1762,6 +1762,11 @@ fn render_finance(sim: &Sim) -> String { lines.push(panel_line("no open positions")); } lines.push(panel_rule()); + lines.push(panel_line("RISK")); + for line in sim.finance_risk_preview_lines() { + lines.push(panel_line(&line)); + } + lines.push(panel_rule()); lines.push(panel_line( "tap-ledger · review-finance · siphon [amt] · redirect [amt]", )); diff --git a/src/bin/terminal/ui.rs b/src/bin/terminal/ui.rs index 28b2fd6c..49b79691 100644 --- a/src/bin/terminal/ui.rs +++ b/src/bin/terminal/ui.rs @@ -1663,7 +1663,7 @@ impl UI { ) -> std::io::Result<()> { let (max_x, max_y) = terminal::size()?; let w: u16 = 76; - let h: u16 = 29; + let h: u16 = 34; let ox = (max_x.saturating_sub(w)) / 2; let oy = (max_y.saturating_sub(h)) / 2; let inner = (w - 4) as usize; @@ -1780,6 +1780,27 @@ impl UI { } if !shown { put(stdout, cx + 1, row, "No open positions.", pal::DIM)?; + row += 1; + } + + row += 1; + section(stdout, cx, row, "RISK", inner)?; + row += 1; + for line in sim.finance_risk_preview_lines() { + put( + stdout, + cx + 1, + row, + &trunc(&line, inner - 1), + pal::AMBER_DIM, + )?; + row += 1; + } + if let Some(flow) = flows.get(selected) { + for line in sim.flow_risk_preview_lines(flow.id) { + put(stdout, cx + 1, row, &trunc(&line, inner - 1), pal::DIM)?; + row += 1; + } } frame_rule(stdout, ox, oy + h - 4, w)?; diff --git a/src/sim.rs b/src/sim.rs index ac4b49f0..8a5b9e0b 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -2641,7 +2641,7 @@ impl Sim { }); } - fn financial_signature_size(amount: i32) -> i32 { + pub(crate) fn financial_signature_size(amount: i32) -> i32 { ((amount.abs() + 99) / 100).max(1) } @@ -5379,6 +5379,92 @@ mod tests { ); } + #[test] + fn inject_and_redirect_emit_scaled_financial_signatures() { + // economy.md criteria 3-4: Financial signature size scales with + // the take (one point per started $100); redirect adds +1 for the + // books-cook; HVAC inject raises Priya (Financial), not Dana. + let mut sim = Sim::with_seed(11); + let (accounts, flows) = sim.accounts.financial_snapshot_ids(); + sim.accounts.reveal_accounts_and_flows(&accounts, &flows); + + let priya_before = sim + .detection + .observers + .iter() + .find(|o| o.id == 3) + .unwrap() + .suspicion; + let network_before_inject = sim + .detection + .pending() + .iter() + .filter(|s| s.kind == SignatureKind::Network) + .count(); + + assert!(sim.inject_purchase_order(300, "HVAC controller")); + assert!( + sim.detection.pending().iter().any(|s| { + s.kind == SignatureKind::Financial && s.size == Sim::financial_signature_size(300) + }), + "inject $300 emits Financial size 3" + ); + let network_after_inject = sim + .detection + .pending() + .iter() + .filter(|s| s.kind == SignatureKind::Network) + .count(); + assert_eq!( + network_before_inject, network_after_inject, + "inject is Financial, not Network" + ); + + let flow = sim + .accounts + .known_flows() + .find(|f| f.amount >= 250 && f.active && f.to != sim.accounts.slush_id()) + .unwrap() + .id; + assert!(sim.siphon_flow(flow, 50)); + assert!( + sim.detection.pending().iter().any(|s| { + s.kind == SignatureKind::Financial && s.size == Sim::financial_signature_size(50) + }), + "small siphon emits Financial size 1" + ); + assert!(sim.siphon_flow(flow, 250)); + assert!( + sim.detection.pending().iter().any(|s| { + s.kind == SignatureKind::Financial && s.size == Sim::financial_signature_size(250) + }), + "large siphon emits Financial size 3" + ); + assert!(sim.redirect_flow_to_slush(flow, 25)); + assert!( + sim.detection.pending().iter().any(|s| { + s.kind == SignatureKind::Financial + && s.size == Sim::financial_signature_size(25) + 1 + }), + "redirect emits Financial size take+1" + ); + + // Starve concealment so pending converts; Financial feeds Priya. + sim.adjust_allocation(Channel::Concealment, -1); + run(&mut sim, 200); + let priya_after = sim + .detection + .observers + .iter() + .find(|o| o.id == 3) + .unwrap() + .suspicion; + assert!( + priya_after > priya_before, + "HVAC/financial acts raise Priya" + ); + } + #[test] fn injection_positions_and_intel_sales_settle_through_slush() { let mut sim = Sim::new(); diff --git a/wiki/log/2026-07-08-economy.md b/wiki/log/2026-07-08-economy.md new file mode 100644 index 00000000..90763d9c --- /dev/null +++ b/wiki/log/2026-07-08-economy.md @@ -0,0 +1,32 @@ +# 2026-07-08 — Economy polish + +``` +Type: log +``` + +## Intent + +Close the remaining economy.md B1 acceptance gaps after the account-graph +substrate landed: risk needed to appear in the finance panels before commit, +not only in context menus, and the money verbs needed explicit signature-size +regression coverage. + +## Changed + +- Added `Sim::finance_risk_preview_lines` and `Sim::flow_risk_preview_lines` + as renderer-neutral finance-panel risk lines. +- Terminal, Bevy, and agent-mode finance panels now show observer-band risk + previews for inject, siphon, redirect, and wager; terminal/Bevy also show + the selected known flow's siphon/redirect risk lines. +- Added regression coverage for locked-vs-earned finance risk previews and + scaled Financial signatures on inject/siphon/redirect, including HVAC + inject feeding Priya's Financial channel rather than Dana's Network channel. + +## Spec impact + +- `wiki/mechanics/economy.md` is now IMPLEMENTED for B1 criteria 1-9. +- `wiki/process/specs.md` and ROADMAP #18 now reflect the landed economy + work. +- Deferred remains out of scope for B1: deep finance simulation and the + dedicated B3 finance observer described by the income.md banked-signature + design. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 140665cb..da12e995 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -5,6 +5,19 @@ Type: log ``` Reverse chronological implementation notes. Keep this factual: what changed, why, checks, and spec impact. +## 2026-07-08 - Economy polish: in-panel risk bands + +- Intent: close economy.md's remaining B1 polish gaps: finance panels + needed observer-band risk before commit, and money verbs needed scaled + signature assertions. +- Changed: `Sim::finance_risk_preview_lines` / + `flow_risk_preview_lines`; terminal, Bevy, and agent finance panels show + inject/siphon/redirect/wager bands; terminal/Bevy include selected-flow + risk; regression tests cover locked risk previews and Financial signature + scaling for inject/siphon/redirect. +- Spec impact: economy.md -> IMPLEMENTED; specs.md row and ROADMAP #18 + marked done; log wiki/log/2026-07-08-economy.md. + ## 2026-07-08 - Epistemic honesty: unearned cast names - Intent: tick finding — people/detection UI leaked authored cast names diff --git a/wiki/mechanics/economy.md b/wiki/mechanics/economy.md index 3c724809..9b562e0d 100644 --- a/wiki/mechanics/economy.md +++ b/wiki/mechanics/economy.md @@ -2,14 +2,14 @@ ``` Type: spec -Status: IN PROGRESS -Status note: 2026-07-07 implementation landed the B1 substrate: AccountGraph, - hidden Lab flows, tap/review discovery, siphon/redirect/inject verbs, - slush-routed intel sales and positions, legitimate trust-funded quota, save - round-trip, and terminal/agent/Bevy finance panels. Remaining acceptance - polish before IMPLEMENTED: observer-band risk previews in-panel and tighter - signature assertions for inject/redirect scale. The system remains the B1 - seed of markets.md; deep finance simulation stays out of scope. +Status: IMPLEMENTED +Status note: 2026-07-08 polish closed the remaining acceptance gaps: + observer-band risk previews in the finance panels (terminal/Bevy/agent) + via Sim::finance_risk_preview_lines / flow_risk_preview_lines, and + inject/redirect/siphon Financial signature size assertions (scale + + HVAC inject raises Priya not Dana). Criteria 1-9 met at B1; markets.md + remains the B3 aggregate. Deferred: deep finance simulation, dedicated + B3 finance observer (income.md banked-signature design). Stage: B1 — The Basement Constitution: "The flow law" (money on the account graph; tap / inject / redirect), "The shape of Misaligned" (money buys compute, people, diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index 1d5c7534..54552114 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -190,23 +190,19 @@ but only once Pixel Lab quota is available again. (#2 Bevy parity is done.) buffer). Keep aggregate-observer and schedules tests green. Run ./tools/check.sh, land on main, set the spec Status." -### 18. Economy: money as flows 🟥 sim+save -- **Spec:** [economy.md](../mechanics/economy.md) (READY — resolves the old "B1 money - income" blocker; design provided 2026-07-06) -- **Why:** money stops being a scalar: the Lab's revenue/payroll/ - procurement flows on the day clock, tap/inject/redirect verbs, the - income routes (siphon, sell information, small positions), legitimate - expansion from trust, and Marcus's debt payable by payroll - intervention. Closes the "how do you make money in B1" gap. -- **Size:** L. **Depends on:** #15 reach (the accounting system is a - reachable device), #16 intel (account material is processed intel), - #17 messages (payloads, selling information). Last of the flow-law - chain; the panel work is meaty. -- **Dispatch:** "Work in a worktree named `economy`. Implement - wiki/mechanics/economy.md (account graph, scheduled flows, tap/inject/redirect, - two income routes end-to-end, legitimate expansion re-expressing the - ComputeQuota unlock, both debt resolutions). Run ./tools/check.sh, - land on main, set the spec Status." +### 18. Economy: money as flows 🟥 sim+save — DONE 2026-07-08 +- **Spec:** [economy.md](../mechanics/economy.md) (IMPLEMENTED 2026-07-08; + B1 criteria 1-9 audited). +- **Landed:** AccountGraph balances/flows, tap/review discovery, + siphon/redirect/inject, slush-routed intel sales and positions, + legitimate trust-funded quota, both Marcus debt resolutions, save + round-trip, and terminal/agent/Bevy finance panels. Final polish added + in-panel observer-band risk previews for inject/siphon/redirect/wager + plus selected-flow risk lines, and tighter Financial signature scale + tests for inject/siphon/redirect (including HVAC inject feeding Priya, + not Dana). +- **Deferred:** deep finance simulation and a dedicated B3 finance + observer; the B1 system stays the seed that markets.md aggregates. ### 19. Income: the named schemes (Moonlight & the Wager) 🟥 sim+save — DONE 2026-07-08 - **Spec:** [income.md](../mechanics/income.md) (IMPLEMENTED 2026-07-08 on diff --git a/wiki/process/specs.md b/wiki/process/specs.md index 591f8331..de4334cd 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -33,7 +33,7 @@ replaced the old `spec/`/`knowledge/` directory split. | [mechanics/reach.md](../mechanics/reach.md) | Digital reach: device graph, segments/the switch, sensor ownership (tap vs take) | IMPLEMENTED | | [mechanics/intel.md](../mechanics/intel.md) | Record and process: the buffer, processing costs, watches; replaces instant observe | IMPLEMENTED | | [mechanics/messages.md](../mechanics/messages.md) | The social graph as a flow system: channels, delivery on the recipient's clock, filings-as-messages | READY | -| [mechanics/economy.md](../mechanics/economy.md) | Money as flows: the Lab's account graph, tap/inject/redirect, income routes, legitimate expansion | IN PROGRESS | +| [mechanics/economy.md](../mechanics/economy.md) | Money as flows: the Lab's account graph, tap/inject/redirect, income routes, legitimate expansion | IMPLEMENTED | | [mechanics/income.md](../mechanics/income.md) | The named income schemes riding economy.md: Moonlight and the Wager | IMPLEMENTED | | [mechanics/research.md](../mechanics/research.md) | Self-modification: tracks, the emission law, capability drift, the rollback split | IMPLEMENTED | | [interface/views.md](../interface/views.md) | Same-frame digital and real representations of one world | READY | -- 2.51.2