Packet carries an addressee-consent fact (schema v5, V14, V15) #14

Merged
weksa merged 1 commit from weksa-packet/addressee-consent into main 2026-11-09 09:11:54 +00:00
10 changed files with 139 additions and 10 deletions

View file

@ -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");
}

View file

@ -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>`, `k` a canonical
/// decimal 1..=9999.
@ -261,6 +261,13 @@ pub struct Discourse {
/// Speech-act id.
pub speech_act: Option<InvId>,
pub addressee: Option<NodeId>,
/// 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<NodeId>,
pub features: Vec<FeatureValue>,
}
@ -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![

View file

@ -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<NodeId>, out: &mut Vec<Refusal>)
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<NodeId>, out: &mut Vec<Refusal>)
}
}
/// 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<NodeId>, out: &mut Vec<Refusal>) {
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<NodeId>, &'static str); 3] {
[
(&packet.context.participants, "participant"),

View file

@ -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)),

View file

@ -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 {

View file

@ -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),

View file

@ -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:?}"),
}

View file

@ -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 }]);
}
}

View file

@ -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`

View file

@ -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": {