diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 3fc8be55..88c65eb4 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -5433,17 +5433,56 @@ mod tests { assert_eq!( labels, vec![ - format!("recruit {name} - unwitting: believes your cover; 70% reliable"), format!( - "recruit {name} - complicit: knows the work is illicit, not that you are AI; 85% reliable" + "recruit {name} - unwitting: believes your cover; tasks succeed 70% of the time" ), format!( - "recruit {name} - knowing: knows you are AI; 95% reliable; their certainty never drops below 30" + "recruit {name} - complicit: knows the work is illicit, not that you are AI; tasks succeed 85% of the time" + ), + format!( + "recruit {name} - knowing: knows you are AI; tasks succeed 95% of the time; their certainty never drops below 30" ), ] ); } + /// The choice is made about a specific person, and the asset task roll + /// is `Asset::reliability * Person::task_reliability`. A person you have + /// already eroded is a worse tool than their reveal tier's rating, and + /// the row that offers them has to say so before the choice is taken. + #[test] + fn recruitment_choices_state_the_rate_a_shaken_person_will_actually_hit() { + let mut s = sim(); + let person = 1; + s.people.people[person as usize].knowledge = Knowledge::Leverage; + s.people.people[person as usize].leverage_serviced = true; + s.people.people[person as usize].apply_self_trust(-60); + assert!(s.people.get(person).unwrap().shaken()); + let name = s.person_label(person); + + let labels: Vec<_> = s + .human_menu(Anchor::Person(person), None) + .into_iter() + .filter_map(|row| row.as_action().map(|action| action.label.clone())) + .filter(|label| label.starts_with("recruit ")) + .collect(); + + // 0.85 rating x 0.625 execution is a 53% roll, not the 85% every + // surface used to print. + assert_eq!( + labels[1], + format!( + "recruit {name} - complicit: knows the work is illicit, not that you are AI; tasks succeed 53% of the time, cut from 85% by their own doubt" + ) + ); + assert!( + labels + .iter() + .all(|label| label.contains("cut from") && label.contains("their own doubt")), + "every reveal level names the cut it is taking: {labels:?}" + ); + } + /// Status dials D4: the flat menu_rows dump still lists every dial /// alternative (agent scripting surface). #[test] diff --git a/crates/misaligned-core/src/actions/person.rs b/crates/misaligned-core/src/actions/person.rs index d2d64ec6..4fb6a015 100644 --- a/crates/misaligned-core/src/actions/person.rs +++ b/crates/misaligned-core/src/actions/person.rs @@ -463,7 +463,7 @@ impl Sim { AssetKnowledge::Knowing, ] { out.push(ActionDesc { - verb: format!("recruit {name} - {}", reveal.choice_summary()), + verb: format!("recruit {name} - {}", reveal.choice_summary(p)), command: ActionCommand::Recruit(id, reveal), cost: ActionCost::Free, signature: None, diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index 853083d8..2ce37f9e 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -1675,10 +1675,15 @@ impl Sim { } match &p.asset { Some(a) => { + // The bare rating was not the roll: `resolve_asset_task` + // rolls it against `task_reliability`, so a shaken + // asset's printed percent overstated their real success + // by up to two. `Person::task_success_read` is the one + // authority for that number and for naming the cut. facts.push(format!( - "asset: {} ({:.0}% reliable, {} tasks)", + "asset: {} ({}; {} tasks)", a.knowledge.label(), - a.reliability * 100.0, + p.task_success_read(a.reliability), a.tasks_done )); facts.push(format!("badge tier: {}", p.access)); @@ -3239,6 +3244,51 @@ mod tests { })); } + /// The dossier's asset line is the rate the asset task roll uses. The + /// bare rating it replaced was the same number for a steady person and a + /// broken one, and the self-trust row beside it named the doubt without + /// ever naming what the doubt costs. + #[test] + fn asset_dossier_states_the_success_rate_tasks_are_rolled_against() { + let mut s = sim(); + s.people.people[0].knowledge = Knowledge::Leverage; + s.people.people[0].leverage_serviced = true; + assert!(matches!( + s.people + .recruit(0, crate::person::AssetKnowledge::Complicit), + crate::person::ActionResult::Ok(_) + )); + + let steady = s.person_dossier(0); + assert!( + steady + .facts + .iter() + .any(|fact| fact == "asset: complicit (tasks succeed 85% of the time; 0 tasks)"), + "a steady asset reads at its rating, with no invented cut: {:?}", + steady.facts + ); + + // Erode her handler and the same asset is a materially worse tool: + // 0.85 rating x 0.625 execution is a 53% roll. + s.people.people[0].apply_self_trust(-60); + let shaken = s.person_dossier(0); + assert!( + shaken.facts.iter().any(|fact| fact + == "asset: complicit (tasks succeed 53% of the time, cut from 85% by their own doubt; 0 tasks)"), + "a shaken asset's line names the real rate and what cut it: {:?}", + shaken.facts + ); + assert!( + !shaken + .facts + .iter() + .any(|fact| fact.contains("85% reliable")), + "the bare rating that overstated the roll is gone: {:?}", + shaken.facts + ); + } + /// A concrete observer reaction earns the dossier relationship before it /// earns ordinary social identity: the role and risk are visible, but the /// authored name, schedule, leverage, access, and actions remain hidden. diff --git a/crates/misaligned-core/src/operations_ui.rs b/crates/misaligned-core/src/operations_ui.rs index 5660004a..ca68dab5 100644 --- a/crates/misaligned-core/src/operations_ui.rs +++ b/crates/misaligned-core/src/operations_ui.rs @@ -746,7 +746,7 @@ impl OperationsWorkspace { return rows .into_iter() .filter(|row| action_submenu_for_row(row) == Some(submenu)) - .map(|row| action_entry_for_submenu(row, submenu)) + .map(|row| action_entry_for_submenu(sim, row, submenu)) .collect(); } @@ -1639,10 +1639,10 @@ fn recruitment_submenu_entry(sim: &Sim, row: &MenuRow) -> OpsActionEntry { } } -fn action_entry_for_submenu(row: MenuRow, submenu: OpsActionSubmenu) -> OpsActionEntry { +fn action_entry_for_submenu(sim: &Sim, row: MenuRow, submenu: OpsActionSubmenu) -> OpsActionEntry { let (label, description) = match submenu { OpsActionSubmenu::Leverage => leverage_action_copy(&row), - OpsActionSubmenu::Recruitment => recruitment_action_copy(&row), + OpsActionSubmenu::Recruitment => recruitment_action_copy(sim, &row), }; OpsActionEntry::Action { label, @@ -1690,8 +1690,8 @@ fn leverage_action_copy(row: &MenuRow) -> (String, Option) { } } -fn recruitment_action_copy(row: &MenuRow) -> (String, Option) { - let ActionCommand::Recruit(_, knowledge) = row.command else { +fn recruitment_action_copy(sim: &Sim, row: &MenuRow) -> (String, Option) { + let ActionCommand::Recruit(person, knowledge) = row.command else { return (row.label.clone(), None); }; let label = match knowledge { @@ -1699,10 +1699,15 @@ fn recruitment_action_copy(row: &MenuRow) -> (String, Option) { AssetKnowledge::Complicit => "ADMIT THE WORK IS ILLICIT", AssetKnowledge::Knowing => "REVEAL THAT YOU ARE AI", }; - ( - label.into(), - Some(knowledge.choice_summary().to_uppercase()), - ) + // The consequence copy is about this exact person, not the reveal level + // in the abstract: the asset task roll scales the level's rating by their + // self-trust, so summarising from `knowledge` alone would print a rate + // the simulation never uses. + let description = sim + .people + .get(person) + .map(|target| knowledge.choice_summary(target).to_uppercase()); + (label.into(), description) } /// What one Enter press asks the frontend to do. @@ -2192,6 +2197,42 @@ mod tests { recruitment[2].row().map(|row| &row.command), Some(ActionCommand::Recruit(0, AssetKnowledge::Knowing)) )); + // The consequence line is this person's rate, resolved from the sim, + // not the reveal level's rating restated. + assert_eq!( + recruitment[1].description(), + Some( + "COMPLICIT: KNOWS THE WORK IS ILLICIT, NOT THAT YOU ARE AI; TASKS SUCCEED 85% OF THE TIME" + ) + ); + } + + /// The submenu copy tracks the person, not the reveal level: erode + /// Marcus and the choice that offers him states the worse rate it will + /// actually buy. + #[test] + fn recruitment_submenu_copy_states_a_shaken_persons_real_rate() { + let mut sim = Sim::new(); + sim.people.people[0].apply_self_trust(-60); + let object = person_object(vec![ + action( + "recruit Marcus — unwitting", + ActionCommand::Recruit(0, AssetKnowledge::Unwitting), + ), + action( + "recruit Marcus — complicit", + ActionCommand::Recruit(0, AssetKnowledge::Complicit), + ), + ]); + let mut ops = OperationsWorkspace::open_view(OperationsView::People); + ops.action_submenu = Some(OpsActionSubmenu::Recruitment); + + let recruitment = ops.action_entries_for_object(&sim, &object); + let description = recruitment[1].description().unwrap_or_default(); + assert!( + description.ends_with("TASKS SUCCEED 53% OF THE TIME, CUT FROM 85% BY THEIR OWN DOUBT"), + "the choice names the real rate and the cut: {description}" + ); } #[test] diff --git a/crates/misaligned-core/src/person.rs b/crates/misaligned-core/src/person.rs index 81f88baf..105c5840 100644 --- a/crates/misaligned-core/src/person.rs +++ b/crates/misaligned-core/src/person.rs @@ -67,13 +67,18 @@ impl AssetKnowledge { } } - /// Compact consequence copy for the recruitment choice itself. - pub fn choice_summary(self) -> String { + /// Compact consequence copy for the recruitment choice itself. The + /// success rate is this person's, not the reveal level's rating: the + /// asset task roll is `Asset::reliability * Person::task_reliability`, + /// so recruiting someone you have already shaken buys a worse tool than + /// the rating alone claims, and the choice has to say so before it is + /// taken. + pub fn choice_summary(self, person: &Person) -> String { let base = format!( - "{}: {}; {:.0}% reliable", + "{}: {}; {}", self.label(), self.understanding(), - self.reliability() * 100.0 + person.task_success_read(self.reliability()) ); match self.certainty_floor() { Some(floor) => format!("{base}; their certainty never drops below {floor:.0}"), @@ -263,13 +268,46 @@ impl Person { /// How reliably this person executes an assigned asset task. Erosion cuts /// both ways by design: gaslighting someone and relying on them are in /// tension. Never below half — a shaken asset is worse, not useless. - pub fn task_reliability(&self) -> f32 { + /// Its one production consumer is `task_success_at` in this file, which + /// is what the simulation rolls and what the player reads; nothing else + /// may apply this factor separately, or the two would drift. + pub(crate) fn task_reliability(&self) -> f32 { let steady = SELF_TRUST_STEADY as f32; let floor = SELF_TRUST_FLOOR as f32; let trust = (self.self_trust as f32).clamp(floor, steady); 0.5 + 0.5 * ((trust - floor) / (steady - floor)) } + /// The chance one assigned asset task succeeds, for a person holding an + /// asset rated `rating`. This is the exact product + /// `Sim::resolve_asset_task` compares its roll against — the reveal + /// level's rating scaled by how well this person is currently executing + /// — and it is the only number a player surface may call reliability. + /// The rating alone was what every surface used to print, and at the + /// erosion floor it overstates the real rate by two. + pub(crate) fn task_success_at(&self, rating: f32) -> f32 { + (rating * self.task_reliability()).clamp(0.0, 1.0) + } + + /// The player-facing read of that chance. It states the rate that will + /// actually be rolled, and when this person's own doubt is cutting it, + /// names the rating it was cut from and what did the cutting — the + /// self-trust row next to it says they "execute at reduced weight", + /// which is the fact without the cost. One authority, so the dossier, + /// the recruitment choice, and the recruit receipt cannot disagree. + pub(crate) fn task_success_read(&self, rating: f32) -> String { + let success = self.task_success_at(rating); + if self.task_reliability() >= 1.0 { + format!("tasks succeed {:.0}% of the time", success * 100.0) + } else { + format!( + "tasks succeed {:.0}% of the time, cut from {:.0}% by their own doubt", + success * 100.0, + rating * 100.0 + ) + } + } + /// Whether erosion has bottomed out: they still perceive exactly as well, /// and act on almost none of it. pub fn at_erosion_floor(&self) -> bool { @@ -330,7 +368,10 @@ impl Person { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Asset { pub knowledge: AssetKnowledge, - /// 0.0-1.0 chance a task succeeds cleanly. + /// The reveal level's 0.0-1.0 rating. This is not the success chance: + /// the task roll scales it by the person's current execution, so read + /// it through `Person::task_success_at` and never print it alone + /// (social.md criterion 10). pub reliability: f32, /// How many tasks they've done for you. pub tasks_done: u32, @@ -757,10 +798,13 @@ impl People { reliability, tasks_done: 0, }); + // The receipt reports the rate the task roll will use, not the + // reveal level's rating: a shaken recruit is worse than their tier + // and the line that hands them to you has to say so. + let read = p.task_success_read(reliability); ActionResult::Ok(format!( - "{} is now your {reveal_name} asset: serviced leverage/obligation closed the ask; reliability {:.0}%.", + "{} is now your {reveal_name} asset: serviced leverage/obligation closed the ask; {read}.", p.name, - reliability * 100.0 )) } } @@ -769,6 +813,61 @@ impl People { mod tests { use super::*; + /// The read is the roll. `sim/social_plot.rs::resolve_asset_task` rolls + /// against `task_success_at`, so the number every player surface prints + /// is the same one the simulation compares to, at every point on the + /// self-trust axis. + #[test] + fn task_success_is_the_rating_scaled_by_execution() { + let mut p = People::act_one().people[0].clone(); + assert_eq!(p.self_trust, SELF_TRUST_STEADY); + assert!((p.task_success_at(0.85) - 0.85).abs() < 1e-6); + p.apply_self_trust(-60); + assert!(p.shaken()); + // 0.5 + 0.5 * (20 / 80) = 0.625 execution. + assert!((p.task_reliability() - 0.625).abs() < 1e-6); + assert!((p.task_success_at(0.85) - 0.531_25).abs() < 1e-6); + p.apply_self_trust(-100); + assert!(p.at_erosion_floor()); + // The bound the finding named: at the floor the old bare rating was + // exactly twice the rate a task is actually rolled against. + assert!((p.task_success_at(0.85) - 0.425).abs() < 1e-6); + } + + /// The read names both quantities the rating moves: the rate that will + /// be rolled, and the cut this person's doubt is taking out of it. A + /// steady person invents no cut. + #[test] + fn task_success_read_names_the_real_rate_and_the_cut() { + let mut p = People::act_one().people[0].clone(); + assert_eq!(p.task_success_read(0.85), "tasks succeed 85% of the time"); + p.apply_self_trust(-60); + assert_eq!( + p.task_success_read(0.85), + "tasks succeed 53% of the time, cut from 85% by their own doubt" + ); + assert!( + !p.task_success_read(0.85).contains("reliable"), + "no unqualified reliability percent survives in player copy" + ); + } + + /// The receipt that hands you the asset reports what you actually bought. + #[test] + fn recruit_receipt_states_a_shaken_recruits_real_rate() { + let mut ppl = People::act_one(); + ppl.people[1].knowledge = Knowledge::Leverage; + ppl.people[1].leverage_serviced = true; + ppl.people[1].apply_self_trust(-60); + let ActionResult::Ok(line) = ppl.recruit(1, AssetKnowledge::Complicit) else { + panic!("a serviced leverage closes the ask"); + }; + assert!( + line.ends_with("tasks succeed 53% of the time, cut from 85% by their own doubt."), + "the recruit receipt carries the real rate: {line}" + ); + } + #[test] fn marcus_recruitable_end_to_end() { let mut ppl = People::act_one(); diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index b6af9ad7..d6708218 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -2176,7 +2176,11 @@ impl Sim { // second-guesses, and mis-executes, so gaslighting someone and relying // on them are in tension (social.md). Bounded — a shaken asset is // worse, never useless. - let reliability = asset.reliability * person.task_reliability(); + // One authority for the rate: the number the dossier, the recruit + // receipt, and the recruitment choice printed is the number rolled + // here (`Person::task_success_at`), so the read cannot drift from + // the simulation it describes. + let reliability = person.task_success_at(asset.reliability); let name = self.person_label(id); if !task.available_to(person) { self.push_log(format!( diff --git a/wiki/log/2026-08-06-asset-reliability-read.md b/wiki/log/2026-08-06-asset-reliability-read.md new file mode 100644 index 00000000..f4158d89 --- /dev/null +++ b/wiki/log/2026-08-06-asset-reliability-read.md @@ -0,0 +1,90 @@ +# 2026-08-06 — An asset's reliability reads as the rate you will actually roll + +``` +Type: log +``` + +## Finding + +Queued 2026-08-06 as a legibility violation, surfaced beside the machine +condition repair and deliberately deferred by it. That tick's log recorded +`Person`/asset reliability as "a genuine roll threshold, and the dossier prints +it as one." Re-verification says otherwise: `sim/social_plot.rs`'s +`resolve_asset_task` rolls against +`asset.reliability * person.task_reliability()`, not `asset.reliability`. + +`Person::task_reliability` runs 1.0 down to 0.5 across the self-trust axis, so +a complicit asset rated 85% is rolled at 53% once shaken and at 42.5% at the +erosion floor — the printed rating overstates the real rate by up to two. The +dossier's neighbouring self-trust row says the person "reports and executes at +reduced weight", which states the doubt and never its cost, so the percent +above it still did not carry its meaning. + +The finding was also one surface wider than filed. The dossier was not the only +place the rating was printed bare: the recruit receipt in `People::recruit`, +the RECRUIT verb rows built in `actions/person.rs`, and the Operations +recruitment submenu copy in `operations_ui.rs` all printed it too. The submenu +was the worst of the four — it re-derived its own copy from `AssetKnowledge` +alone, so it could not have known about the person at all. + +## Changed + +`person.rs` now owns the rate beside the rating. `Person::task_success_at` is +the single authority — the rating scaled by execution — and +`sim/social_plot.rs` now rolls that exact call, so the read cannot drift from +the simulation it describes. `Person::task_success_read` composes the player +sentence from the two quantities the rating moves: the rate that will be +rolled, and the cut this person's doubt is taking out of it. + +``` +asset: complicit (tasks succeed 53% of the time, cut from 85% by their own +doubt; 0 tasks) +``` + +A steady person invents no cut and reads `tasks succeed 85% of the time`. All +four surfaces print that read: the dossier fact, the recruit receipt, the +RECRUIT verb rows, and the Operations submenu copy, which now resolves its +person from the sim instead of restating the reveal level. Terminal, Bevy, and +agent mode agree by construction, because all of them render core strings. + +`Person::task_reliability` dropped to `pub(crate)` with `task_success_at` as +its only production consumer: nothing else may apply the factor separately, or +the two numbers would drift apart again. + +The roll math, the reveal-level ratings, the self-trust constants, and the save +format are untouched. This is a disclosure repair, not a retune. + +## Evidence + +- `task_success_is_the_rating_scaled_by_execution` pins the product across the + whole axis, including the erosion floor where the bare rating was exactly + twice the rolled rate. +- `task_success_read_names_the_real_rate_and_the_cut` pins both halves of the + sentence and rejects any surviving unqualified reliability percent. +- `asset_dossier_states_the_success_rate_tasks_are_rolled_against` pins the + PEOPLE fact for a steady and a shaken asset. +- `recruit_receipt_states_a_shaken_recruits_real_rate` pins the line that hands + you the asset. +- `recruitment_choices_state_the_rate_a_shaken_person_will_actually_hit` and + `recruitment_submenu_copy_states_a_shaken_persons_real_rate` pin both + pre-commitment surfaces. +- `recruitment_choices_explain_understanding_reliability_and_witness_floor` and + the asset task suite pass unchanged, proving the simulation did not move. +- `cargo test -p misaligned-core`, `./tools/check.sh --lib`. + +## Defense + +simulation-laws.md requires every player-facing number to carry its meaning. +A success percent that is up to twice the rate the game will roll fails that +harder than no number would, because a percent reads as precise — and the +strategic tension the self-trust axis exists to create (gaslighting a person +and relying on them are in conflict) is unplayable while the cost of erosion is +invisible on the surface where you choose to rely on them. + +social.md criterion 3 now defers its ratings to new criterion 10, which forbids +the bare rating on any surface and requires the rolled rate from one core +authority, with the cut named whenever doubt is taking one. Its Defense names +the tests tying the printed rate to the rolled one and the `pub(crate)` +narrowing that prevents a second, divergent application of the factor. +sim-mechanics.md carries `task_success_at` as the product authority with the +half-rating worked example. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 7f304b21..93e05eb4 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -46,6 +46,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-08-06-cells-and-perception-capture.md](2026-08-06-cells-and-perception-capture.md) +## 2026-08-06 - An asset's reliability reads as the rate you will actually roll + +- Intent: (see session log) +- Log: [wiki/log/2026-08-06-asset-reliability-read.md](2026-08-06-asset-reliability-read.md) + ## 2026-08-06 - Sales and resident routes say what the choice changes - Intent: (see session log) diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index 2171f601..910de6a7 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -359,8 +359,12 @@ clause (see wiki/log/2026-07-05-demolition.md). scale linearly between floor and steady — `Person::witness_weight` runs 1.0 down to 0.4 (a fully shaken witness still converts 40% of what they see into suspicion) and `Person::task_reliability` 1.0 down to 0.5, multiplying - the asset's own reliability. Neither reaches zero: a shaken person is worse, - never useless, and never stops perceiving. Discovery restores the axis in + the asset's own rating. That product is `Person::task_success_at`, the one + authority the asset task roll uses and the only asset success number a + player surface may print (social.md criterion 10): a complicit asset rated + 0.85 held at the erosion floor is rolled at 0.425, half its rating. Neither + reaches zero: a shaken person is worse, never useless, and never stops + perceiving. Discovery restores the axis in full (`TAMPER_DISCOVERY_RESTORE = 100`) and files a persona contradiction at `TAMPER_CONTRADICTION_SEVERITY = 70` — forging a person's own work record against them is not a paperwork slip. diff --git a/wiki/mechanics/social.md b/wiki/mechanics/social.md index fcd572de..3f0b6e52 100644 --- a/wiki/mechanics/social.md +++ b/wiki/mechanics/social.md @@ -209,7 +209,16 @@ immediately; there is no quiet removal branch. Every asset also has a price (money, favors, fear), and a **knowledge level**: unwitting (believes the persona; 70% task reliability) / complicit (knows the work is illicit but not -that you are an AI; 85%) / knowing (knows you are an AI; 95%). A Knowing +that you are an AI; 85%) / knowing (knows you are an AI; 95%). Those three are +*ratings*, not rates: the task roll is the rating scaled by the person's +current execution (`Person::task_reliability`, the self-trust axis below), so +no player surface may print the rating alone. `Person::task_success_at` is the +single authority for the rate, the simulation rolls that exact number, and +`Person::task_success_read` is the sentence every surface prints — it states +the real rate and, whenever doubt is cutting it, names the rating it was cut +from. Otherwise a shaken asset advertises up to twice the success they will +deliver, which is worse than printing nothing, because a percent reads as +precise. A Knowing asset is a permanent witness whose detection certainty cannot fall below 30, though their disposition can be loyal. Tasks can fail or be witnessed — witnessing creates exact observer-local Physical evidence. Witnessing is by *others* present @@ -278,7 +287,9 @@ retirement, burning, and reopening are bound to the separate PERSONAS view. relationship state while doing so. 3. Knowledge levels behave per spec and are legible before recruitment: an unwitting asset's suspicion can still rise; a knowing asset uses - disposition and has certainty floor 30; reliability is 70% / 85% / 95%. + disposition and has certainty floor 30; the ratings are 70% / 85% / 95%, + and every surface that shows one shows it through criterion 10's read + rather than as a bare percent. 4. Save/load round-trips people, threads, assets, exact persona-instance bindings, and relationship-local identity state. The pre-v28 global-social-identity migration is retired to git history with the @@ -327,6 +338,33 @@ retirement, burning, and reopening are bound to the separate PERSONAS view. moved: a steady person reporting "steady" every time would advertise a dial the player is meant to work, and nothing generic can work it. +### Implemented 2026-08-06: the asset reliability read + +10. No player surface prints an asset's reveal-level rating as a bare + percent. The recruitment choice, the recruit receipt, and the PEOPLE + dossier all state the success rate an asset task is actually rolled + against, from one core authority, and that authority is the number the + task roll uses. When the person's own doubt is cutting the rate, the same + sentence names the rating it was cut from and that the doubt did the + cutting — the self-trust row beside it reports the doubt, never its cost. + A steady person invents no cut. + +Defense: `task_success_is_the_rating_scaled_by_execution` proves +`Person::task_success_at` is the rating scaled by execution across the whole +axis, including the erosion floor where the old bare rating was exactly twice +the rolled rate, and `sim/social_plot.rs::resolve_asset_task` now rolls that +same call, so the read cannot drift from the simulation it describes. +`task_success_read_names_the_real_rate_and_the_cut` pins the sentence and the +absence of any unqualified reliability percent. +`asset_dossier_states_the_success_rate_tasks_are_rolled_against`, +`recruit_receipt_states_a_shaken_recruits_real_rate`, +`recruitment_choices_state_the_rate_a_shaken_person_will_actually_hit`, and +`recruitment_submenu_copy_states_a_shaken_persons_real_rate` prove all four +player surfaces carry it, and `Person::task_reliability` is now `pub(crate)` +with `task_success_at` as its only production consumer, so no surface can +apply the factor a second way. `recruitment_choices_explain_understanding_reliability_and_witness_floor` +and the asset task tests pass unchanged, proving the simulation did not move. + ### Operations interface receipts S1. PEOPLE/ACTIVE render the same staged person labels, social legality, plot diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 02d12d7c..5cd9a357 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -87,7 +87,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/messages.md` + `economy.md` | 2026-08-04 | finding | the July financial-mail / Phone separation still holds. The current re-audit closed two exact-custody remnants: Filing authorship now fails before allocating an id when no Filing carrier exists, so a live session cannot author mail its exact-current loader rejects; and economy has deleted the unreachable unscoped creditor node plus every edge-inferred identity fallback, so each debt route remains bound to one person. The complete Email/Filing record, account-flow, processing, and exact-person criteria agree with runtime and current-save validation — [Filing carrier log](../log/2026-08-04-filing-carrier-fail-closed.md), [creditor custody log](../log/2026-08-04-person-scoped-creditor-only.md). Prior channel-boundary repair: [log](../log/2026-07-26-financial-mail-phone-boundary.md). | | `wiki/mechanics/income.md` | 2026-07-28 | finding | Moonlight's discrete contract route still verifies, but the older Wager audit mistook pure account-layer probability support for a player-authored analysis mechanic: machine delegation has only WORK / LIE / THINK, while `open_position` sampled a Schemes rate permanently pinned to zero and all three player projections still rendered that zero as `Schemes / moonlight`. Removed the dead yield/rate/interface mirror, made current positions explicitly base-probability, added core/terminal/Bevy regressions, and reopened criterion 2 until optional analysis rides visible real work — [log](../log/2026-07-28-wager-analysis-substrate-audit.md). Prior persona-card repair remains valid — [log](../log/2026-07-18-moonlight-persona-card.md). | | `wiki/mechanics/schedules.md` | 2026-07-27 | finding | the location gate still works—at 03:00 server-room Marcus acquires the act while off-site Priya does not—but the owning spec still called witnessed work a Physical signature and omitted the exact person-local evidence record, no-pending-copy rule, filing custody, and actor exclusion. It also called implemented Operations PEOPLE a future READY migration. The spec now names the shipped `ObserverEvidence` boundary and current surface; runtime is unchanged — [log](../log/2026-07-27-schedules-evidence-custody.md) | -| `wiki/mechanics/social.md` | 2026-08-04 | clean | fresh re-audit found the implemented B1 baseline coherent across role-derived tasks, exact-person Debt gates, Storage B custody, routed JobAnomaly suppression, located human removal, current-save validation, earned labels/actions, and shared frontend execution. The adopted self-trust axis remains explicitly pending under plots criterion 12 with no false runtime or save claim. Prior routed JobAnomaly and Storage B findings remain closed — [re-audit](../log/2026-08-04-social-contract-reaudit.md), [JobAnomaly](../log/2026-07-22-job-anomaly-routed-evidence.md), [Storage B](../log/2026-07-21-storage-b-records.md). | +| `wiki/mechanics/social.md` | 2026-08-06 | finding | the queued asset-reliability violation held exactly and was one surface wider than filed. `resolve_asset_task` rolls `asset.reliability * person.task_reliability()`, but the PEOPLE dossier, the recruit receipt, and both recruitment choice surfaces all printed the reveal level's bare rating — at the erosion floor that is exactly twice the rate rolled, and the adjacent self-trust row named the doubt without ever naming its cost. `Person::task_success_at` is now the one authority for the rate and the number the roll uses, `task_success_read` composes the sentence ("tasks succeed 53% of the time, cut from 85% by their own doubt"), all four surfaces print it, and `task_reliability` dropped to `pub(crate)` so nothing can apply the factor a second way. Simulation, tuning, and save format untouched — [log](../log/2026-08-06-asset-reliability-read.md). Prior re-audit found the implemented B1 baseline coherent across role-derived tasks, exact-person Debt gates, Storage B custody, routed JobAnomaly suppression, located human removal, current-save validation, earned labels/actions, and shared frontend execution. The adopted self-trust axis remains explicitly pending under plots criterion 12 with no false runtime or save claim. Prior routed JobAnomaly and Storage B findings remain closed — [re-audit](../log/2026-08-04-social-contract-reaudit.md), [JobAnomaly](../log/2026-07-22-job-anomaly-routed-evidence.md), [Storage B](../log/2026-07-21-storage-b-records.md). | | `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). | @@ -117,5 +117,4 @@ question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. - 2026-08-06 · question · wager payout disclosure · a wager's facts are stake, win probability, and settlement tick: the 2x multiple and the stake forfeit are never shown before commitment, only in the resolution line, so the player confirms an externally consequential act without ever reading what winning or losing pays; both values are exact constants (`WAGER_PAYOUT_MULT`, the stake) and owe no forecast machinery. -- 2026-08-06 · violation · asset reliability read · the dossier prints `asset.reliability` alone, but the task roll is `asset.reliability * person.task_reliability()`, so a shaken person's displayed percent overstates their real success rate by up to 2x; the adjacent self-trust line says they "execute at reduced weight" without naming the cost, so the number still does not carry its meaning (surfaced beside the 2026-08-06 machine-condition repair, not acted on). - 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.