From 81572b1a5425e76ac89fc9da20faa0d0581582f5 Mon Sep 17 00:00:00 2001 From: Weksa Date: Sat, 7 Nov 2026 22:04:59 +0000 Subject: [PATCH] Packet carries an addressee-consent fact (schema v5, V14, V15) Discourse.addressee_consent names the predication the addressee asked for or agreed to. V4 checks it resolves, V14 needs an addressee and a predication, V15 allows only Authored or GameTelemetry sources. The English lowerer renders no consent and refuses a packet carrying one. Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/src/lower.rs | 3 + crates/weksa/src/packet.rs | 10 +++- crates/weksa/src/validate.rs | 41 ++++++++++++- crates/weksa/tests/common/mod.rs | 1 + crates/weksa/tests/entities.rs | 2 +- crates/weksa/tests/lower.rs | 2 + crates/weksa/tests/stores.rs | 4 +- crates/weksa/tests/validate.rs | 59 +++++++++++++++++++ docs/interlingua-standard.md | 16 +++-- .../weksa.interlingua_packet.schema.json | 11 ++++ 10 files changed, 139 insertions(+), 10 deletions(-) diff --git a/crates/weksa/src/lower.rs b/crates/weksa/src/lower.rs index 43a8752..8f7b856 100644 --- a/crates/weksa/src/lower.rs +++ b/crates/weksa/src/lower.rs @@ -139,6 +139,9 @@ pub fn lower( { return unrendered("features"); } + if input.discourse().addressee_consent.is_some() { + return unrendered("addressee consent"); + } if !input.relations().is_empty() { return unrendered("relations"); } diff --git a/crates/weksa/src/packet.rs b/crates/weksa/src/packet.rs index f2f06de..9d6c582 100644 --- a/crates/weksa/src/packet.rs +++ b/crates/weksa/src/packet.rs @@ -16,7 +16,7 @@ pub use crate::inventory::InvId; use crate::inventory::OptionLabel; /// Bumped by a commit that changes these types; the published schema moves with it. -pub const SCHEMA_VERSION: u32 = 4; +pub const SCHEMA_VERSION: u32 = 5; /// Id of a referent or predication within one packet: `n`, `k` a canonical /// decimal 1..=9999. @@ -261,6 +261,13 @@ pub struct Discourse { /// Speech-act id. pub speech_act: Option, pub addressee: Option, + /// The predication the addressee asked for or agreed to. A fact about what + /// the addressee did, never about how the line sounds, so it is no feature: + /// only a source that saw the request may set it (V15), it needs an + /// addressee (V14) and names a predication (V14), and a lowering that + /// speaks for the addressee's request ("you asked her to") reads it here + /// and refuses without it. + pub addressee_consent: Option, pub features: Vec, } @@ -385,6 +392,7 @@ pub fn proof_packet() -> Packet { discourse: Discourse { speech_act: Some(id("speech_act:5")), addressee: Some(node("n1")), + addressee_consent: None, features: vec![], }, referents: vec![ diff --git a/crates/weksa/src/validate.rs b/crates/weksa/src/validate.rs index 6630430..e8aaf2f 100644 --- a/crates/weksa/src/validate.rs +++ b/crates/weksa/src/validate.rs @@ -3,7 +3,7 @@ //! returns `Ok(())` or the refusals. [`LoweringInput`], the only thing a //! lowerer sees, comes from `PacketStore::admit` alone, which builds it through //! the crate-private `admissible`. The stance is checked like every feature -//! list, in slot `Stance`. The rules are V1..V8 and V10..V13: the shape of an id +//! list, in slot `Stance`. The rules are V1..V8 and V10..V15: the shape of an id //! or a grade is its type's parse, not a rule, so V9 is retired and not //! renumbered. V13 guards the one other way features reach a lowering view: //! [`LoweringInput::with_scene`]. @@ -29,7 +29,7 @@ use std::fmt; use crate::inventory::{Family, InvId, Inventory, InventoryEntry, Slot, ValueKind}; use crate::packet::{ Constraints, Context, Discourse, Extension, FeatValue, FeatureValue, NodeId, Packet, - PacketKind, Predication, Referent, Relation, SCHEMA_VERSION, Stance, + PacketKind, Predication, ProvenanceSource, Referent, Relation, SCHEMA_VERSION, Stance, }; /// A broken rule, named. @@ -71,6 +71,15 @@ pub enum Refusal { /// state the speaker's relation to the addressee, so with none they /// describe nobody. V13SceneWithoutAddressee, + /// V14: an addressee consent that no addressee can have given, or that + /// names a node that is not a predication. It is what the addressee asked + /// for or agreed to, an act, so a referent cannot be its object. + V14ConsentWithoutAddressee, + V14ConsentNotAPredication { id: NodeId }, + /// V15: an addressee consent set by a source that only read wording. + /// A decomposition of text, or imported corpus text, sees "you asked her + /// to" as words; only authoring or game telemetry saw the request. + V15ConsentSource { source: ProvenanceSource }, } impl Refusal { @@ -90,6 +99,10 @@ impl Refusal { | Refusal::V11DuplicateRole { .. } => "V11", Refusal::V12TooManyNodes { .. } => "V12", Refusal::V13SceneWithoutAddressee => "V13", + Refusal::V14ConsentWithoutAddressee | Refusal::V14ConsentNotAPredication { .. } => { + "V14" + } + Refusal::V15ConsentSource { .. } => "V15", } } } @@ -156,6 +169,7 @@ pub(crate) fn admissible( let nodes = check_nodes(packet, &mut out); check_refs(packet, &nodes, &mut out); check_repeats(packet, &mut out); + check_consent(packet, &nodes, &mut out); if inventory.has_revision(packet.inventory_rev) { Inventoried { inv: inventory, @@ -208,6 +222,9 @@ fn check_refs(packet: &Packet, nodes: &BTreeSet, out: &mut Vec) if let Some(a) = &packet.discourse.addressee { need(a, "addressee"); } + if let Some(c) = &packet.discourse.addressee_consent { + need(c, "addressee consent"); + } for (list, at) in node_lists(packet) { for id in list { need(id, at); @@ -215,6 +232,26 @@ fn check_refs(packet: &Packet, nodes: &BTreeSet, out: &mut Vec) } } +/// V14 and V15: the addressee's consent is a typed fact with one origin. A +/// dangling id is V4's, so V14 judges only a node that exists. +fn check_consent(packet: &Packet, nodes: &BTreeSet, out: &mut Vec) { + let Some(consent) = &packet.discourse.addressee_consent else { + return; + }; + if packet.discourse.addressee.is_none() { + out.push(Refusal::V14ConsentWithoutAddressee); + } + if nodes.contains(consent) && !packet.predications.iter().any(|p| &p.id == consent) { + out.push(Refusal::V14ConsentNotAPredication { + id: consent.clone(), + }); + } + match packet.provenance.source { + ProvenanceSource::Authored | ProvenanceSource::GameTelemetry => {} + source => out.push(Refusal::V15ConsentSource { source }), + } +} + fn node_lists(packet: &Packet) -> [(&Vec, &'static str); 3] { [ (&packet.context.participants, "participant"), diff --git a/crates/weksa/tests/common/mod.rs b/crates/weksa/tests/common/mod.rs index d866904..60ba544 100644 --- a/crates/weksa/tests/common/mod.rs +++ b/crates/weksa/tests/common/mod.rs @@ -266,6 +266,7 @@ pub fn good() -> Packet { discourse: Discourse { speech_act: Some(iv(WARN)), addressee: Some(nd("n1")), + addressee_consent: None, features: vec![FeatureValue { feature: iv(HEDGE), value: FeatValue::Grade(grade(300)), diff --git a/crates/weksa/tests/entities.rs b/crates/weksa/tests/entities.rs index 5493943..5f1f7cf 100644 --- a/crates/weksa/tests/entities.rs +++ b/crates/weksa/tests/entities.rs @@ -397,7 +397,7 @@ fn packet_entities_resolve() { #[test] fn packet_schema_v2() { - assert_eq!(SCHEMA_VERSION, 4); + assert_eq!(SCHEMA_VERSION, 5); let mut p = good(); p.schema_version = 2; let Err(refused) = validate(&p, &inventory()) else { diff --git a/crates/weksa/tests/lower.rs b/crates/weksa/tests/lower.rs index 4f1691c..1d8301d 100644 --- a/crates/weksa/tests/lower.rs +++ b/crates/weksa/tests/lower.rs @@ -142,6 +142,7 @@ fn a_packet_carrying_what_the_lowerer_does_not_render_is_refused() { filler, }) }; + let consent = |p: &mut Packet| p.discourse.addressee_consent = Some(nd("n5")); let lone_referent = |p: &mut Packet| { let mut r = p.referents[0].clone(); r.id = nd("n99"); @@ -150,6 +151,7 @@ fn a_packet_carrying_what_the_lowerer_does_not_render_is_refused() { for (what, edit) in [ ("features", &discourse as &dyn Fn(&mut Packet)), ("features", &predication), + ("addressee consent", &consent), ("relations", &relation), ("extensions", &extension), ("no predication", &no_predication), diff --git a/crates/weksa/tests/stores.rs b/crates/weksa/tests/stores.rs index 07cbe0c..cf5ba4a 100644 --- a/crates/weksa/tests/stores.rs +++ b/crates/weksa/tests/stores.rs @@ -989,14 +989,14 @@ fn packet_admission_goes_through_the_validator() { let mut w = world(); let mut store = PacketStore::open(&w.dir.path().join("p.cc")).unwrap(); let mut bad = good(); - bad.schema_version = 5; + bad.schema_version = 6; match store.admit(bad, &w.inventory, &w.corpus, &w.entities) { Err(e @ StoreError::Invalid(_)) => { assert!(e.to_string().contains("V1"), "{e}"); let StoreError::Invalid(r) = e else { unreachable!() }; - assert_eq!(r, vec![Refusal::V1SchemaVersion { found: 5 }]); + assert_eq!(r, vec![Refusal::V1SchemaVersion { found: 6 }]); } other => panic!("{other:?}"), } diff --git a/crates/weksa/tests/validate.rs b/crates/weksa/tests/validate.rs index 3453376..aaa87bd 100644 --- a/crates/weksa/tests/validate.rs +++ b/crates/weksa/tests/validate.rs @@ -683,3 +683,62 @@ fn v11_role_repeat_in_one_predication_refused() { ok.predications.push(second); assert_eq!(refusals(&ok), vec![]); } + +// The addressee's consent: which predication the addressee asked for, set only +// by a source that saw the request. + +fn consenting() -> Packet { + let mut p = good(); + p.discourse.addressee_consent = Some(nd("n2")); + p +} + +#[test] +fn consent_to_a_predication_by_an_authored_or_telemetry_source_passes() { + assert_eq!(refusals(&consenting()), vec![]); + let mut p = consenting(); + p.provenance.source = ProvenanceSource::GameTelemetry; + assert_eq!(refusals(&p), vec![]); +} + +#[test] +fn consent_needs_an_addressee() { + let mut p = consenting(); + p.discourse.addressee = None; + assert_eq!(refusals(&p), vec![Refusal::V14ConsentWithoutAddressee]); +} + +#[test] +fn consent_names_a_predication_not_a_referent() { + let mut p = consenting(); + p.discourse.addressee_consent = Some(nd("n1")); + assert_eq!( + refusals(&p), + vec![Refusal::V14ConsentNotAPredication { id: nd("n1") }] + ); +} + +#[test] +fn consent_to_nothing_is_dangling_only() { + let mut p = consenting(); + p.discourse.addressee_consent = Some(nd("n99")); + assert_eq!( + refusals(&p), + vec![Refusal::V4DanglingRef { + id: nd("n99"), + at: "addressee consent" + }] + ); +} + +#[test] +fn a_source_that_only_read_wording_cannot_set_consent() { + for source in [ + ProvenanceSource::ModelDecomposition, + ProvenanceSource::ImportedCorpus, + ] { + let mut p = consenting(); + p.provenance.source = source; + assert_eq!(refusals(&p), vec![Refusal::V15ConsentSource { source }]); + } +} diff --git a/docs/interlingua-standard.md b/docs/interlingua-standard.md index 6a835dd..ca392d1 100644 --- a/docs/interlingua-standard.md +++ b/docs/interlingua-standard.md @@ -120,7 +120,7 @@ Packet id, schema_version, inventory_rev, kind provenance { source, corpus_item, encoder, confidence } context { setting, medium, participants, audience } - discourse { speech_act, addressee, features } + discourse { speech_act, addressee, addressee_consent, features } referents [ { id, kind, concept, entity, features } ] predications [ { id, concept, frame, roles: [ { role, filler } ], features } ] relations [ { relation, from, to, features } ] @@ -144,8 +144,8 @@ names the rule. - V1: `schema_version` is the version these types define. - V2: at least one of `referents`, `predications`, `relations` is present. - V3: a node id appears once across referents and predications. -- V4: every role filler, relation end, addressee, participant, audience member - and preserved node names a node. +- V4: every role filler, relation end, addressee, addressee consent, + participant, audience member and preserved node names a node. - V5: every inventory id resolves at the packet's `inventory_rev`: introduced at or before it, and not retired at or before it. - V6: an inventory id is of the family its slot takes. @@ -159,6 +159,10 @@ names the rule. predication's roles. - V12: a packet has at most 64 referents, predications and relations together (a calibration constant, capacity and not meaning). +- V14: an `addressee_consent` needs an `addressee` and names a predication. +- V15: an `addressee_consent` is set only by provenance `Authored` or + `GameTelemetry`; a decomposition of text or imported corpus text cannot mint + it. V9 is retired and not renumbered: the form of a key or a grade is its type, and an extension key's family is V6. `PacketStore::admit` returns the lowering view of a @@ -189,7 +193,11 @@ ids. Why the packet is said: `speech_act` is a speech-act id, `addressee` a node id, 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. +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. ### `stance` diff --git a/schemas/cultnet/weksa.interlingua_packet.schema.json b/schemas/cultnet/weksa.interlingua_packet.schema.json index d839203..97442f9 100644 --- a/schemas/cultnet/weksa.interlingua_packet.schema.json +++ b/schemas/cultnet/weksa.interlingua_packet.schema.json @@ -164,6 +164,17 @@ } ] }, + "addressee_consent": { + "description": "The predication the addressee asked for or agreed to. A fact about what\nthe addressee did, never about how the line sounds, so it is no feature:\nonly a source that saw the request may set it (V15), it needs an\naddressee (V14) and names a predication (V14), and a lowering that\nspeaks for the addressee's request (\"you asked her to\") reads it here\nand refuses without it.", + "anyOf": [ + { + "$ref": "#/$defs/NodeId" + }, + { + "type": "null" + } + ] + }, "features": { "type": "array", "items": { -- 2.47.3