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.