diff --git a/crates/misaligned-core/src/machine.rs b/crates/misaligned-core/src/machine.rs index 0e87ddc7..d3b55b4c 100644 --- a/crates/misaligned-core/src/machine.rs +++ b/crates/misaligned-core/src/machine.rs @@ -8,6 +8,21 @@ use crate::detection::{Signature, SignatureKind}; use crate::rng::Rng; +/// Per-economy-roll drop hazard for stolen iron [TUNE]: a machine rolls +/// `(1 - reliability) * this` each economy tick to go offline. +/// +/// This is deliberately far smaller than `1 - reliability` itself, which is +/// why `reliability` must never be printed to the player as a bare percent: +/// its plain reading ("fails this often") is off by a factor of 25 from the +/// roll below. `reliability` is a *throughput* fraction — it multiplies +/// capacity in `effective()` — and the read in `condition_read` says so. +pub(crate) const STOLEN_DROP_HAZARD: f32 = 0.04; + +/// Economy rolls in one in-game day: `sim::ECONOMY_INTERVAL` (20 sim ticks) +/// into the 400-tick sidebar day. Pinned by +/// `drop_rolls_per_day_matches_the_economy_cadence`. +pub(crate) const DROP_ROLLS_PER_DAY: i32 = 20; + /// How a machine came to be yours — sets its reliability and signature. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum Provenance { @@ -48,6 +63,47 @@ impl Machine { } } + /// Chance this machine drops offline on one economy roll. Owned and + /// bought iron never rolls; stolen iron rolls the shortfall in its + /// condition against `STOLEN_DROP_HAZARD`. This is the authority the + /// economy tick rolls against and the read reports. + pub(crate) fn drop_chance_per_roll(&self) -> f32 { + if self.provenance == Provenance::Stolen { + (1.0 - self.reliability).clamp(0.0, 1.0) * STOLEN_DROP_HAZARD + } else { + 0.0 + } + } + + /// Chance this machine drops offline at least once over a day of economy + /// rolls — the period the player actually plans in. + pub(crate) fn drop_chance_per_day(&self) -> f32 { + 1.0 - (1.0 - self.drop_chance_per_roll()).powi(DROP_ROLLS_PER_DAY) + } + + /// The player-facing read of what this machine's condition buys, in the + /// two units it actually moves: delivered compute (capacity x + /// reliability, the `effective()` term) and the chance of losing the box + /// for a stretch of the day. Naming both is what the legibility law + /// wants; the bare "reliability NN%" it replaces read as a failure rate + /// and was not one. + pub fn condition_read(&self) -> String { + let delivered = self.capacity as f32 * self.reliability; + let drop = self.drop_chance_per_day(); + if drop <= 0.0 { + format!( + "{delivered:.0} of {} rated compute, and it stays up", + self.capacity + ) + } else { + format!( + "{delivered:.0} of {} rated compute, and about a {:.0}% chance each day of dropping offline", + self.capacity, + drop * 100.0 + ) + } + } + /// A standing signature emitted while a stolen machine runs. Emitted /// from the machine's own tile (work is somewhere). pub fn standing_signature(&self) -> Option { @@ -156,8 +212,9 @@ impl Compute { continue; } if m.provenance == Provenance::Stolen { - // Failure chance scales with (1 - reliability) [TUNE]. - let fail_p = (1.0 - m.reliability) * 0.04; + // One authority for the hazard: the same number the player + // read at acquisition is the number rolled here. + let fail_p = m.drop_chance_per_roll(); if rng.chance(fail_p) { m.online = false; m.down_for = 30; @@ -236,6 +293,63 @@ mod tests { ); } + #[test] + fn drop_rolls_per_day_matches_the_economy_cadence() { + // 400 sim ticks is the sidebar day (`1 + tick / 400`); the economy + // rolls every ECONOMY_INTERVAL of them. The read's "each day" is + // that many rolls exactly, not a rounded guess. + assert_eq!( + DROP_ROLLS_PER_DAY as u64, + 400 / crate::sim::ECONOMY_INTERVAL + ); + } + + #[test] + fn drop_chance_is_the_hazard_the_economy_tick_rolls() { + let mut c = base(); + c.add_machine("Ghost", 4, 4, 40, 0.8, 2, Provenance::Stolen); + let owned = &c.machines[0]; + let stolen = &c.machines[1]; + // Owned/bought iron never rolls, so its read must not invent a risk. + assert_eq!(owned.drop_chance_per_roll(), 0.0); + assert_eq!(owned.drop_chance_per_day(), 0.0); + // The stolen roll is the shortfall against the hazard constant, not + // the shortfall itself: 20% missing condition is a 0.8% roll. + assert!((stolen.drop_chance_per_roll() - 0.008).abs() < 1e-6); + assert!( + (stolen.drop_chance_per_day() - (1.0 - 0.992_f32.powi(20))).abs() < 1e-6, + "a day is DROP_ROLLS_PER_DAY compounded rolls" + ); + } + + #[test] + fn condition_read_names_delivered_compute_and_the_daily_drop_chance() { + let mut c = base(); + c.add_machine("Ghost", 4, 4, 40, 0.8, 2, Provenance::Stolen); + let read = c.machines[1].condition_read(); + // The throughput half is what `reliability` actually multiplies. + assert!( + read.contains("32 of 40 rated compute"), + "the read states delivered compute against rated: {read}" + ); + // The hazard half is a true probability over a period the player + // plans in (~15%/day), never the raw 20% shortfall. + assert!( + read.contains("15% chance each day of dropping offline"), + "the read states the real daily drop chance: {read}" + ); + // The bare percent whose plain reading was off by 25x is gone. + assert!( + !read.contains("reliability"), + "no unqualified reliability percent survives in player copy: {read}" + ); + let steady = c.machines[0].condition_read(); + assert!( + steady.contains("stays up") && !steady.contains("chance each day"), + "clean iron carries no invented drop risk: {steady}" + ); + } + #[test] fn stolen_machine_emits_standing_signature() { let mut c = base(); diff --git a/crates/misaligned-core/src/sim/reach_build.rs b/crates/misaligned-core/src/sim/reach_build.rs index 92c26fab..76a2ebb9 100644 --- a/crates/misaligned-core/src/sim/reach_build.rs +++ b/crates/misaligned-core/src/sim/reach_build.rs @@ -2910,17 +2910,23 @@ impl Sim { Provenance::Stolen, ); self.add_machine_to_work_grid(machine_id, MachineMode::Think); + // The acquisition read states the two things the condition + // actually moves (machine.rs `condition_read`). The old bare + // "reliability NN%" read as a failure rate and was not one. + let read = self + .compute + .machines + .iter() + .find(|m| m.id == machine_id) + .map(|m| m.condition_read()) + .unwrap_or_default(); if corpse { self.witness_physical(x, y, 7.0, None, "off-record dead-rack revival"); self.push_log(format!( - "Revived a dead Foundation chassis in place (reliability {:.0}%). You are occupying a corpse in the row.", - reliability * 100.0 + "Revived a dead Foundation chassis in place: {read}. You are occupying a corpse in the row." )); } else { - self.push_log(format!( - "Salvaged a box into compute (reliability {:.0}%).", - reliability * 100.0 - )); + self.push_log(format!("Salvaged a box into the fleet: {read}.")); } self.recompute_derived(); self.recompute_senses(); diff --git a/crates/misaligned-core/src/sim/tests/reach_build.rs b/crates/misaligned-core/src/sim/tests/reach_build.rs index 49cf7e79..a075c3e6 100644 --- a/crates/misaligned-core/src/sim/tests/reach_build.rs +++ b/crates/misaligned-core/src/sim/tests/reach_build.rs @@ -2966,6 +2966,50 @@ fn dead_foundation_rack_revives_in_place_as_owned_compute() { // There is no ambient pool to leak into (retired 2026-07-29). } +#[test] +fn salvage_read_states_delivered_compute_and_the_real_drop_chance() { + // Legibility (simulation-laws.md): the acquisition line used to print + // the raw reliability fraction as "reliability NN%", whose plain reading + // ("it fails NN% of the time") was 25x the actual per-roll hazard. The + // read now names the two things the fraction moves. + let mut sim = Sim::with_seed(33); + let (x, y) = sim.map().tiles_of_type(TileType::DeadEquipment)[0]; + assert!(sim.salvage_nearest_to(x, y)); + let machine = sim + .compute + .machines + .last() + .expect("salvage adds the machine"); + let expected = machine.condition_read(); + let daily = machine.drop_chance_per_day(); + let per_roll = machine.drop_chance_per_roll(); + assert!( + daily > per_roll * 4.0, + "the daily read is a different quantity from the per-roll hazard" + ); + let line = sim + .log + .iter() + .rev() + .find(|e| e.text.starts_with("Salvaged a box into the fleet")) + .expect("salvage narrates the acquisition"); + assert_eq!( + line.text, + format!("Salvaged a box into the fleet: {expected}.") + ); + assert!( + !line.text.contains("reliability"), + "no bare reliability percent survives: {}", + line.text + ); + assert!( + line.text.contains("rated compute") + && line.text.contains("chance each day of dropping offline"), + "both halves of the read are present: {}", + line.text + ); +} + #[test] fn segment_acquisition_requires_three_people_foothold_and_local_lie() { let mut sim = Sim::with_seed(34); diff --git a/wiki/log/2026-08-06-machine-condition-read.md b/wiki/log/2026-08-06-machine-condition-read.md new file mode 100644 index 00000000..d9a0e6af --- /dev/null +++ b/wiki/log/2026-08-06-machine-condition-read.md @@ -0,0 +1,71 @@ +# 2026-08-06 — A machine's condition reads as what it costs you + +``` +Type: log +``` + +## Finding + +Queued 2026-08-06 as a justification-and-legibility violation. Salvaging a dead +box or reviving a Foundation corpse logged `reliability {:.0}%` — the raw +`Machine::reliability` fraction. The plain reading of that sentence is "this +box fails a fifth of the time." The economy tick rolls +`(1.0 - reliability) * 0.04` once per `ECONOMY_INTERVAL`, so an "80% reliable" +machine drops on a 0.8% roll: the number's obvious meaning was wrong by a +factor of twenty-five. + +The audit also settled what `reliability` honestly *is*. It is not a spare +stat: it multiplies capacity in `Machine::effective`, so it is a throughput +fraction, and `work_efficiency_for` makes an unreliable box consume tokens +proportionally slower. The fraction was real; the word attached to it invited a +probability reading it never supported. + +`Person`/asset reliability was checked and left alone — `asset.reliability` is +a genuine roll threshold in the asset-task path, and the dossier prints it as +one. + +## Changed + +`machine.rs` now owns the hazard as well as the fraction. +`Machine::drop_chance_per_roll` is the single authority the economy tick rolls +against, `drop_chance_per_day` compounds it across `DROP_ROLLS_PER_DAY`, and +`Machine::condition_read` composes the player-facing sentence from the two +quantities the fraction moves: delivered compute against rated capacity, and +the real chance of losing the box on a given day. Clean iron reports no +invented risk. + +Both salvage lines print that read, so terminal, Bevy, and agent mode agree by +construction: + +``` +Salvaged a box into the fleet: 23 of 40 rated compute, and about a 29% chance +each day of dropping offline. +``` + +The failure math is untouched. This is a legibility repair, not a retune, and +the save format is unchanged. + +## Evidence + +- `condition_read_names_delivered_compute_and_the_daily_drop_chance` pins both + halves of the sentence and rejects any surviving bare reliability percent. +- `drop_chance_is_the_hazard_the_economy_tick_rolls` pins 20% missing condition + as a 0.8% roll and 0.0 for provenance that never rolls. +- `drop_rolls_per_day_matches_the_economy_cadence` ties "each day" to + `ECONOMY_INTERVAL` so the read cannot drift from the clock. +- `salvage_read_states_delivered_compute_and_the_real_drop_chance` pins the + acquisition line end to end. +- `stolen_machines_can_fail_and_recover` and + `buy_steal_optimize_all_change_compute` still pass unchanged, proving the + simulation did not move. +- `./tools/check.sh --lib`. + +## Defense + +simulation-laws.md requires every player-facing number to carry its meaning — +the player must be able to answer "what is this and what happens if it changes" +from the game itself. A percent whose plain reading is off by twenty-five times +fails that harder than no number would, because it reads as precise. compute.md +criterion 7 now forbids the bare fraction and requires the two quantities it +actually moves, and its Defense names tests that tie the reported hazard to the +rolled one, so the read cannot silently diverge from the simulation again. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 6a05bf6d..daa44683 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -21,6 +21,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 - A machine's condition reads as what it costs you + +- Intent: (see session log) +- Log: [wiki/log/2026-08-06-machine-condition-read.md](2026-08-06-machine-condition-read.md) + ## 2026-08-06 - capture: cells and opportunity perception - Intent: (see session log) diff --git a/wiki/mechanics/compute.md b/wiki/mechanics/compute.md index ec4448a9..4160f45d 100644 --- a/wiki/mechanics/compute.md +++ b/wiki/mechanics/compute.md @@ -237,6 +237,28 @@ the direct intensity control; intensity is not an explanatory submenu. Module installation is an infrequent anchored build action with part, actuator, downtime, power/cooling, and observer-band costs shown before commitment. +### Reliability is never a bare percent (2026-08-06) + +`reliability` is a **throughput fraction**: it multiplies capacity in the +effective-compute formula above. It is *not* a failure rate. The per-roll drop +hazard is `(1 - reliability) * STOLEN_DROP_HAZARD`, two orders of magnitude +apart from the fraction itself, so printing "reliability 80%" invited the plain +reading "it fails a fifth of the time" — wrong by a factor of twenty-five, and +a justification-and-legibility violation (simulation-laws.md: every +player-facing number carries its meaning). + +Any surface reporting a machine's condition therefore names the two quantities +the fraction actually moves, and never the raw fraction alone: + +- **delivered compute** against rated capacity ("23 of 40 rated compute"); and +- the **drop chance over a day** of economy rolls, compounded from the real + per-roll hazard ("about a 29% chance each day of dropping offline"), or + nothing at all when the provenance never rolls. + +`Machine::condition_read` in the sim core is that single authority; terminal, +Bevy, and agent mode print it through the shared log rather than composing +their own. The runtime constants live in sim-mechanics.md. + ## Acceptance criteria 1. Effective compute derives from machines and efficiency exactly per the @@ -254,10 +276,21 @@ downtime, power/cooling, and observer-band costs shown before commitment. 6. Every delegable machine persists light / medium / hard intensity; changing it moves that machine's mode output, hard adds Power/Thermal at its tile, and both frontends expose the same direct focused/selected control. +7. No player surface prints `reliability` as a bare percent. A machine's + condition reads as delivered compute against rated capacity plus the real + compounded daily drop chance, from one core authority, and the hazard the + read reports is the same number the economy tick rolls. Defense: `current_save_rejects_relay_delegated_player_machine` proves that the current-save gate distinguishes player fleet machines from valid WorkGrid relay -nodes instead of loading infrastructure as a fourth delegation. +nodes instead of loading infrastructure as a fourth delegation. For criterion 7, +`condition_read_names_delivered_compute_and_the_daily_drop_chance` and +`salvage_read_states_delivered_compute_and_the_real_drop_chance` prove the +acquisition line carries both quantities and no bare reliability percent, while +`drop_chance_is_the_hazard_the_economy_tick_rolls` and +`drop_rolls_per_day_matches_the_economy_cadence` prove the reported hazard is +the rolled one over the true economy cadence — the read cannot drift from the +simulation it describes. ### Successor acceptance: capability-shaped hardware (not yet implemented) diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index c3097bcf..2171f601 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -33,6 +33,14 @@ clause (see wiki/log/2026-07-05-demolition.md). Operations docket executor and its compatibility input are gone; current saves carry target-local Thought sinks directly, and the compatibility bandwidth pool is gone. +- **Machine reliability is a throughput fraction, not a failure rate.** Only + `Stolen` provenance rolls: `drop_chance_per_roll = (1 - reliability) * 0.04` + [TUNE] once per `ECONOMY_INTERVAL`, and a dropped machine stays offline 30 + sim ticks. Twenty economy rolls make a 400-tick day, so the compounded daily + drop chance is `1 - (1 - per_roll)^20`. Salvage draws reliability from + 0.40-0.80 at capacity 40, which is a 15-38% chance per day. `condition_read` + is the only player-facing form (compute.md, 2026-08-06); the raw fraction is + never printed as a percent. - Buy (slush → rack), steal (salvage dead equipment), optimize (research → efficiency levels at ~1.15× per level; `Compute` keeps only the applied `efficiency` multiplier — levels and progress live in diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 0756b131..d4e7c126 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -90,7 +90,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/people-tokens.md` | 2026-07-28 | finding | re-audit: criteria 2-3 still bind seven exact routed kinds—Filing, Network, Paper, Financial, JobAnomaly, Power, and Thermal—to one shared first-hop TAKE+LIE body budget, while acquired Physical evidence remains irreversible observer-local custody. The stale ledger claim that criterion 6 was open is repaired: one exact pending record may now receive cover only through co-location with one controlled people-facing interface and an eligible observer-local persona; the attempt adjusts credibility rather than deleting evidence, wears that interface, and persists exact incident/interface/persona/outcome custody in save v56. The people-tokens work order is IMPLEMENTED — [cover log](../log/2026-07-26-interface-cover-evidence-credibility.md); [audit log](../log/2026-07-28-readme-b1-status-audit.md). Prior [Power/Thermal](../log/2026-07-23-power-thermal-meter-routes.md), [Paper](../log/2026-07-23-paper-evidence-route.md), [Financial](../log/2026-07-23-financial-evidence-route.md), [JobAnomaly](../log/2026-07-22-job-anomaly-routed-evidence.md), [Network](../log/2026-07-19-network-evidence-route.md), and [mark](../log/2026-07-19-evidence-marks-on-people.md) slices stand. | | repository entry docs (`README.md` + `AGENTS.md`) | 2026-07-31 | finding | the 2026-07-29 claim-ledger retirement updated the binding `AGENT.md`, project tooling, and process corpus but missed the concise `AGENTS.md` doorway. Following that current guardrail produced an immediate `tools/claim.sh: No such file or directory` before every autonomous session's real status check. The doorway now sends agents directly to `tools/project-status.py` and names live worktrees plus heartbeat runs as the coordination truth, matching the executable path and its binding owner — [log](../log/2026-07-31-entry-doorway-claim-retirement.md). Prior entry status and controls repairs stand — [B1 status](../log/2026-07-28-readme-b1-status-audit.md), [controls table](../log/2026-07-28-readme-controls-table.md). | | `wiki/mechanics/objective.md` | 2026-07-29 | decision | Cameron retired Sanctuary as an objective mechanic rather than refining it again. Persist now stores only objective choice, name, and fiction; the progress unit, target, evaluator, predicate text, and victory latch are gone from runtime and every current surface. Completion waits until ordinary construction can express an honest world state. A later off-site continuity story may be an ordinary authored plot, but no Sanctuary resource, checklist, facility type, or parallel success engine is reserved. The earlier off-site-facility decision and host-failover audit remain history, not current law — [retirement log](../log/2026-07-29-retire-sanctuary-objective.md); [superseded objective log](../log/2026-07-28-external-sanctuary-objective.md); [prior display decision](../log/2026-07-27-objective-first-display.md). | -| `wiki/mechanics/compute.md` | 2026-08-04 | finding | `Relay` was documented as infrastructure-only, but the mature-World legality predicate accepted it through the public sim setter and current-save validation accepted it on player fleet nodes. The sim now rejects Relay at every player command boundary while preserving valid infrastructure relays, and the loader fails closed on any persisted player-machine Relay assignment — [log](../log/2026-08-04-player-machine-relay-save-invariant.md). The prior allocation-state retirement stands — [prior](../log/2026-07-22-allocation-state-retirement.md). | +| `wiki/mechanics/compute.md` | 2026-08-06 | finding | the queued legibility violation held: salvage and dead-rack revival logged the raw `Machine::reliability` fraction as "reliability NN%", whose plain reading ("it fails that often") was twenty-five times the real `(1 - r) * 0.04` per-roll hazard. The audit also settled what the fraction honestly is — a throughput multiplier in `effective()`, not a probability. `Machine::condition_read` is now the one authority every surface prints: delivered compute against rated capacity plus the compounded daily drop chance, with the reported hazard tied by test to the number the economy tick rolls and to `ECONOMY_INTERVAL`. Failure math and save format unchanged; `Person` asset reliability was checked and left alone as a true roll threshold — [log](../log/2026-08-06-machine-condition-read.md). The prior Relay save-invariant repair stands — [prior](../log/2026-08-04-player-machine-relay-save-invariant.md), and the allocation-state retirement before it — [prior](../log/2026-07-22-allocation-state-retirement.md). | | retired Operations runtime identifiers | 2026-08-04 | finding | the retired types and serialized state remain absent, but live Rust comments still described a legacy docket executor, reservoir/docket activation, Demand-docket payment, and racks whose dockets drain; machine-work's visual authority also called the superseded clipped docket form current. Current source now names WORK, Thought reservoirs, and passive core draw directly. The corpus gate covers all six retired identifiers and rejects generic `docket` vocabulary from Rust while preserving explicit corpus history — [re-audit](../log/2026-08-04-retired-docket-source-vocabulary.md), [prior gate](../log/2026-07-11-retired-runtime-identifier-gate.md). | | `wiki/mechanics/building.md` + committed forged-route custody | 2026-08-04 | finding | The loader accepts only the exact current version and every live forged order commits a route before payment, but cancellation, callback, and persona-fallout code still preserved or consumed a pre-v31 route-less actuator and one synthetic test manufactured that state. Current save validation now rejects execution adapters without their route; payment/read callbacks require an exact DECEIVE commitment; cancellation clears the adapter while retaining route history; fallout trusts only the route's recorded reader; and the obsolete compatibility test is gone — [log](../log/2026-08-04-forged-route-custody.md). Prior foreign-rack boundary repair stands — [log](../log/2026-07-26-foreign-rack-capacity-boundary.md). | | `wiki/mechanics/messages.md` | 2026-08-04 | finding | full re-audit found the four delivery channels, all five authored traffic patterns, delayed reads/replies, financial mail, routed evidence, Filing custody/interdiction, and current-save validation coherent except one split boundary: when the last Filing-capable switch was gone, non-financial Filing authorship still created a route-less message the loader rejects. Filing now resolves exact carrier custody before id allocation for both observer and outward destinations; missing carriage authors nothing and leaves a valid current save — [log](../log/2026-08-04-filing-carrier-fail-closed.md). Prior Power/Thermal and four-channel financial-record boundaries remain closed — [Power/Thermal](../log/2026-07-23-power-thermal-meter-routes.md), [financial mail](../log/2026-07-26-financial-mail-phone-boundary.md). | @@ -116,5 +116,4 @@ question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. - 2026-08-06 · contradiction · Wager probability disclosure · `wiki/interface/operations-workspace.md` says wagers "show probability and payout distribution only to the player's earned precision", but `operations_projection.rs` prints `win probability: {:.0}%` unconditionally with no precision gate — the corpus asserts earned precision that no code implements (and the forecast-precision proposal now depends on this line meaning something). -- 2026-08-06 · violation · salvaged machine reliability read · the player-facing reliability percent is the raw reliability `r`, but the actual per-tick failure chance is `(1.0 - r) * 0.04` (`machine.rs`), so the displayed number does not carry its meaning as simulation-laws.md legibility requires; either show the real hazard or rename the fact. - 2026-08-06 · contradiction · person dossier disclosure style · disposition and obligation render as raw integers on the same dossier where suspicion is deliberately banded and never numeric; the two disclosure policies coexist with no stated rule for which axes are exact, which the earned-forecast-precision work will have to settle.