From ebbb5c49ea10ca3de145ecb410b3dfe6e989caa1 Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Tue, 8 Sep 2026 12:41:52 +0200 Subject: [PATCH] docs(persona): the TUI says "attribute" too, not "fact" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The identity pane was the third surface `design-docs/persona-vocabulary.md` governs and the one left behind when the console and `pnm` were brought over. A tab was literally named *Your facts* and its form said *Add a fact*. The word was wrong in two independent ways. It asserts what the model cannot promise: everything in the pool is self-asserted until a credential backs it, and a face exists so a person may show an old value, a pinned version, a per-context override, or something untrue — the pane said *fact* directly above the provenance line that said *you said so*. And it was already taken: `Facts` is the VTC ceremony engine's term for a *verified* policy input. - tab `Your facts` → `Your attributes`; `Add a fact` → `Add an attribute` - `wears: work (3 facts)` → `(3 attributes)` in `BindingSummary::describe` - `no facts recorded`, `(unnamed fact)`, `Saved the fact`, `Forgot the fact`, `Could not read your facts` and the pane's guidance copy follow - the definition phrase is now the table's: *something you say about yourself, held once* - `the_pane_speaks_the_agreed_vocabulary` bans `fact` and permits `attribute`, with the reasoning inline so it is not "fixed" back Nothing in `openvtc-core::persona`'s types, the wire format or the task URIs moves — they always said `attribute`. Ordinary English (`after the fact`, `in fact`) is left alone. 446 + 520 + 28 tests pass; clippy and fmt clean. Signed-off-by: Glenn Gore --- openvtc-core/src/persona/binding.rs | 16 ++-- openvtc-core/src/persona/disclosure.rs | 14 +-- openvtc-core/src/persona/mod.rs | 2 +- openvtc-core/src/persona/pool.rs | 10 +- openvtc-core/src/persona/profile.rs | 4 +- openvtc/src/health_cmd.rs | 4 +- openvtc/src/state_handler/actions/mod.rs | 4 +- .../src/state_handler/main_page/content.rs | 8 +- openvtc/src/state_handler/persona_actions.rs | 14 +-- .../main/components/communities_panel.rs | 2 +- .../pages/main/components/identity_panel.rs | 96 +++++++++++-------- openvtc/src/ui/pages/main/mod.rs | 2 +- 12 files changed, 98 insertions(+), 78 deletions(-) diff --git a/openvtc-core/src/persona/binding.rs b/openvtc-core/src/persona/binding.rs index 6522af7..fd15cc0 100644 --- a/openvtc-core/src/persona/binding.rs +++ b/openvtc-core/src/persona/binding.rs @@ -98,12 +98,12 @@ impl BindingSummary { .clone() .or_else(|| self.profile_id.clone()) .unwrap_or_else(|| "an unnamed face".to_string()); - let facts = if self.claim_count == 1 { - "1 fact".to_string() + let attributes = if self.claim_count == 1 { + "1 attribute".to_string() } else { - format!("{} facts", self.claim_count) + format!("{} attributes", self.claim_count) }; - format!("wears: {label} ({facts})") + format!("wears: {label} ({attributes})") } } @@ -228,7 +228,7 @@ mod tests { claim_count: 3, ..Default::default() }; - assert_eq!(s.describe(), "wears: work (3 facts)"); + assert_eq!(s.describe(), "wears: work (3 attributes)"); } /// One claim is not "1 claims". Small, and the kind of thing that makes a @@ -241,7 +241,7 @@ mod tests { claim_count: 1, ..Default::default() }; - assert_eq!(s.describe(), "wears: gaming (1 fact)"); + assert_eq!(s.describe(), "wears: gaming (1 attribute)"); } /// A profile with no label falls back to its id, and then to a phrase — @@ -255,13 +255,13 @@ mod tests { claim_count: 2, ..Default::default() }; - assert_eq!(by_id.describe(), "wears: 01J8 (2 facts)"); + assert_eq!(by_id.describe(), "wears: 01J8 (2 attributes)"); let bare = BindingSummary { bound: true, claim_count: 2, ..Default::default() }; - assert_eq!(bare.describe(), "wears: an unnamed face (2 facts)"); + assert_eq!(bare.describe(), "wears: an unnamed face (2 attributes)"); } } diff --git a/openvtc-core/src/persona/disclosure.rs b/openvtc-core/src/persona/disclosure.rs index 9c0bf07..0a96200 100644 --- a/openvtc-core/src/persona/disclosure.rs +++ b/openvtc-core/src/persona/disclosure.rs @@ -11,7 +11,7 @@ //! "disclose something now" button here would be a request with no requester — //! the one shape the two-call gate exists to prevent. //! -//! What the TUI can usefully answer is the question after the fact: what does +//! What the TUI can usefully answer is the question asked after the fact: what does //! anyone already know, and how did they come to know it. That is this module. //! //! # A rung is not a detail @@ -111,17 +111,17 @@ impl DisclosureRow { } } - /// The facts as one line: `email.work (whole), age.over18 (yes/no only)`. + /// The attributes as one line: `email.work (whole), age.over18 (yes/no only)`. /// - /// The rung travels with every fact rather than being summarised, because + /// The rung travels with every attribute rather than being summarised, because /// there is no summary of a mixed release that is not misleading in one /// direction or the other — and because severity inverts intuition: a - /// credential shown *whole* links the holder more than a fact they simply + /// credential shown *whole* links the holder more than an attribute they simply /// asserted. #[must_use] pub fn describe_claims(&self) -> String { if self.claims.is_empty() { - return "no facts recorded".to_string(); + return "no attributes recorded".to_string(); } self.claims .iter() @@ -178,7 +178,7 @@ mod tests { /// Rungs are shown in the words the vocabulary fixes, and an unrecognised /// one is passed through rather than mapped to a friendlier neighbour — the /// four words carry a privacy ordering, so guessing would misstate how much - /// of the fact left. + /// of the attribute left. #[test] fn a_fact_carries_its_rung_into_the_row() { let row = DisclosureRow::from_wire(&serde_json::json!({ @@ -224,6 +224,6 @@ mod tests { #[test] fn a_factless_record_says_so() { let row = DisclosureRow::default(); - assert_eq!(row.describe_claims(), "no facts recorded"); + assert_eq!(row.describe_claims(), "no attributes recorded"); } } diff --git a/openvtc-core/src/persona/mod.rs b/openvtc-core/src/persona/mod.rs index c582880..0725076 100644 --- a/openvtc-core/src/persona/mod.rs +++ b/openvtc-core/src/persona/mod.rs @@ -1,4 +1,4 @@ -//! The holder's own identity — the personas, the facts behind them, and what each +//! The holder's own identity — the personas, the attributes behind them, and what each //! persona presents where. //! //! # Two meanings of "persona", and they compose diff --git a/openvtc-core/src/persona/pool.rs b/openvtc-core/src/persona/pool.rs index 686d446..af14ab2 100644 --- a/openvtc-core/src/persona/pool.rs +++ b/openvtc-core/src/persona/pool.rs @@ -1,4 +1,4 @@ -//! The attribute pool — the facts themselves, held once and projected many +//! The attribute pool — the attributes themselves, held once and projected many //! times. **Holder-scoped**: above every trust context, and never readable //! from inside one. //! @@ -94,7 +94,7 @@ impl ProvenanceKind { /// /// Always shown beside the label, because it is the half people miss: a /// credential is *provable* and carries the same issuer signature to every - /// verifier, which is the more consequential of the two facts and the less + /// verifier, which is the more consequential of the two attributes and the less /// obvious one. Severity inverts intuition here — a credential shown whole /// links more than a value the holder simply asserted — so the words must /// not hide it. @@ -174,7 +174,7 @@ impl PoolAttribute { match self.label.as_deref() { Some(label) if !label.trim().is_empty() => label, _ if !self.claim_type.is_empty() => &self.claim_type, - _ => "(unnamed fact)", + _ => "(unnamed attribute)", } } @@ -292,7 +292,7 @@ impl AttributeEdit { pub fn refusal(kind: ProvenanceKind) -> Self { Self::Refused(match kind { ProvenanceKind::CredentialBacked => { - "This fact comes from a credential — typing over it would turn something \ + "This attribute comes from a credential — typing over it would turn something \ provable into something you said. Change it at its source, or replace the \ credential." .to_string() @@ -542,7 +542,7 @@ mod tests { assert_eq!(attr.revealed_value(true), "4242424242424242"); } - /// A type whose style is `none` is shown as it is held. Masking every fact + /// A type whose style is `none` is shown as it is held. Masking every attribute /// would teach the reveal key as a reflex, and a reveal pressed by reflex /// protects nothing. #[test] diff --git a/openvtc-core/src/persona/profile.rs b/openvtc-core/src/persona/profile.rs index 138d338..0f606f2 100644 --- a/openvtc-core/src/persona/profile.rs +++ b/openvtc-core/src/persona/profile.rs @@ -11,7 +11,7 @@ //! attribute, a `pinVersion` pin of one, an `override` of its value, and an //! `inline` value that never enters the pool. [`put`] from here writes the //! first, and a picker over the pool is exactly what that form is: tick the -//! facts this persona shows. It keeps "edit once, everywhere" true, which is the +//! attributes this persona shows. It keeps "edit once, everywhere" true, which is the //! property a holder is relying on when they correct their address in one //! place. //! @@ -147,7 +147,7 @@ impl ResolvedClaim { /// rather than an omission. A resolved claim has no identity of its own to /// reveal *one* of — a face is read as a whole — so the only reveal this /// type could offer is the blanket one the mask exists to avoid. A holder - /// who wants to check a value reads it among their facts, one at a time. + /// who wants to check a value reads it among their attributes, one at a time. #[must_use] pub fn display_value(&self) -> String { if self.stale { diff --git a/openvtc/src/health_cmd.rs b/openvtc/src/health_cmd.rs index cee568b..082fbf3 100644 --- a/openvtc/src/health_cmd.rs +++ b/openvtc/src/health_cmd.rs @@ -405,7 +405,9 @@ impl VtaAccess { // `--capabilities` narrows everywhere else it appears, and someone // pasting that second line deserves to know why this one does not. println!(" `persona-holder` is the exception that grants rather than narrows: it adds"); - println!(" authority over your own identity — the facts and faces that sit above every"); + println!( + " authority over your own identity — the attributes and faces that sit above every" + ); println!(" context — without widening this install's reach into any other context."); println!(); } diff --git a/openvtc/src/state_handler/actions/mod.rs b/openvtc/src/state_handler/actions/mod.rs index 89f1157..95ccd4d 100644 --- a/openvtc/src/state_handler/actions/mod.rs +++ b/openvtc/src/state_handler/actions/mod.rs @@ -205,9 +205,9 @@ pub enum PersonaAction { /// a listing fetched without values does not hold them, which is what makes /// this an opt-in rather than a blindfold. ToggleValues, - /// Show the selected fact's value unmasked, or stop showing it. + /// Show the selected attribute's value unmasked, or stop showing it. /// - /// One fact, not a mode: a claim type carrying a mask style is shown + /// One attribute, not a mode: a claim type carrying a mask style is shown /// reduced even in a listing that asked for values, and this lifts that for /// the row under the cursor only. Moving the selection puts it back. RevealValue(usize), diff --git a/openvtc/src/state_handler/main_page/content.rs b/openvtc/src/state_handler/main_page/content.rs index d81760c..c67b2df 100644 --- a/openvtc/src/state_handler/main_page/content.rs +++ b/openvtc/src/state_handler/main_page/content.rs @@ -412,7 +412,7 @@ pub enum PersonaTab { /// The persona DIDs themselves — a persona as an *identity*. #[default] Personas, - /// The attribute pool: the facts, held once. + /// The attribute pool: the attributes, held once. Attributes, /// Named projections over the pool. Profiles, @@ -447,7 +447,7 @@ impl PersonaTab { pub fn label(self) -> &'static str { match self { PersonaTab::Personas => "Personas", - PersonaTab::Attributes => "Your facts", + PersonaTab::Attributes => "Your attributes", PersonaTab::Profiles => "Faces", PersonaTab::Communities => "Communities", PersonaTab::Disclosures => "What has left", @@ -746,12 +746,12 @@ pub struct IdentityState { /// holder asking to see their own identity, so it is a keypress they make /// on purpose and a network round-trip, not a display flag. pub show_values: bool, - /// The one fact whose value is being shown unmasked, by `attribute_id`. + /// The one attribute whose value is being shown unmasked, by `attribute_id`. /// /// One, and only while it is also the selected row — the render checks /// both. Sensitivity is a property of the claim type, so a card number and /// a date of birth are masked even in a listing the holder asked to see - /// (`openvtc_core::persona::claim_types`), and lifting that is a per-fact + /// (`openvtc_core::persona::claim_types`), and lifting that is a per-attribute /// act rather than a mode the pane can be left in. /// /// Cleared by moving the selection, changing tab, or a re-read. A reveal diff --git a/openvtc/src/state_handler/persona_actions.rs b/openvtc/src/state_handler/persona_actions.rs index 39a7f43..21303ad 100644 --- a/openvtc/src/state_handler/persona_actions.rs +++ b/openvtc/src/state_handler/persona_actions.rs @@ -21,7 +21,7 @@ //! This module is engineering-side: it keeps the spec's nouns //! (`attribute`, `profile`, `binding`), because those are what it addresses on //! the wire. The strings it *hands to the panel* — status lines, refusals — use -//! the vocabulary a person reads (`design-docs/persona-vocabulary.md`): a fact, +//! the vocabulary a person reads (`design-docs/persona-vocabulary.md`): an attribute, //! a face, wearing one. //! //! # Questions are asked once, and correctly @@ -118,7 +118,7 @@ pub(crate) fn apply(state: &mut State, action: &PersonaAction) -> PersonaEffect p.open_profile = None; p.status_message = None; // A reveal is granted to one row on one tab. Coming back to the - // facts should find them masked again, not still open. + // attributes should find them masked again, not still open. p.revealed_attribute = None; // Read on arrival, once. The agent-served tabs are not polled: a // pane nobody has opened should not be asking the agent about the @@ -652,7 +652,7 @@ impl PersonaJobRun { let client = self.admin_vta; match self.job { PersonaJob::AttributePut(draft) => PersonaOutcome::Written { - verb: "Saved the fact", + verb: "Saved the attribute", error: pool::put(&client, draft) .await .err() @@ -662,7 +662,7 @@ impl PersonaJobRun { attribute_id, cascade, } => PersonaOutcome::Written { - verb: "Forgot the fact", + verb: "Forgot the attribute", error: pool::delete(&client, &attribute_id, cascade) .await .err() @@ -1098,7 +1098,7 @@ mod tests { assert!(matches!(personas(&state).mode, PersonaMode::Attribute(_))); } - /// A reveal names one fact, is put back by the same key, and never becomes + /// A reveal names one attribute, is put back by the same key, and never becomes /// a read. /// /// Lifting the mask touches nothing but this pane: the value is already in @@ -1276,7 +1276,7 @@ mod tests { for error in [None, Some("refused".to_string())] { let mut state = State::default(); PersonaOutcome::Written { - verb: "Saved the fact", + verb: "Saved the attribute", error, } .apply(&mut state); @@ -1348,7 +1348,7 @@ mod tests { }); PersonaOutcome::Written { - verb: "Saved the fact", + verb: "Saved the attribute", error: Some("version conflict".to_string()), } .apply(&mut state); diff --git a/openvtc/src/ui/pages/main/components/communities_panel.rs b/openvtc/src/ui/pages/main/components/communities_panel.rs index 12f1f04..518bf62 100644 --- a/openvtc/src/ui/pages/main/components/communities_panel.rs +++ b/openvtc/src/ui/pages/main/components/communities_panel.rs @@ -32,7 +32,7 @@ impl Panel for CommunitiesPanel { /// Render the communities panel content. /// /// `bindings` is the persona pane's map, read here rather than copied: this -/// panel and the persona pane render the same fact — what a persona presents in a +/// panel and the persona pane render the same thing — what a persona presents in a /// community — and two copies of it would drift the moment one was refreshed /// and the other was not. pub fn render( diff --git a/openvtc/src/ui/pages/main/components/identity_panel.rs b/openvtc/src/ui/pages/main/components/identity_panel.rs index c9cda8d..f73fe44 100644 --- a/openvtc/src/ui/pages/main/components/identity_panel.rs +++ b/openvtc/src/ui/pages/main/components/identity_panel.rs @@ -1,7 +1,7 @@ //! The identity pane — every surface for a person's own identity, in one place. //! //! Five tabs, in the order the concepts build on each other. On screen they are -//! **Personas**, **Your facts**, **Faces**, **Communities** and **What has +//! **Personas**, **Your attributes**, **Faces**, **Communities** and **What has //! left**; in the code and on the wire they are personas, attributes, profiles, //! contexts and disclosures. The first comes from `Config`; the rest come from //! the agent. @@ -12,12 +12,17 @@ //! console, `pnm`, the mobile agent and this pane say the same thing. The //! spec's words are exact and stay in the types, on the wire and in the audit //! log; they are kept off the screen. `persona/attribute/put` stays -//! `persona/attribute/put` — the form says *Add a fact*. +//! `persona/attribute/put` — the form says *Add an attribute*. //! //! The three that matter most here: //! -//! - an **attribute** is a **fact** — *a fact about you, held once*; -//! - a **profile** is a **face** — *the set of facts you show together*. Not +//! - an **attribute** stays an **attribute** — *something you say about +//! yourself, held once*. This row is the one the table does not translate: +//! the friendlier word it used to carry, *fact*, claimed a truth a +//! self-asserted value does not have, and `Facts` already means a verified +//! policy input in `vtc-service`. Truth is carried by the provenance line +//! beneath the value, never by the noun; +//! - a **profile** is a **face** — *the set of attributes you show together*. Not //! "profile", which already means three things in this product and "my //! LinkedIn page" to everyone else; //! - a **binding** is **wearing**: a persona *wears* a face in a community. @@ -28,7 +33,7 @@ //! //! # What this pane will not draw //! -//! A value the holder never asked to see. The list of facts is fetched without +//! A value the holder never asked to see. The list of attributes is fetched without //! values by default and `v` re-reads *with* them — an opt-in that costs a //! round-trip rather than a display flag over data already in memory, because a //! listing that holds values has already read someone's identity whether or not @@ -202,7 +207,7 @@ fn hints(state: &IdentityState) -> &'static str { "↑/↓ select n: new persona g: agent names d: remove unused ⇥/⇧⇥: tab" } PersonaTab::Attributes => { - "↑/↓ select n: add a fact e: edit d: delete v: values s: show one \ + "↑/↓ select n: add an attribute e: edit d: delete v: values s: show one \ r: refresh ⇥/⇧⇥: tab" } PersonaTab::Profiles => { @@ -316,14 +321,14 @@ fn render_personas(state: &IdentityState, lines: &mut Vec>) { // --------------------------------------------------------------------------- fn render_attributes(state: &IdentityState, lines: &mut Vec>) { - if push_agent_state(state, lines, "facts") { + if push_agent_state(state, lines, "attributes") { return; } lines.push(Line::from(vec![ Span::styled( format!( - " {} fact{} about you", + " {} attribute{} about you", state.attributes.len(), if state.attributes.len() == 1 { "" } else { "s" } ), @@ -347,7 +352,7 @@ fn render_attributes(state: &IdentityState, lines: &mut Vec>) { if state.attributes.is_empty() { lines.push( Line::from( - " No facts yet. `n` adds one — a name, an email, a date of birth. A fact \ + " No attributes yet. `n` adds one — a name, an email, a date of birth. An attribute \ about you, held once; faces select from these.", ) .fg(COLOR_DARK_GRAY), @@ -362,7 +367,7 @@ fn render_attributes(state: &IdentityState, lines: &mut Vec>) { if state.attributes.iter().any(PoolAttribute::is_masked) { lines.push( Line::from( - " Some facts are masked by what they are — `s` shows the selected one. The \ + " Some attributes are masked by what they are — `s` shows the selected one. The \ mask is against someone reading over your shoulder; your agent has already \ sent the value here.", ) @@ -400,7 +405,7 @@ fn render_attributes(state: &IdentityState, lines: &mut Vec>) { }, ), ])); - // A reveal is granted to one fact, and only while it is the selected + // A reveal is granted to one attribute, and only while it is the selected // one. Checking the selection as well as the identifier means a list // that re-sorted under a stale grant cannot open a row nobody chose. let revealed = @@ -457,12 +462,13 @@ fn render_profiles(state: &IdentityState, lines: &mut Vec>) { } if detail.resolved.is_empty() { lines.push( - Line::from(" This face shows nothing — no facts are on it.").fg(COLOR_DARK_GRAY), + Line::from(" This face shows nothing — no attributes are on it.") + .fg(COLOR_DARK_GRAY), ); } else { lines.push( Line::from(format!( - " Shows {} fact{}:", + " Shows {} attribute{}:", detail.resolved.len(), if detail.resolved.len() == 1 { "" } else { "s" } )) @@ -481,7 +487,7 @@ fn render_profiles(state: &IdentityState, lines: &mut Vec>) { ), ])); // A value that lives only in this face is not among the - // holder's facts, so correcting it there will not correct it + // holder's attributes, so correcting it there will not correct it // here. Saying so on the row is the only place they find out. let origin = match claim.attribute_id { Some(_) => claim.provenance.label().to_string(), @@ -496,12 +502,12 @@ fn render_profiles(state: &IdentityState, lines: &mut Vec>) { lines.push(Line::from("")); // No per-claim reveal here: a face has no cursor over its claims, so // the only reveal this view could offer is the blanket one the mask - // exists to avoid. The one-at-a-time reveal lives with the facts. + // exists to avoid. The one-at-a-time reveal lives with the attributes. if detail.resolved.iter().any(ResolvedClaim::is_masked) { lines.push( Line::from( " Some values are masked by what they are. Read one of them among your \ - facts, where they open one at a time.", + attributes, where they open one at a time.", ) .fg(COLOR_DARK_GRAY), ); @@ -528,9 +534,9 @@ fn render_profiles(state: &IdentityState, lines: &mut Vec>) { if state.profiles.is_empty() { lines.push( Line::from( - " No faces yet. A face is the set of facts you show together — \"Work\", \ + " No faces yet. A face is the set of attributes you show together — \"Work\", \ \"Gaming\" — and it is what a persona wears in a community. What you leave \ - unticked stays out, including facts you add later.", + unticked stays out, including attributes you add later.", ) .fg(COLOR_DARK_GRAY), ); @@ -552,7 +558,7 @@ fn render_profiles(state: &IdentityState, lines: &mut Vec>) { ), Span::styled( format!( - "{} fact{}", + "{} attribute{}", profile.entry_count, if profile.entry_count == 1 { "" } else { "s" } ), @@ -783,7 +789,8 @@ fn push_agent_state(state: &IdentityState, lines: &mut Vec>, noun: fn holder_grant_hint(credential_did: Option<&str>) -> Vec { let subject = credential_did.unwrap_or(""); vec![ - " Your agent credential administers this context. Your facts, and the faces".to_string(), + " Your agent credential administers this context. Your attributes, and the faces" + .to_string(), " over them, sit above every context — reaching them is a separate grant:".to_string(), String::new(), format!(" pnm acl update {subject} --capabilities persona-holder"), @@ -815,9 +822,9 @@ fn render_attribute_form(form: &AttributeForm) -> Vec> { let mut lines = vec![Line::from("")]; lines.push( Line::from(if form.attribute_id.is_some() { - " Edit a fact" + " Edit an attribute" } else { - " Add a fact" + " Add an attribute" }) .fg(COLOR_SUCCESS) .bold(), @@ -825,7 +832,7 @@ fn render_attribute_form(form: &AttributeForm) -> Vec> { lines.push(Line::from("")); lines.push( Line::from( - " A fact about you, held once. Faces select it, so correcting it here corrects \ + " Something you say about yourself, held once. Faces select it, so correcting it here corrects \ it everywhere it is worn.", ) .fg(COLOR_DARK_GRAY), @@ -908,14 +915,14 @@ fn render_profile_form(state: &IdentityState, form: &ProfileForm) -> Vec Vec PoolAttribute { PoolAttribute { attribute_id: "01B".into(), diff --git a/openvtc/src/ui/pages/main/mod.rs b/openvtc/src/ui/pages/main/mod.rs index ed94673..2d1b6ca 100644 --- a/openvtc/src/ui/pages/main/mod.rs +++ b/openvtc/src/ui/pages/main/mod.rs @@ -3554,7 +3554,7 @@ mod key_handler_tests { s.main_page.content_panel.identity.tab = PersonaTab::Attributes; }); page.handle_key_event(press(KeyCode::Char('s'))); - assert!(rx.try_recv().is_err(), "no fact, no reveal"); + assert!(rx.try_recv().is_err(), "no attribute, no reveal"); } /// An armed confirmation owns `y`/`n` — every other pane verb is suppressed