diff --git a/crates/weksa/src/daodejing.rs b/crates/weksa/src/daodejing.rs index 6eda129..3a7f514 100644 --- a/crates/weksa/src/daodejing.rs +++ b/crates/weksa/src/daodejing.rs @@ -42,8 +42,8 @@ const CONCEPTS: &[&str] = &[ "resemble", ]; -/// Frame glosses, from `frame:3`. 3 is minted from a Train chapter, 4 and 5 -/// from Validation chapters, 6 and 7 from Test chapters. +/// Frame glosses, from `frame:4`. 4 is minted from a Train chapter, 5 and 6 +/// from Validation chapters, 7 and 8 from Test chapters. const FRAMES: &[&str] = &[ "act", "intransitive", @@ -56,7 +56,7 @@ const FRAMES: &[&str] = &[ pub fn revision() -> (InventoryRevision, Vec) { let mut entries = Vec::new(); for (family, glosses) in [(Family::Concept, CONCEPTS), (Family::Frame, FRAMES)] { - let first = if family == Family::Concept { 6 } else { 3 }; + let first = if family == Family::Concept { 7 } else { 4 }; for (i, gloss) in glosses.iter().enumerate() { entries.push(InventoryEntry { id: family.id(NonZeroU32::new(first + i as u32).expect("nonzero")), @@ -85,7 +85,7 @@ fn concept(gloss: &str) -> &'static str { .iter() .position(|c| *c == gloss) .expect("known concept"); - Box::leak(format!("concept:{}", 6 + at).into_boxed_str()) + Box::leak(format!("concept:{}", 7 + at).into_boxed_str()) } fn frame(gloss: &str) -> &'static str { @@ -93,7 +93,7 @@ fn frame(gloss: &str) -> &'static str { .iter() .position(|c| *c == gloss) .expect("known frame"); - Box::leak(format!("frame:{}", 3 + at).into_boxed_str()) + Box::leak(format!("frame:{}", 4 + at).into_boxed_str()) } fn hex8(ascii: &str) -> String { diff --git a/crates/weksa/src/inventory.rs b/crates/weksa/src/inventory.rs index 405b4ae..c9d69a2 100644 --- a/crates/weksa/src/inventory.rs +++ b/crates/weksa/src/inventory.rs @@ -485,9 +485,20 @@ pub fn proof_revision() -> (InventoryRevision, Vec) { for (family, glosses) in [ ( Family::Concept, - ["endanger", "prevent", "player", "control", "ship_mind"].as_slice(), + [ + "endanger", + "prevent", + "player", + "control", + "ship_mind", + "request", + ] + .as_slice(), + ), + ( + Family::Frame, + ["hazard_warning", "intervention", "request_intervention"].as_slice(), ), - (Family::Frame, ["hazard_warning", "intervention"].as_slice()), ] { for (i, gloss) in glosses.iter().enumerate() { entries.push(InventoryEntry { diff --git a/crates/weksa/src/lower.rs b/crates/weksa/src/lower.rs index 27f7d2b..96f6d92 100644 --- a/crates/weksa/src/lower.rs +++ b/crates/weksa/src/lower.rs @@ -17,6 +17,14 @@ //! the target language, is refused: the concept word would say a different //! thing than the name the packet cites. //! +//! A request clause ("You asked Nibu to stop.") says what the addressee did, so +//! it is gated on the packet's own record of it: `Discourse.addressee_consent` +//! (set only by authoring or game telemetry, V15) must name that predication, +//! and its agent must be the addressee. Without it the lowerer refuses +//! ([`LowerError::ConsentMissing`], [`LowerError::AskerNotAddressee`]); it does +//! not read wording or guess who asked. A consent naming any other predication +//! is an unrendered slot. +//! //! The table is a temporary owner. Each row speaks for an inventory entry and //! names the gloss it speaks for, so a row cannot outlive a change of what its id //! means. `language-ontology` moves the rows into ontology documents. @@ -56,31 +64,31 @@ const CONCEPTS: &[Row] = &[ ("concept:5", "ship_mind", "ship mind"), // The Dao De Jing test bed (`daodejing.rs`). Nouns, then the verbs a // `{verb}` frame speaks. - ("concept:6", "sage", "sage"), - ("concept:7", "mind", "minds"), - ("concept:8", "belly", "bellies"), - ("concept:9", "will", "wills"), - ("concept:10", "bone", "bones"), - ("concept:11", "empty", "empties"), - ("concept:12", "fill", "fills"), - ("concept:13", "weaken", "weakens"), - ("concept:14", "strengthen", "strengthens"), - ("concept:15", "heaven", "heaven"), - ("concept:16", "earth", "earth"), - ("concept:17", "is_long_enduring", "is long-enduring"), - ("concept:18", "continues_long", "continues long"), - ("concept:19", "highest_excellence", "highest excellence"), - ("concept:20", "water", "water"), - ("concept:21", "true_words", "words that are strictly true"), - ("concept:22", "seem_paradoxical", "seem paradoxical"), + ("concept:7", "sage", "sage"), + ("concept:8", "mind", "minds"), + ("concept:9", "belly", "bellies"), + ("concept:10", "will", "wills"), + ("concept:11", "bone", "bones"), + ("concept:12", "empty", "empties"), + ("concept:13", "fill", "fills"), + ("concept:14", "weaken", "weakens"), + ("concept:15", "strengthen", "strengthens"), + ("concept:16", "heaven", "heaven"), + ("concept:17", "earth", "earth"), + ("concept:18", "is_long_enduring", "is long-enduring"), + ("concept:19", "continues_long", "continues long"), + ("concept:20", "highest_excellence", "highest excellence"), + ("concept:21", "water", "water"), + ("concept:22", "true_words", "words that are strictly true"), + ("concept:23", "seem_paradoxical", "seem paradoxical"), ( - "concept:23", + "concept:24", "space_between_heaven_and_earth", "space between heaven and earth", ), - ("concept:24", "bellows", "bellows"), - ("concept:25", "compare", "compare"), - ("concept:26", "resemble", "resemble"), + ("concept:25", "bellows", "bellows"), + ("concept:26", "compare", "compare"), + ("concept:27", "resemble", "resemble"), ]; /// Frames: the clause a predication of that frame says. `{gloss}` is the filler @@ -92,23 +100,32 @@ const FRAMES: &[Row] = &[ "{agent} may endanger {patient} by touching {instrument}", ), ("frame:2", "intervention", "{agent} will stop {patient}"), - // The Dao De Jing test bed. Frame 3 was written from a Train chapter, 4 and - // 5 from Validation chapters, 6 and 7 from Test chapters. - ("frame:3", "act", "{agent} {verb} {patient}"), - ("frame:4", "intransitive", "{experiencer} {verb}"), - ("frame:5", "likeness", "{experiencer} is like {goal}"), ( - "frame:6", + "frame:3", + "request_intervention", + "{agent} asked {patient} to stop", + ), + // The Dao De Jing test bed. Frame 4 was written from a Train chapter, 5 and + // 6 from Validation chapters, 7 and 8 from Test chapters. + ("frame:4", "act", "{agent} {verb} {patient}"), + ("frame:5", "intransitive", "{experiencer} {verb}"), + ("frame:6", "likeness", "{experiencer} is like {goal}"), + ( + "frame:7", "seems_paradoxical", "{experiencer} seem paradoxical", ), ( - "frame:7", + "frame:8", "may_be_compared", "{experiencer} may be compared to {goal}", ), ]; +/// The frame of "{agent} asked {patient} to stop": the one frame that reads +/// the discourse's addressee consent. +const REQUEST_FRAME: &str = "frame:3"; + /// Speech acts: the opener of the first sentence. const SPEECH_ACTS: &[Row] = &[ ("speech_act:1", "assert", ""), @@ -138,6 +155,12 @@ pub enum LowerError { NoPronunciation { entity: EntityId }, /// IPA was requested for a packet that cites no entity. NoEntityReferent, + /// A request predication the packet does not carry the addressee's consent + /// to: no consent, or a consent that names another predication. The + /// lowerer does not guess who asked. + ConsentMissing { predication: NodeId }, + /// A request predication whose agent is not the discourse addressee. + AskerNotAddressee { predication: NodeId }, /// The entity port failed to read. EntityRead { entity: EntityId, why: String }, } @@ -182,9 +205,7 @@ pub fn lower( { return unrendered("features"); } - if input.discourse().addressee_consent.is_some() { - return unrendered("addressee consent"); - } + request_gate(input, inv)?; if !input.relations().is_empty() { return unrendered("relations"); } @@ -231,6 +252,59 @@ pub fn lower( Ok(out) } +/// The one place consent is read. A request clause ([`REQUEST_FRAME`]) says what +/// the addressee did, so it needs the packet's own record of it: the discourse +/// `addressee_consent`, which validation lets only authoring or game telemetry +/// set (V15), must name that predication, and its agent must be the addressee. +/// Nothing here reads wording. A consent naming any other predication is a slot +/// the lowerer does not render. +fn request_gate(input: &LoweringInput, inv: &Inventory) -> Result<(), LowerError> { + let consent = input.discourse().addressee_consent.as_ref(); + let requests = input + .predications() + .iter() + .filter(|p| p.frame.to_string() == REQUEST_FRAME); + for p in requests { + if consent != Some(&p.id) { + return Err(LowerError::ConsentMissing { + predication: p.id.clone(), + }); + } + let agent = inv + .entries() + .find(|e| { + e.family() == Family::Role && e.gloss == "agent" && e.live_at(input.inventory_rev()) + }) + .ok_or_else(|| LowerError::UnknownRole { + frame: p.frame, + role: "agent".to_string(), + })?; + let asker = p + .roles + .iter() + .find(|r| r.role == agent.id) + .map(|r| &r.filler); + if asker.is_none() || asker != input.discourse().addressee.as_ref() { + return Err(LowerError::AskerNotAddressee { + predication: p.id.clone(), + }); + } + } + match consent { + Some(_) + if !input + .predications() + .iter() + .any(|p| p.frame.to_string() == REQUEST_FRAME) => + { + Err(LowerError::Unrendered { + what: "addressee consent", + }) + } + _ => Ok(()), + } +} + /// The ruled IPA of every entity a packet cites, one per entity referent in /// the packet's referent order, as stored: bare, stress-marked, nothing /// derived. A rejected pronunciation is never read. An entity the port does not diff --git a/crates/weksa/tests/lower.rs b/crates/weksa/tests/lower.rs index 1d8301d..3d796a5 100644 --- a/crates/weksa/tests/lower.rs +++ b/crates/weksa/tests/lower.rs @@ -436,3 +436,132 @@ fn ipa_needs_an_entity_the_port_holds_and_a_packet_that_cites_one() { Err(LowerError::NoEntityReferent) ); } + +/// `nibu-proof-001` re-cut as the request: the addressee asked Nibu to stop, +/// one frame:3 predication (n4), consent naming it, and no other claim. +fn asked() -> Packet { + let mut p = proof_packet(); + p.referents.remove(1); + let mut ask = p.predications[1].clone(); + ask.id = nd("n4"); + ask.concept = iv("concept:6"); + ask.frame = iv("frame:3"); + ask.roles = vec![ + weksa::packet::RoleFill { + role: iv("role:1"), + filler: nd("n1"), + }, + weksa::packet::RoleFill { + role: iv("role:2"), + filler: nd("n3"), + }, + ]; + p.predications = vec![ask]; + p.discourse.speech_act = Some(iv("speech_act:1")); + p.discourse.addressee_consent = Some(nd("n4")); + p.constraints.preserve = vec![nd("n1")]; + p +} + +/// The request plus a frame:2 predication (n5) the consent could wrongly name. +fn asked_and_stopped() -> Packet { + let mut p = asked(); + let mut stop = proof_packet().predications.remove(1); + stop.roles[1].filler = nd("n4"); + p.predications.push(stop); + p +} + +#[test] +fn a_consented_request_lowers_to_what_the_addressee_asked() { + let (inv, _dir) = committed(); + assert_eq!(lowered(&asked(), &inv).unwrap(), "You asked Nibu to stop."); + let mut p = asked(); + p.provenance.source = weksa::packet::ProvenanceSource::GameTelemetry; + assert_eq!(lowered(&p, &inv).unwrap(), "You asked Nibu to stop."); +} + +#[test] +fn a_request_without_its_consent_is_refused_not_invented() { + let (inv, _dir) = committed(); + let mut p = asked(); + p.discourse.addressee_consent = None; + assert_eq!( + lowered(&p, &inv), + Err(LowerError::ConsentMissing { + predication: nd("n4") + }) + ); + // Consent that names a different predication is no consent to the request. + let mut p = asked_and_stopped(); + p.discourse.addressee_consent = Some(nd("n5")); + assert_eq!( + lowered(&p, &inv), + Err(LowerError::ConsentMissing { + predication: nd("n4") + }) + ); +} + +#[test] +fn consent_to_one_of_two_requests_leaves_the_other_unconsented() { + let (inv, _dir) = committed(); + let mut p = asked(); + let mut second = p.predications[0].clone(); + second.id = nd("n5"); + p.predications.push(second); + assert_eq!( + lowered(&p, &inv), + Err(LowerError::ConsentMissing { + predication: nd("n5") + }) + ); +} + +#[test] +fn a_request_by_someone_other_than_the_addressee_is_refused() { + let (inv, _dir) = committed(); + let mut p = asked(); + p.predications[0].roles[0].filler = nd("n3"); + p.predications[0].roles[1].filler = nd("n1"); + assert_eq!( + lowered(&p, &inv), + Err(LowerError::AskerNotAddressee { + predication: nd("n4") + }) + ); +} + +#[test] +fn consent_on_a_claim_that_is_no_request_stays_unrendered() { + let (inv, _dir) = committed(); + let mut p = proof_packet(); + p.discourse.addressee_consent = Some(nd("n5")); + assert_eq!( + lowered(&p, &inv), + Err(LowerError::Unrendered { + what: "addressee consent" + }) + ); +} + +#[test] +fn a_decomposition_or_import_cannot_reach_the_lowerer_with_consent() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let inventory = InventoryStore::open(&root.join("data/interlingua/inventory.cc")).unwrap(); + let corpus = CorpusStore::open(&root.join("data/corpus/corpus.cc")).unwrap(); + let entities = EntityStore::open(&root.join("data/entity/entity.cc")).unwrap(); + for source in [ + weksa::packet::ProvenanceSource::ModelDecomposition, + weksa::packet::ProvenanceSource::ImportedCorpus, + ] { + let mut p = asked(); + p.provenance.source = source; + let dir = tempfile::tempdir().unwrap(); + let refused = PacketStore::open(&dir.path().join("p.cc")) + .unwrap() + .admit(p, &inventory, &corpus, &entities) + .unwrap_err(); + assert!(format!("{refused:?}").contains("V15"), "{source:?}"); + } +} diff --git a/data/daodejing/inventory.cc b/data/daodejing/inventory.cc index 0d2377f..373eb3c 100644 Binary files a/data/daodejing/inventory.cc and b/data/daodejing/inventory.cc differ diff --git a/data/daodejing/packets.cc b/data/daodejing/packets.cc index 26ef9c9..35b5bbe 100644 --- a/data/daodejing/packets.cc +++ b/data/daodejing/packets.cc @@ -1 +1 @@ -“²cultcache.store.v1‘—¸weksa.interlingua_packet¸weksa.interlingua_packet»weksa.interlingua_packet.v1¸weksa.interlingua_packetÙd{"schemaName":"weksa.interlingua_packet","schemaVersion":"weksa.interlingua_packet.v1","members":[]}‘¸weksa.interlingua_packet�•”·packet:64616f2d30333032¸weksa.interlingua_packet´2026-11-09T10:13:02ZÅRž·packet:64616f2d30333032­CorpusExample”¨Authored°item:daodejing:1¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:1ÀÀ�••¢n1¯referent_kind:1©concept:6À�•¢n2°referent_kind:11©concept:7À�•¢n3¯referent_kind:3©concept:8À�•¢n4°referent_kind:11©concept:9À�•¢n5¯referent_kind:3ªconcept:10À�”•¢n6ªconcept:11§frame:3’’¦role:1¢n1’¦role:2¢n2�•¢n7ªconcept:12§frame:3’’¦role:1¢n1’¦role:2¢n3�•¢n8ªconcept:13§frame:3’’¦role:1¢n1’¦role:2¢n4�•¢n9ªconcept:14§frame:3’’¦role:1¢n1’¦role:2¢n5��“�ÂÀ�’‘ÙXFour acts of one agent; 'in the exercise of his government' and 'their' are not carried.�‘‘’ªfeature:18�¥GradeÍ ”·packet:64616f2d30353032¸weksa.interlingua_packet´2026-11-09T10:13:02ZÅ^ž·packet:64616f2d30353032­CorpusExample”¨Authored°item:daodejing:5¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:2ÀÀ�’•¢n1¯referent_kind:4ªconcept:23À�•¢n2¯referent_kind:5ªconcept:24À�‘•¢n3ªconcept:25§frame:7’’¦role:3¢n1’¦role:6¢n2��“�ÂÀ�’‘ÙRA rhetorical question; the speech act is ask and the negated 'may not' is dropped.�‘‘’ªfeature:18�¥GradeÍ ”·packet:64616f2d30373031¸weksa.interlingua_packet´2026-11-09T10:13:02ZÅyž·packet:64616f2d30373031­CorpusExample”¨Authored°item:daodejing:2¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:1ÀÀ�’•¢n1¯referent_kind:4ªconcept:15À�•¢n2¯referent_kind:4ªconcept:16À�’•¢n3ªconcept:17§frame:4‘’¦role:3¢n1�•¢n4ªconcept:18§frame:4‘’¦role:3¢n2��“�ÂÀ�’‘ÙTHeaven and earth as places; whether they are entities is a ruling the ontology owes.�‘‘’ªfeature:18�¥GradeÍ ”·packet:64616f2d30383031¸weksa.interlingua_packet´2026-11-09T10:13:02ZÅwž·packet:64616f2d30383031­CorpusExample”¨Authored°item:daodejing:3¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:1ÀÀ�’•¢n1°referent_kind:11ªconcept:19À�•¢n2°referent_kind:11ªconcept:20À�‘•¢n3ªconcept:26§frame:5’’¦role:3¢n1’¦role:6¢n2��“�ÂÀ�’‘ÙSThe metaphor is a plain likeness predication; the tenor and vehicle are not marked.�‘’’ªfeature:18�¥GradeÍ ’ªfeature:16�¥GradeÍ,”·packet:64616f2d37383034¸weksa.interlingua_packet´2026-11-09T10:13:02ZÅNž·packet:64616f2d37383034­CorpusExample”¨Authored°item:daodejing:4¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:1ÀÀ�‘•¢n1°referent_kind:11ªconcept:21À�‘•¢n2ªconcept:22§frame:6‘’¦role:3¢n1��“�ÂÀ�’‘ÙmThe whole paradox is in the referent's concept and the frame; the packet holds no more than the surface does.�‘‘’ªfeature:18�¥GradeÍ \ No newline at end of file +“²cultcache.store.v1‘—¸weksa.interlingua_packet¸weksa.interlingua_packet»weksa.interlingua_packet.v1¸weksa.interlingua_packetÙd{"schemaName":"weksa.interlingua_packet","schemaVersion":"weksa.interlingua_packet.v1","members":[]}‘¸weksa.interlingua_packet�•”·packet:64616f2d30333032¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅSž·packet:64616f2d30333032­CorpusExample”¨Authored°item:daodejing:1¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:1ÀÀ�••¢n1¯referent_kind:1©concept:7À�•¢n2°referent_kind:11©concept:8À�•¢n3¯referent_kind:3©concept:9À�•¢n4°referent_kind:11ªconcept:10À�•¢n5¯referent_kind:3ªconcept:11À�”•¢n6ªconcept:12§frame:4’’¦role:1¢n1’¦role:2¢n2�•¢n7ªconcept:13§frame:4’’¦role:1¢n1’¦role:2¢n3�•¢n8ªconcept:14§frame:4’’¦role:1¢n1’¦role:2¢n4�•¢n9ªconcept:15§frame:4’’¦role:1¢n1’¦role:2¢n5��“�ÂÀ�’‘ÙXFour acts of one agent; 'in the exercise of his government' and 'their' are not carried.�‘‘’ªfeature:18�¥GradeÍ ”·packet:64616f2d30353032¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅ^ž·packet:64616f2d30353032­CorpusExample”¨Authored°item:daodejing:5¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:2ÀÀ�’•¢n1¯referent_kind:4ªconcept:24À�•¢n2¯referent_kind:5ªconcept:25À�‘•¢n3ªconcept:26§frame:8’’¦role:3¢n1’¦role:6¢n2��“�ÂÀ�’‘ÙRA rhetorical question; the speech act is ask and the negated 'may not' is dropped.�‘‘’ªfeature:18�¥GradeÍ ”·packet:64616f2d30373031¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅyž·packet:64616f2d30373031­CorpusExample”¨Authored°item:daodejing:2¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:1ÀÀ�’•¢n1¯referent_kind:4ªconcept:16À�•¢n2¯referent_kind:4ªconcept:17À�’•¢n3ªconcept:18§frame:5‘’¦role:3¢n1�•¢n4ªconcept:19§frame:5‘’¦role:3¢n2��“�ÂÀ�’‘ÙTHeaven and earth as places; whether they are entities is a ruling the ontology owes.�‘‘’ªfeature:18�¥GradeÍ ”·packet:64616f2d30383031¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅwž·packet:64616f2d30383031­CorpusExample”¨Authored°item:daodejing:3¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:1ÀÀ�’•¢n1°referent_kind:11ªconcept:20À�•¢n2°referent_kind:11ªconcept:21À�‘•¢n3ªconcept:27§frame:6’’¦role:3¢n1’¦role:6¢n2��“�ÂÀ�’‘ÙSThe metaphor is a plain likeness predication; the tenor and vehicle are not marked.�‘’’ªfeature:18�¥GradeÍ ’ªfeature:16�¥GradeÍ,”·packet:64616f2d37383034¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅNž·packet:64616f2d37383034­CorpusExample”¨Authored°item:daodejing:4¸encoder:636c617564652d73ÍX”ÀÀ��”¬speech_act:1ÀÀ�‘•¢n1°referent_kind:11ªconcept:22À�‘•¢n2ªconcept:23§frame:7‘’¦role:3¢n1��“�ÂÀ�’‘ÙmThe whole paradox is in the referent's concept and the frame; the packet holds no more than the surface does.�‘‘’ªfeature:18�¥GradeÍ \ No newline at end of file diff --git a/data/interlingua/inventory.cc b/data/interlingua/inventory.cc index becfc92..cf353db 100644 Binary files a/data/interlingua/inventory.cc and b/data/interlingua/inventory.cc differ diff --git a/docs/interlingua-standard.md b/docs/interlingua-standard.md index ca392d1..e349a90 100644 --- a/docs/interlingua-standard.md +++ b/docs/interlingua-standard.md @@ -195,9 +195,10 @@ and `features` carry source-side nuance on content (hedge, certainty, irony and whatever the inventory grows). This is source-side communicative pressure, not target-language register. `addressee_consent` is no feature: it names the predication the addressee asked for or agreed to, a fact about what the -addressee did. The English lowerer renders no consent and refuses a packet that -carries one; a row that speaks for the request ("you asked her to") reads it -and refuses without it. +addressee did. The English lowerer's request row (`frame:3`, "{agent} asked +{patient} to stop") reads it: the consent must name that predication and its +agent must be the addressee, else `ConsentMissing` or `AskerNotAddressee`. A +consent naming any other predication is refused as unrendered. ### `stance` diff --git a/docs/targets/interlingua-lowering-map.md b/docs/targets/interlingua-lowering-map.md index 4517320..5fa3022 100644 --- a/docs/targets/interlingua-lowering-map.md +++ b/docs/targets/interlingua-lowering-map.md @@ -342,7 +342,7 @@ measurable baseline from a packet: `lower` gives text, `score` gives numbers. | Whether a packet exists | `validate` / `PacketStore::admit` (unchanged) | packet, inventory, corpus, entities | admitted packet or typed refusal | the lowerer | | Wording of a packet in English | `crates/weksa/src/lower.rs`: `lower(&LoweringInput, &Inventory) -> Result` | the validated lowering view and the inventory at its revision | text, or a typed refusal naming the missing template | the corpus item, the scorer, any model | | English template table (concept words, frame templates, speech-act openers) | the same module, as rows `(inventory id, gloss it speaks for, surface)` | none; it is data in code | rows the lowerer looks up | the inventory (it owns the ids and glosses, not the English) | -| The inventory's concepts and frames | an inventory revision; here `inventory::proof_revision()` (rev 2), hand-seeded as `seed()` is | none | the 5 concepts and 2 frames `nibu-proof-001` names | the lowerer | +| The inventory's concepts and frames | an inventory revision; here `inventory::proof_revision()` (rev 2), hand-seeded as `seed()` is | none | the 6 concepts and 3 frames `nibu-proof-001` and the consent-gated request packet name | the lowerer | | What a score counts | `Scorer::score` (unchanged) | output text, reference text | `ItemScore` | `weksa score`, which only reads text and calls it | | The proof packet | `packet::proof_packet()`, typed ids of rev 2, admitted by `weksa packet proof ` through `PacketStore::admit`. Not JSON: `Packet`'s serde is CultCache's positional tuple, and JSON in is the xenos boundary the cut 7 encoder owns | none | admitted packet | the CLI decides nothing about validity |