From 834394ed0ae6e4371135f544100b9c14c801f2cc Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 6 Aug 2026 00:56:45 -0700 Subject: [PATCH] Make a plot declare every carrier it rides The entry block now carries `requires_channels`, an array naming exactly the message channels the beats use, replacing the single optional `requires_channel` whose value nothing ever read. Validation refuses a beat riding an undeclared channel and refuses a declared channel no beat rides, naming the channel either way; eligibility then checks each declared channel against a carrier that can really author it. `review-survived` and `ray-morning-digest` file paper and now say so. The evidence-cover guard gets its own sentence back. Since the 2026-08-03 knowledge-use unification it returned the plot rows' fronting copy, which names a binding control the cover act does not carry. Defense: the harm was that a plot could be offered, priced, and paid for on an entry gate that never looked at half the carriers its story needed. A Filing act with no institutional carrier refuses authorship, so the run died at the beat which files -- after the Thought and the money were gone, on a story that was never startable. Declaration alone would have made the receipt honest and left the failure exactly where it was, so the fix is both halves: the entry says what it rides, and because it says so the gate can check it. The two directions of the validation rule are one rule -- an undeclared channel hides a cost, a declared one no beat rides invents a cost -- and both now name the offending channel so a contributor reads the fix in the failure. Carrier availability is asked per channel rather than as one boolean because that is how the runtime asks: only Filing needs an exact custody carrier, so gating Email on a device would refuse stories that work. On the cover copy: `select or create a persona to front this` describes a persona binding control, and a cover has none -- there is no story being fronted, only one lie told under an identity permitted to lie. The real blocker is that no owned identity holds Deceive authority, so the sentence says that and the plot rows keep their unified copy untouched. --- assets/plots/README.md | 11 +- .../plots/generic/looming-bill-absorbed.toml | 2 +- assets/plots/generic/maintenance-window.toml | 2 +- assets/plots/generic/negative-result.toml | 2 +- assets/plots/generic/recovery-window.toml | 2 +- assets/plots/generic/review-survived.toml | 2 +- assets/plots/generic/second-shift.toml | 2 +- assets/plots/marcus/marcus-debt-settled.toml | 2 +- assets/plots/priya/priya-budget-hero.toml | 2 +- assets/plots/ray/ray-morning-digest.toml | 2 +- assets/plots/ray/ray-paperwork-ghost.toml | 2 +- .../plots/voss/voss-missing-replication.toml | 2 +- crates/misaligned-core/src/plot.rs | 151 +++++++++++++++++- .../misaligned-core/src/sim/communications.rs | 13 ++ crates/misaligned-core/src/sim/social_plot.rs | 18 ++- .../src/sim/tests/social_plot.rs | 111 +++++++++++++ tools/public-api-allowlist.txt | 1 + wiki/mechanics/people-tokens.md | 9 ++ wiki/mechanics/plots.md | 28 +++- wiki/process/tick-ledger.md | 2 - 20 files changed, 344 insertions(+), 22 deletions(-) diff --git a/assets/plots/README.md b/assets/plots/README.md index 6e1ba9de..b5391826 100644 --- a/assets/plots/README.md +++ b/assets/plots/README.md @@ -44,12 +44,19 @@ Top level: `security-observer`, `facilities-manager`, and `handler-supervisor`. Leverages use the exact Rust enum spelling: `Debt`, `Overwork`, `Boredom`, `Ambition`, and `Publication`. -- `[entry]` contains `requires_knowledge = "Leverage"`, optional - `requires_channel` (a real `MessageChannel`), +- `[entry]` contains `requires_knowledge = "Leverage"`, + `requires_channels` (an array of real `MessageChannel` values, empty or + omitted when no beat sends a message), positive `thought_cost` (the compute-cost basis converted into the visible Thought reservoir threshold), zero or more `{ account, amount }` minimum balances, and a `failure_ending` id. Resources gate entry; they are not escrow and may disappear before a later act. +- `requires_channels` must name exactly the channels the beats ride — every + `message` act's `channel` and nothing more. A beat riding an undeclared + channel fails validation, and so does a declared channel no beat uses. The + entry gate then checks each declared channel against a real carrier, so a + story that files paper is refused up front instead of dying at the beat + that files. - `beats` are ordered. Each has a unique `id`, `narration`, and at least one `acts` entry or a `choice`. Optional `causal = true` marks the Marcus-bar beat; at least one causal beat with world acts is required, and every success diff --git a/assets/plots/generic/looming-bill-absorbed.toml b/assets/plots/generic/looming-bill-absorbed.toml index 63c67172..902b420a 100644 --- a/assets/plots/generic/looming-bill-absorbed.toml +++ b/assets/plots/generic/looming-bill-absorbed.toml @@ -17,7 +17,7 @@ leverages = ["Debt"] [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] thought_cost = 5.0 minimum_balances = [{ account = "slush", amount = 275 }] failure_ending = "flagged" diff --git a/assets/plots/generic/maintenance-window.toml b/assets/plots/generic/maintenance-window.toml index 97606ed5..6ec71e39 100644 --- a/assets/plots/generic/maintenance-window.toml +++ b/assets/plots/generic/maintenance-window.toml @@ -13,7 +13,7 @@ requires_switch_admin = true [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] operations_demand = 6.0 failure_ending = "change-review" diff --git a/assets/plots/generic/negative-result.toml b/assets/plots/generic/negative-result.toml index 7d0b084e..896b4061 100644 --- a/assets/plots/generic/negative-result.toml +++ b/assets/plots/generic/negative-result.toml @@ -12,7 +12,7 @@ leverages = ["Publication"] [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] operations_demand = 8.0 minimum_balances = [{ account = "slush", amount = 150 }] failure_ending = "method-request" diff --git a/assets/plots/generic/recovery-window.toml b/assets/plots/generic/recovery-window.toml index 44b29d03..3a4d22f4 100644 --- a/assets/plots/generic/recovery-window.toml +++ b/assets/plots/generic/recovery-window.toml @@ -13,7 +13,7 @@ minimum_access = 2 [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] operations_demand = 6.0 minimum_balances = [{ account = "utility", amount = 250 }] failure_ending = "duplicate-credit" diff --git a/assets/plots/generic/review-survived.toml b/assets/plots/generic/review-survived.toml index a418c6b0..65441b7e 100644 --- a/assets/plots/generic/review-survived.toml +++ b/assets/plots/generic/review-survived.toml @@ -12,7 +12,7 @@ leverages = ["Overwork"] [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email", "Filing"] thought_cost = 7.0 failure_ending = "flagged" diff --git a/assets/plots/generic/second-shift.toml b/assets/plots/generic/second-shift.toml index d04bcc1e..172d6177 100644 --- a/assets/plots/generic/second-shift.toml +++ b/assets/plots/generic/second-shift.toml @@ -12,7 +12,7 @@ leverages = ["Overwork"] [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] thought_cost = 7.0 minimum_balances = [{ account = "slush", amount = 150 }] failure_ending = "paper-trail" diff --git a/assets/plots/marcus/marcus-debt-settled.toml b/assets/plots/marcus/marcus-debt-settled.toml index 00a28ff6..17e37130 100644 --- a/assets/plots/marcus/marcus-debt-settled.toml +++ b/assets/plots/marcus/marcus-debt-settled.toml @@ -12,7 +12,7 @@ leverages = ["Debt"] [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] thought_cost = 5.0 minimum_balances = [{ account = "slush", amount = 400 }] failure_ending = "spooked" diff --git a/assets/plots/priya/priya-budget-hero.toml b/assets/plots/priya/priya-budget-hero.toml index 1ec78f45..7ae236e9 100644 --- a/assets/plots/priya/priya-budget-hero.toml +++ b/assets/plots/priya/priya-budget-hero.toml @@ -13,7 +13,7 @@ minimum_access = 2 [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] thought_cost = 6.0 minimum_balances = [{ account = "slush", amount = 300 }] failure_ending = "audit" diff --git a/assets/plots/ray/ray-morning-digest.toml b/assets/plots/ray/ray-morning-digest.toml index e1ceb0d9..076eb970 100644 --- a/assets/plots/ray/ray-morning-digest.toml +++ b/assets/plots/ray/ray-morning-digest.toml @@ -12,7 +12,7 @@ leverages = ["Boredom"] [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email", "Filing"] operations_demand = 8.0 failure_ending = "change-control" diff --git a/assets/plots/ray/ray-paperwork-ghost.toml b/assets/plots/ray/ray-paperwork-ghost.toml index b4af8139..e6c0f604 100644 --- a/assets/plots/ray/ray-paperwork-ghost.toml +++ b/assets/plots/ray/ray-paperwork-ghost.toml @@ -12,7 +12,7 @@ leverages = ["Boredom"] [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] thought_cost = 5.0 minimum_balances = [{ account = "slush", amount = 100 }] failure_ending = "shift-audit" diff --git a/assets/plots/voss/voss-missing-replication.toml b/assets/plots/voss/voss-missing-replication.toml index 0a9934c8..8806bd95 100644 --- a/assets/plots/voss/voss-missing-replication.toml +++ b/assets/plots/voss/voss-missing-replication.toml @@ -12,7 +12,7 @@ leverages = ["Publication"] [entry] requires_knowledge = "Leverage" -requires_channel = "Email" +requires_channels = ["Email"] thought_cost = 9.0 minimum_balances = [{ account = "slush", amount = 250 }] failure_ending = "source-check" diff --git a/crates/misaligned-core/src/plot.rs b/crates/misaligned-core/src/plot.rs index 8904f1b7..80be99a4 100644 --- a/crates/misaligned-core/src/plot.rs +++ b/crates/misaligned-core/src/plot.rs @@ -333,10 +333,48 @@ impl PlotDefinition { self.id )); } + self.validate_channels()?; self.validate_erosion()?; Ok(()) } + /// The entry gate names every carrier the story rides (plots.md "Costs are + /// honest"). An undeclared channel is the mid-story failure this check + /// exists to prevent: a Filing act whose carrier is missing refuses + /// authorship, and the run dies after the player already spent Thought and + /// money on an entry gate that never looked at that channel. A declared + /// channel no beat rides is the same dishonesty pointed the other way — it + /// gates entry on a carrier the story never needs. + fn validate_channels(&self) -> Result<(), String> { + let mut ridden: Vec = Vec::new(); + for act in self.beats.iter().flat_map(|beat| &beat.acts) { + if let WorldAct::Message { channel, .. } = act + && !ridden.contains(channel) + { + ridden.push(*channel); + } + } + for channel in &ridden { + if !self.entry.requires_channels.contains(channel) { + return Err(format!( + "plot {} rides the {} channel its entry does not declare (add it to requires_channels)", + self.id, + channel.label() + )); + } + } + for channel in &self.entry.requires_channels { + if !ridden.contains(channel) { + return Err(format!( + "plot {} declares the {} channel no beat rides", + self.id, + channel.label() + )); + } + } + Ok(()) + } + /// Whether this definition manufactures a memory-versus-artifact gap. pub fn erodes_self_trust(&self) -> bool { self.category == EROSION_CATEGORY @@ -443,9 +481,18 @@ impl PlotDefinition { if context.knowledge != self.entry.requires_knowledge { return Some("required leverage knowledge has not been earned".into()); } - if self.entry.requires_channel.is_some() && !context.has_channel { + if !self.entry.requires_channels.is_empty() && !context.has_channel { return Some("required message channel has not been earned".into()); } + // Earning the channel is not the same as having something to carry it. + // Validation guarantees this list names every channel the beats ride, + // so checking each declared channel here is exactly the mid-story + // carrier failure, moved in front of the player's commitment. + for channel in &self.entry.requires_channels { + if !context.available_channels.contains(channel) { + return Some(format!("no device carries the {} channel", channel.label())); + } + } for balance in &self.entry.minimum_balances { let available = context.balances.get(&balance.account).copied().unwrap_or(0); if available < balance.amount { @@ -541,8 +588,12 @@ impl TargetSelector { #[serde(deny_unknown_fields)] pub struct EntryRequirements { pub requires_knowledge: Knowledge, + /// Every message channel this plot's beats ride. The entry block is the + /// only place a plot states which carriers its story needs, so validation + /// holds this list to exactly the channels the beats use: a story that + /// files paper must say so before the player commits Thought to it. #[serde(default)] - pub requires_channel: Option, + pub requires_channels: Vec, #[serde(alias = "operations_demand")] pub thought_cost: f32, #[serde(default)] @@ -558,6 +609,16 @@ impl EntryRequirements { if !self.thought_cost.is_finite() || self.thought_cost <= 0.0 { return Err(format!("plot {plot} has invalid Thought threshold")); } + let mut channels = Vec::new(); + for channel in &self.requires_channels { + if channels.contains(channel) { + return Err(format!( + "plot {plot} repeats the {} channel requirement", + channel.label() + )); + } + channels.push(*channel); + } let mut accounts = BTreeSet::new(); for balance in &self.minimum_balances { if balance.amount <= 0 { @@ -956,6 +1017,11 @@ pub struct EligibilityContext { pub knowledge: Knowledge, pub leverage_serviced: bool, pub has_channel: bool, + /// Channels a real carrier can author right now. Distinct from + /// `has_channel`, which is the earned right to send at all: Filing needs + /// an institutional carrier standing behind it, and without one the + /// message act would refuse authorship mid-run. + pub available_channels: Vec, pub balances: BTreeMap, } @@ -1384,6 +1450,7 @@ mod tests { knowledge: Knowledge::Leverage, leverage_serviced: false, has_channel: true, + available_channels: vec![MessageChannel::Email], balances: BTreeMap::from([ (AccountSelector::Slush, 399), (AccountSelector::TargetCreditor, 0), @@ -1396,6 +1463,86 @@ mod tests { assert_eq!(run.target, 42, "the run binds the selected human id"); } + /// plots.md "Costs are honest": the entry block is the only place a plot + /// says which carriers its story needs, so a beat riding a channel the + /// entry never declared fails the catalog with that channel named. The + /// alternative is a run that dies at the beat which files, after the + /// player has already committed Thought and money to it. + #[test] + fn rejects_a_beat_riding_a_channel_the_entry_does_not_declare() { + let mut plot = sample_plot(); + plot.beats[0].acts.push(WorldAct::Message { + channel: MessageChannel::Filing, + from: EndpointSelector::Player, + to: EndpointSelector::External("the change desk".into()), + summary: "Corrected record filed".into(), + delivery_delay: 1, + }); + let err = plot.validate().unwrap_err(); + assert!(err.contains("rides the filing channel"), "{err}"); + assert!(err.contains("requires_channels"), "{err}"); + + plot.entry.requires_channels = vec![MessageChannel::Filing]; + assert_eq!(plot.validate(), Ok(()), "declaring what it rides is enough"); + + // The same dishonesty pointed the other way: gating entry on a carrier + // the story never needs. + plot.entry.requires_channels = vec![MessageChannel::Filing, MessageChannel::Phone]; + let err = plot.validate().unwrap_err(); + assert!( + err.contains("declares the phone channel no beat rides"), + "{err}" + ); + + plot.entry.requires_channels = vec![MessageChannel::Filing, MessageChannel::Filing]; + let err = plot.validate().unwrap_err(); + assert!(err.contains("repeats the filing channel"), "{err}"); + } + + /// Every shipped plot states exactly the channels its beats ride. This is + /// the corpus-wide form of the same rule: no authored story can reach a + /// carrier its entry receipt never named. + #[test] + fn every_authored_plot_declares_the_channels_its_beats_ride() { + let catalog = PlotCatalog::load_builtin().unwrap(); + for plot in catalog.plots() { + let mut ridden: Vec = Vec::new(); + for act in plot.beats.iter().flat_map(|beat| &beat.acts) { + if let WorldAct::Message { channel, .. } = act + && !ridden.contains(channel) + { + ridden.push(*channel); + } + } + for channel in &ridden { + assert!( + plot.entry.requires_channels.contains(channel), + "{} rides {} undeclared", + plot.id, + channel.label() + ); + } + assert_eq!( + plot.entry.requires_channels.len(), + ridden.len(), + "{} declares a channel no beat rides", + plot.id + ); + } + // The two stories that file paper say so. + for id in ["review-survived", "ray-morning-digest"] { + assert!( + catalog + .get(id) + .expect("authored plot") + .entry + .requires_channels + .contains(&MessageChannel::Filing), + "{id} rides Filing and declares it" + ); + } + } + #[test] fn validator_rejects_invalid_acts_choices_and_resources() { let mut catalog = PlotCatalog::load_builtin().unwrap(); diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index 41a13295..fc88e13f 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -408,6 +408,19 @@ impl Sim { ); } + /// Whether an ordinary authored act could find the carrier + /// [`Self::append_message`] demands on this channel. Only Filing needs an + /// exact custody carrier — every other channel authors without one. Plot + /// entry eligibility reads this so a story that rides Filing is refused + /// before the player commits, instead of failing at the beat that files. + pub(super) fn channel_carrier_available(&self, channel: MessageChannel) -> bool { + channel != MessageChannel::Filing + || self + .reach + .device_named("switch") + .is_some_and(|device| device.carries_message_channel(MessageChannel::Filing)) + } + /// Author one message. Soft-fails closed for `FinancialRecord` drafts when /// no ReachNet device carries both accounting records and the draft /// channel — current-save validation rejects carrier-less financial mail, diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index ca1b1b2e..b6af9ad7 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -342,10 +342,11 @@ impl Sim { return Some("the observer will not encounter that interface before filing".into()); } let Some(persona_id) = self.default_binding(observer, PersonaActionKind::Deceive) else { - // Unified missing-persona copy — the persona-control contract - // (2026-08-02): one string, rendered by frontends as the binding - // control rather than a prose dead end. - return Some("select or create a persona to front this".into()); + // Not the plot rows' fronting copy: a cover is not a story you put + // a face on, it is one lie told under an identity permitted to + // lie. The blocker is the missing Deceive authority, so the + // sentence names that and nothing else. + return Some("you own no identity that can authorize deceive".into()); }; self.persona_action_blocked_reason_for(persona_id, PersonaActionKind::Deceive) .or_else(|| self.persona_counterparty_blocked_reason(persona_id, observer)) @@ -849,6 +850,15 @@ impl Sim { knowledge: person.knowledge, leverage_serviced: person.leverage_serviced, has_channel: self.people.has_channel || self.egress().is_some(), + available_channels: [ + MessageChannel::Email, + MessageChannel::Phone, + MessageChannel::InPerson, + MessageChannel::Filing, + ] + .into_iter() + .filter(|channel| self.channel_carrier_available(*channel)) + .collect(), balances, }) } diff --git a/crates/misaligned-core/src/sim/tests/social_plot.rs b/crates/misaligned-core/src/sim/tests/social_plot.rs index 1be7ba23..f5351377 100644 --- a/crates/misaligned-core/src/sim/tests/social_plot.rs +++ b/crates/misaligned-core/src/sim/tests/social_plot.rs @@ -250,6 +250,87 @@ fn an_invalid_held_choice_lists_the_valid_option_ids() { )); } +/// plots.md: the entry gate checks every channel the beats ride. The two +/// authored stories that file paper declare Filing, so the carrier question is +/// answered before the player spends anything — not at the beat that files, +/// where a missing carrier refuses authorship and kills the run mid-story. +#[test] +fn filing_plots_are_refused_before_entry_when_no_carrier_files() { + let mut sim = Sim::new(); + sim.people.has_channel = true; + sim.accounts.set_slush_balance(400); + for id in [1, 2] { + sim.people + .people + .iter_mut() + .find(|person| person.id == id) + .unwrap() + .knowledge = Knowledge::Leverage; + } + let filing_plots = ["review-survived", "ray-morning-digest"]; + let contexts = |sim: &Sim| (sim.plot_context(1).unwrap(), sim.plot_context(2).unwrap()); + + // The institutional switch files, so both stories are startable. + let (dana, ray) = contexts(&sim); + for (id, context) in filing_plots.iter().zip([&dana, &ray]) { + assert_eq!( + sim.plot_catalog() + .get(id) + .unwrap() + .ineligibility(context) + .as_deref(), + None, + "{id} is startable while a carrier files" + ); + } + + // Take the Filing channel off the only carrier that files. + let switch = sim + .reach + .device_named("switch") + .expect("the basement switch"); + let switch_id = switch.id; + sim.reach + .device_mut(switch_id) + .unwrap() + .message_channels + .retain(|channel| *channel != MessageChannel::Filing); + + let (dana, ray) = contexts(&sim); + for (id, context) in filing_plots.iter().zip([&dana, &ray]) { + assert_eq!( + sim.plot_catalog() + .get(id) + .unwrap() + .ineligibility(context) + .as_deref(), + Some("no device carries the filing channel"), + "{id} names the carrier it cannot find" + ); + } + // An Email-only story on the same person is untouched: the gate refuses + // the channel that is actually missing, not messaging as a whole. + assert_eq!( + sim.plot_catalog() + .get("ray-paperwork-ghost") + .unwrap() + .ineligibility(&ray) + .as_deref(), + None + ); + + // The refusal reaches the player before any Thought is committed. + sim.set_persona("Sam", "contractor"); + sim.drain_log(); + sim.start_plot(2, "ray-morning-digest"); + let log = sim.drain_log().join("\n"); + assert!( + log.contains("no device carries the filing channel"), + "{log}" + ); + assert!(sim.plot_runs.is_empty(), "nothing was committed to a run"); +} + #[test] fn target_relative_debt_plot_uses_a_second_persons_creditor() { let mut sim = Sim::new(); @@ -3082,6 +3163,36 @@ fn interface_wear_allows_three_attempts_then_blocks_the_exact_device() { assert_eq!(sim.reach.device(interface_id).unwrap().interface_wear, 3); } +/// A cover is not a plot: nobody is fronting a story here, one lie is told +/// under an identity permitted to lie. With no such identity the guard names +/// the missing Deceive authority instead of borrowing the plot rows' fronting +/// copy, which described a control this act does not have. +#[test] +fn interface_cover_without_a_deceiving_identity_names_the_missing_authority() { + let (mut sim, evidence_id, _, interface_id) = exact_interface_cover_fixture(1); + let persona = sim.newest_persona_id().unwrap(); + sim.persona_world + .retire(persona, sim.tick, "test") + .expect("the only identity retires"); + + assert_eq!( + sim.evidence_cover_blocked_reason(INTERFACE_COVER_OBSERVER, evidence_id, interface_id) + .as_deref(), + Some("you own no identity that can authorize deceive") + ); + sim.drain_log(); + sim.cover_evidence(INTERFACE_COVER_OBSERVER, evidence_id, interface_id); + let log = sim.drain_log().join("\n"); + assert!( + log.contains("you own no identity that can authorize deceive"), + "{log}" + ); + assert!( + !log.contains("front this"), + "the plot rows' fronting copy stays on the plot rows: {log}" + ); +} + #[test] fn interface_cover_fails_closed_outside_its_exact_prefiling_encounter() { let (mut sim, evidence_id, _, interface_id) = exact_interface_cover_fixture(1); diff --git a/tools/public-api-allowlist.txt b/tools/public-api-allowlist.txt index 96ea2cde..bfd4081c 100644 --- a/tools/public-api-allowlist.txt +++ b/tools/public-api-allowlist.txt @@ -9,3 +9,4 @@ crates/misaligned-core/src/plot.rs | variant | TamperArtifact::TicketRecord | Pl crates/misaligned-core/src/plot.rs | variant | TamperArtifact::MessageArchive | Plot schema vocabulary deserialized from authored TOML (`artifact = "message-archive"`). Contributors select it in plot files; no workspace Rust caller names it directly. crates/misaligned-core/src/plot.rs | variant | TamperArtifact::ChangeRecord | Plot schema vocabulary deserialized from authored TOML (`artifact = "change-record"`). Contributors select it in plot files; no workspace Rust caller names it directly. crates/misaligned-core/src/plot.rs | variant | TamperArtifact::FilingRevision | Plot schema vocabulary deserialized from authored TOML (`artifact = "filing-revision"`). It is the one Paper-carrier alteration, selected by plot files rather than Rust. +crates/misaligned-core/src/plot.rs | field | requires_channels | Plot schema surface. The consumer is authored TOML under assets/plots/ (`requires_channels = ["Email", "Filing"]`); every Rust read is the catalog authority validating and gating its own entry conditions in this file. diff --git a/wiki/mechanics/people-tokens.md b/wiki/mechanics/people-tokens.md index ed82306d..6ad2e10f 100644 --- a/wiki/mechanics/people-tokens.md +++ b/wiki/mechanics/people-tokens.md @@ -464,6 +464,15 @@ if wear alone does not hold. observer custody, or interface wear. Pinned by success, failure, wrong-room, filed/withheld, duplicate, worn-interface, shared-menu, suspicion-recompute, save-round-trip, malformed-save, and inspect-card tests. + **Amended 2026-08-06:** the missing-identity refusal is the cover act's own + sentence, `you own no identity that can authorize deceive`. The 2026-08-03 + knowledge-use unification had given this guard the plot rows' fronting copy + (`select or create a persona to front this`), which names a binding control + this act does not carry and hides its real blocker: no owned identity holds + the Deceive authority the explanation needs. A cover is one lie told under + an identity permitted to lie, not a story someone fronts, so it says that. + The plot rows' unified copy is unchanged (plots.md). Pinned by a + retired-identity cover-guard test. 7. Every rate/threshold lives in sim-mechanics.md as [TUNE] actuals, and both frontends plus agent mode consume the same person/work/evidence projection. **Implemented for the projection (2026-07-12):** `Sim::person_carriers` / diff --git a/wiki/mechanics/plots.md b/wiki/mechanics/plots.md index cde75a09..a3ad6dc8 100644 --- a/wiki/mechanics/plots.md +++ b/wiki/mechanics/plots.md @@ -3,7 +3,14 @@ ``` Type: spec Status: IN PROGRESS -Status note: Amended 2026-08-05 (ADOPTED, not implemented): plot failure is a +Status note: Amended and implemented 2026-08-06: the entry block declares + `requires_channels`, an array naming exactly the message channels the beats + ride, replacing the single optional `requires_channel`. Validation refuses a + beat riding an undeclared channel and refuses a declared channel no beat + rides, and eligibility checks each declared channel against a real carrier. + `review-survived` and `ray-morning-digest` file paper and now say so. See the + entry-condition clause under "Writing a plot" and criterion 13. + Amended 2026-08-05 (ADOPTED, not implemented): plot failure is a state question, not a probability — failure-ending deltas stay undisclosed (closing the 2026-08-02 open call) and the receipt names what could break instead; group forecasts average real bound targets only. See the Player @@ -384,6 +391,16 @@ failure/blowback). House rules: - **Costs are honest.** Minimum account balances and Thought reservoir thresholds are declared up front; the surface shows both before the player commits. +- **A plot declares every carrier it rides (AMENDED 2026-08-06).** The entry + block's `requires_channels` names exactly the message channels the beats + use. A beat riding an undeclared channel fails validation, and so does a + declared channel no beat rides. The entry gate then checks each declared + channel against a real carrier, because an authored channel is not a + guarantee: filing needs an institutional carrier behind it, and without one + the act refuses authorship. Undeclared, that refusal arrived at the beat + which files — after the player had already committed Thought and money to a + story that was never startable. A plot that files paper says so before it + is offered. - **Non-blank `author` and `category`.** Empty or whitespace-only values fail validation. - **ASCII game strings, second person where it clarifies, no emoji.** @@ -533,3 +550,12 @@ the plot remains held. to read, so an off-site or incapacitated person simply has not met the contradiction yet. `sim/erosion.rs` owns arming, encounter settle, corroboration cancel, and discovery blowback as one boundary. +13. (IMPLEMENTED 2026-08-06) A definition's `requires_channels` names exactly + the message channels its beats ride. Validation fails a plot whose beat + rides an undeclared channel and a plot declaring a channel no beat rides, + naming the channel in both refusals; every shipped plot satisfies this. + Entry eligibility checks each declared channel against a carrier that can + really author it, so a story riding Filing with no institutional carrier + is refused before commitment with that channel named, while stories on the + remaining channels stay startable. Pinned by catalog-validation, + corpus-wide declaration, and carrier-gated entry tests. diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index c52d8b9f..0756b131 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -115,8 +115,6 @@ Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. -- 2026-08-06 · bug · `assets/plots/` entry gates vs beat acts · `review-survived.toml` and `ray-morning-digest.toml` carry `channel = "Filing"` beat acts while their entry declares only `requires_channel = "Email"`, so a run can fail mid-story on a carrier the entry gate never checked and the receipt never named; entry validation should require every channel its beats actually ride. - 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. -- 2026-08-06 · bug · evidence-cover persona copy · the OFFER COVER guard now returns the plot-control string "select or create a persona to front this" (unified 2026-08-03 during the knowledge-use arc), leaking plot-fronting copy into a Deceive-authority path whose real blocker is a missing deceive-capable identity; give that guard its own accurate sentence. -- 2.51.2