V18: validate refuses a request_intervention predication the addressee consent does not name #94

Merged
weksa merged 3 commits from weksa-lowering/request-frame-consent into main 2026-11-14 04:21:50 +00:00
5 changed files with 125 additions and 9 deletions

View file

@ -4,7 +4,7 @@
//! lowerer sees, comes from `PacketStore::admit` alone, which builds it from
//! the admitted packet and its corpus item through the crate-private
//! `LoweringInput::of`. The stance is checked like every feature
//! list, in slot `Stance`. The rules are V1..V8, V10..V12 and V14..V17: the
//! list, in slot `Stance`. The rules are V1..V8, V10..V12 and V14..V18: the
//! shape of an id or a grade is its type's parse, not a rule, so V9 is retired
//! and not renumbered; V13 is retired too. [`LoweringInput::with_scene`] reads the
//! scene of the view's own item group from the speaker-state store, and
@ -120,6 +120,12 @@ pub enum Refusal {
V17AddresseeNotAReferent {
id: NodeId,
},
/// V18: a predication whose frame is the inventory's `request_intervention`,
/// with no `addressee_consent` naming it. It says what the addressee did, so
/// only the packet's own record may license it.
V18RequestWithoutConsent {
predication: NodeId,
},
}
impl Refusal {
@ -144,6 +150,7 @@ impl Refusal {
Refusal::V15ConsentSource { .. } => "V15",
Refusal::V16SpeakerNotInScene => "V16",
Refusal::V17AddresseeNotAReferent { .. } => "V17",
Refusal::V18RequestWithoutConsent { .. } => "V18",
}
}
}
@ -173,6 +180,9 @@ impl fmt::Display for Refusal {
"the scene does not cover the speaker of every live item of its group"
}
Refusal::V17AddresseeNotAReferent { .. } => "the addressee names no referent",
Refusal::V18RequestWithoutConsent { .. } => {
"a request frame needs the addressee's consent naming it"
}
};
write!(f, "{}: {what}", self.rule())
}
@ -221,6 +231,7 @@ pub fn validate(packet: &Packet, inventory: &Inventory) -> Result<(), Vec<Refusa
out: &mut out,
}
.check(packet);
check_requests(packet, inventory, &mut out);
} else {
out.push(Refusal::V10UnknownRevision {
rev: packet.inventory_rev,
@ -303,6 +314,25 @@ fn check_consent(packet: &Packet, nodes: &BTreeSet<NodeId>, out: &mut Vec<Refusa
}
}
/// The gloss of the inventory frame that reports an addressee's request.
const REQUEST_FRAME_GLOSS: &str = "request_intervention";
/// V18: a predication whose frame, resolved at the packet's revision, is the
/// request frame is named by `addressee_consent`. An unresolved frame is V5's.
fn check_requests(packet: &Packet, inventory: &Inventory, out: &mut Vec<Refusal>) {
for p in &packet.predications {
if inventory
.resolve(&p.frame, packet.inventory_rev)
.is_some_and(|e| e.gloss == REQUEST_FRAME_GLOSS)
&& packet.discourse.addressee_consent.as_ref() != Some(&p.id)
{
out.push(Refusal::V18RequestWithoutConsent {
predication: p.id.clone(),
});
}
}
}
fn node_lists(packet: &Packet) -> [(&Vec<NodeId>, &'static str); 3] {
[
(&packet.context.participants, "participant"),

View file

@ -593,6 +593,41 @@ mod lowering {
assert!(Prompt::lower(&read, &item, &away(), "en-US", &inv, &held, &empty).is_ok());
}
/// The packet the encoder emits for a request: frame:3, no consent, source
/// ModelDecomposition. Admission refuses it, so no lowering view exists.
#[test]
fn an_encoder_request_never_reaches_a_lowerer() {
let cast = InventoryStore::open(&data("interlingua/inventory.cc")).unwrap();
let entities = EntityStore::open(&data("entity/entity.cc")).unwrap();
let corpus = CorpusStore::open(&data("corpus/corpus.cc")).unwrap();
let dir = tempfile::tempdir().unwrap();
let mut store = PacketStore::open(&dir.path().join("p.cc")).unwrap();
let mut packet = proof_packet();
packet.provenance.source = weksa::packet::ProvenanceSource::ModelDecomposition;
packet.predications[0].frame = weksa::InvId::parse("frame:3").unwrap();
match store.admit(packet.clone(), &cast, &corpus, &entities) {
Err(weksa::StoreError::Invalid(r)) => assert_eq!(
r,
vec![weksa::Refusal::V18RequestWithoutConsent {
predication: weksa::packet::NodeId::parse("n4").unwrap()
}]
),
other => panic!("expected V18, got {:?}", other.map(|_| ())),
}
// Not blanket: telemetry that records the consent admits and lowers.
packet.provenance.source = weksa::packet::ProvenanceSource::GameTelemetry;
packet.discourse.addressee_consent = Some(weksa::packet::NodeId::parse("n4").unwrap());
drop(store);
let view = admitted(packet, &cast, "corpus/corpus.cc", &entities);
let item = CorpusItemId::parse("item:nibu:1").unwrap();
let held = Held(vec![nibu(&[("en-US", &["Nibu"])])]);
let empty = weksa::Ontology::empty();
assert!(
Prompt::lower(&view, &item, &away(), "en-US", &cast.inventory().unwrap(), &held, &empty)
.is_ok()
);
}
fn dao(index: usize) -> (LoweringInput, Inventory, EntityStore, CorpusItemId) {
let inv = InventoryStore::open(&data("daodejing/inventory.cc")).unwrap();
let entities = EntityStore::open(&data("daodejing/entity.cc")).unwrap();

View file

@ -104,6 +104,7 @@ fn refusal_display_names_the_rule_and_no_identifier() {
},
Refusal::V16SpeakerNotInScene,
Refusal::V17AddresseeNotAReferent { id: node() },
Refusal::V18RequestWithoutConsent { predication: node() },
];
assert_eq!(
Refusal::V16SpeakerNotInScene.to_string(),

View file

@ -740,6 +740,53 @@ fn a_source_that_only_read_wording_cannot_set_consent() {
}
}
// V18: a request frame is licensed by the packet's own consent record.
/// The committed inventory, where `frame:3` is `request_intervention`.
fn cast_refusals(p: &Packet) -> Vec<Refusal> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data/interlingua/inventory.cc");
let inv = weksa::InventoryStore::open(&path).unwrap().inventory().unwrap();
validate(p, &inv).err().unwrap_or_default()
}
fn requesting() -> Packet {
let mut p = proof_packet();
p.predications[0].frame = iv("frame:3");
p.discourse.addressee_consent = Some(nd("n4"));
p
}
#[test]
fn a_request_frame_needs_consent_naming_its_predication() {
assert_eq!(cast_refusals(&requesting()), vec![]);
let mut p = requesting();
p.discourse.addressee_consent = None;
assert_eq!(
cast_refusals(&p),
vec![Refusal::V18RequestWithoutConsent { predication: nd("n4") }]
);
p.discourse.addressee_consent = Some(nd("n5"));
assert_eq!(
cast_refusals(&p),
vec![Refusal::V18RequestWithoutConsent { predication: nd("n4") }]
);
let mut p = requesting();
p.provenance.source = ProvenanceSource::GameTelemetry;
assert_eq!(cast_refusals(&p), vec![]);
}
#[test]
fn model_source_consent_is_v15_alone() {
let mut p = requesting();
p.provenance.source = ProvenanceSource::ModelDecomposition;
assert_eq!(
cast_refusals(&p),
vec![Refusal::V15ConsentSource {
source: ProvenanceSource::ModelDecomposition
}]
);
}
// V17: the addressee is someone spoken to, a referent.
#[test]

View file

@ -164,6 +164,7 @@ names the rule.
`GameTelemetry`; a decomposition of text or imported corpus text cannot mint
it.
- V17: an `addressee` names a referent, not a predication.
- V18: a predication whose frame is the inventory's `request_intervention` is named by `addressee_consent`.
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
@ -197,14 +198,16 @@ 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. `V14` and `V15` check the field: it may be set only by authoring or game
telemetry, and it must name a predication of the packet. Open gap: `frame:3`
(`request_intervention`) is in the committed inventories (`data/interlingua`,
`data/original`, `data/daodejing`) and seeded at `inventory.rs:518`. No
committed packet uses it, but the encoder sees it, and `V14` and `V15` check only
the consent field, so a model-encoded `frame:3` predication with a null
`addressee_consent` reaches `Prompt::lower` and nothing refuses it. The retired
gate covered only the deleted `lower()`. Follow-up
`table-retirement-consent-gap` owns the gap.
telemetry, and it must name a predication of the packet. `V18` refuses a predication whose frame resolves, at the packet's revision, to
the gloss `request_intervention` (`frame:3` in the committed inventories)
unless `addressee_consent` names that predication. The encoder can never set
consent (`V15`), so an encoder-produced request frame is always refused.
Residual gaps: `V18` keys on that one gloss, so another frame that asserts the
addressee's act (for example a `frame:2` intervention on the original
inventory) or a renamed gloss is not covered; consent is one field, so two
request predications in one packet cannot validate; and the check that the
asker is the addressee was not kept. Follow-up
`weksa-lowering:follow_up:request-frame-consent-residual` owns them.
### `stance`