Lowering input: the speaker's relation to the addressee (Scene slot, with_scene) #8
5 changed files with 270 additions and 8 deletions
|
|
@ -189,6 +189,9 @@ pub enum Slot {
|
|||
Predication,
|
||||
Relation,
|
||||
Stance,
|
||||
/// The relation of the speaker to the addressee in the scene; see
|
||||
/// `LoweringInput::with_scene`.
|
||||
Scene,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
|
|
@ -508,3 +511,49 @@ pub fn proof_revision() -> (InventoryRevision, Vec<InventoryEntry>) {
|
|||
entries,
|
||||
)
|
||||
}
|
||||
|
||||
/// Revision 3: the speaker's relation to the addressee, features of slot
|
||||
/// `Scene`. Hand-seeded like [`proof_revision`] until the loop's minting round
|
||||
/// and cut `speaker-scene` own the Scene vocabulary. `relative_status` is the
|
||||
/// speaker's standing against the addressee's, not a register word:
|
||||
/// what it selects is each language's ontology's business.
|
||||
pub fn scene_revision() -> (InventoryRevision, Vec<InventoryEntry>) {
|
||||
let taken = seed()
|
||||
.1
|
||||
.iter()
|
||||
.filter(|e| e.family() == Family::Feature)
|
||||
.count() as u32;
|
||||
let labels = ["below", "level", "above"]
|
||||
.map(|l| OptionLabel::parse(l).expect("seed labels are well formed"));
|
||||
let relative_status = ValueKind::Options(
|
||||
OptionSet::new(labels.to_vec()).expect("seed option sets are well formed"),
|
||||
);
|
||||
let mut entries = Vec::new();
|
||||
for (i, (gloss, kind)) in [
|
||||
("relative_status", relative_status),
|
||||
("intimacy", ValueKind::Grade),
|
||||
("in_group", ValueKind::Flag),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
entries.push(InventoryEntry {
|
||||
id: Family::Feature.id(NonZeroU32::MIN.saturating_add(taken + i as u32)),
|
||||
gloss: gloss.to_string(),
|
||||
value_kind: Some(kind),
|
||||
applies_to: vec![Slot::Scene],
|
||||
introduced_rev: 3,
|
||||
retired_rev: None,
|
||||
});
|
||||
}
|
||||
let changes = entries.iter().map(|e| Change::Add(e.id)).collect();
|
||||
(
|
||||
InventoryRevision {
|
||||
n: 3,
|
||||
parent: Some(2),
|
||||
changes,
|
||||
frozen: false,
|
||||
},
|
||||
entries,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
//! the crate-private `admissible`. The stance is checked like every feature
|
||||
//! list, in slot `Stance`. The rules are V1..V8 and V10..V12: the shape of an id
|
||||
//! or a grade is its type's parse, not a rule, so V9 is retired and not
|
||||
//! renumbered.
|
||||
//! renumbered. V13 guards the one other way features reach a lowering view:
|
||||
//! [`LoweringInput::with_scene`].
|
||||
//!
|
||||
//! `validate` owns form and capacity and makes no leak decision. A packet's one
|
||||
//! free text is its trace (`notes` and `rejected_readings`): `validate` does not
|
||||
|
|
@ -66,6 +67,10 @@ pub enum Refusal {
|
|||
V11DuplicateRole { role: InvId, predication: NodeId },
|
||||
/// V12: more referents, predications and relations than `Packet::MAX_NODES`.
|
||||
V12TooManyNodes { count: usize, max: usize },
|
||||
/// V13: scene features were given for a packet with no addressee. They
|
||||
/// state the speaker's relation to the addressee, so with none they
|
||||
/// describe nobody.
|
||||
V13SceneWithoutAddressee,
|
||||
}
|
||||
|
||||
impl Refusal {
|
||||
|
|
@ -84,6 +89,7 @@ impl Refusal {
|
|||
| Refusal::V11DuplicateNodeRef { .. }
|
||||
| Refusal::V11DuplicateRole { .. } => "V11",
|
||||
Refusal::V12TooManyNodes { .. } => "V12",
|
||||
Refusal::V13SceneWithoutAddressee => "V13",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -437,6 +443,11 @@ fn fits(kind: Option<&ValueKind>, value: &FeatValue) -> bool {
|
|||
/// fn read(l: weksa::LoweringInput) { let _ = l.stance; }
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// fn read(l: weksa::LoweringInput) { let _ = l.scene; }
|
||||
/// ```
|
||||
///
|
||||
/// The constructor is private: a call from outside the crate compiles only if
|
||||
/// `LoweringInput::of` is `pub`. The first block is the same code without the
|
||||
/// call, so the second fails for that call and nothing else.
|
||||
|
|
@ -474,6 +485,7 @@ pub struct LoweringInput {
|
|||
constraints: Constraints,
|
||||
extensions: Vec<Extension>,
|
||||
stance: Stance,
|
||||
scene: Vec<FeatureValue>,
|
||||
}
|
||||
|
||||
impl LoweringInput {
|
||||
|
|
@ -489,9 +501,44 @@ impl LoweringInput {
|
|||
constraints: p.constraints.clone(),
|
||||
extensions: p.extensions.clone(),
|
||||
stance: p.stance.clone(),
|
||||
scene: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the speaker's relation to the addressee: feature values of slot
|
||||
/// `Scene`, checked like every feature list (V5 to V8, V11) at this view's
|
||||
/// inventory revision. They state a relation, so the packet must name an
|
||||
/// addressee (V13). Typed ids and grades only, never wording, so a scene
|
||||
/// carries no text to the lowerer. The scene is not part of the packet: a
|
||||
/// scene document supplies it, and replacing it replaces the earlier one.
|
||||
pub fn with_scene(
|
||||
mut self,
|
||||
inventory: &Inventory,
|
||||
scene: Vec<FeatureValue>,
|
||||
) -> Result<Self, Vec<Refusal>> {
|
||||
let mut out = Vec::new();
|
||||
if !inventory.has_revision(self.inventory_rev) {
|
||||
out.push(Refusal::V10UnknownRevision {
|
||||
rev: self.inventory_rev,
|
||||
});
|
||||
} else {
|
||||
Inventoried {
|
||||
inv: inventory,
|
||||
rev: self.inventory_rev,
|
||||
out: &mut out,
|
||||
}
|
||||
.features(&scene, Slot::Scene);
|
||||
}
|
||||
if !scene.is_empty() && self.discourse.addressee.is_none() {
|
||||
out.push(Refusal::V13SceneWithoutAddressee);
|
||||
}
|
||||
if !out.is_empty() {
|
||||
return Err(out);
|
||||
}
|
||||
self.scene = scene;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn inventory_rev(&self) -> u32 {
|
||||
self.inventory_rev
|
||||
}
|
||||
|
|
@ -522,4 +569,8 @@ impl LoweringInput {
|
|||
pub fn stance(&self) -> &Stance {
|
||||
&self.stance
|
||||
}
|
||||
/// The speaker's relation to the addressee; empty until `with_scene`.
|
||||
pub fn scene(&self) -> &[FeatureValue] {
|
||||
&self.scene
|
||||
}
|
||||
}
|
||||
|
|
|
|||
152
crates/weksa/tests/scene.rs
Normal file
152
crates/weksa/tests/scene.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! The speaker's relation to the addressee as a lowering input: typed feature
|
||||
//! values of slot `Scene`, checked against the inventory at the view's revision,
|
||||
//! never wording, and only for a packet that names an addressee.
|
||||
|
||||
mod common;
|
||||
use std::path::Path;
|
||||
|
||||
use common::{grade, iv, lab};
|
||||
use weksa::inventory::{proof_revision, scene_revision, seed};
|
||||
use weksa::packet::{FeatValue, FeatureValue, Packet, proof_packet};
|
||||
use weksa::{
|
||||
CorpusStore, EntityStore, Inventory, InventoryStore, LoweringInput, PacketStore, Refusal, lower,
|
||||
};
|
||||
|
||||
fn store() -> (InventoryStore, Inventory, tempfile::TempDir) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = InventoryStore::open(&dir.path().join("i.cc")).unwrap();
|
||||
for (rev, entries) in [seed(), proof_revision(), scene_revision()] {
|
||||
store.admit_revision(rev, entries).unwrap();
|
||||
}
|
||||
let inv = store.inventory().unwrap();
|
||||
(store, inv, dir)
|
||||
}
|
||||
|
||||
fn admitted(packet: &Packet, inventory: &InventoryStore) -> LoweringInput {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let corpus = CorpusStore::open(&root.join("data/corpus/corpus.cc")).unwrap();
|
||||
let entities = EntityStore::open(&root.join("data/entity/entity.cc")).unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
PacketStore::open(&dir.path().join("p.cc"))
|
||||
.unwrap()
|
||||
.admit(packet.clone(), inventory, &corpus, &entities)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn at_rev(rev: u32) -> Packet {
|
||||
let mut p = proof_packet();
|
||||
p.inventory_rev = rev;
|
||||
p
|
||||
}
|
||||
|
||||
fn feat(id: &str, value: FeatValue) -> FeatureValue {
|
||||
FeatureValue {
|
||||
feature: iv(id),
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
/// `scene_revision` mints the three Scene features after the seed's features.
|
||||
fn status(inv: &Inventory) -> String {
|
||||
inv.entries()
|
||||
.find(|e| e.gloss == "relative_status")
|
||||
.unwrap()
|
||||
.id
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn id_of(inv: &Inventory, gloss: &str) -> String {
|
||||
inv.entries()
|
||||
.find(|e| e.gloss == gloss)
|
||||
.unwrap()
|
||||
.id
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn rules(r: Result<LoweringInput, Vec<Refusal>>) -> Vec<&'static str> {
|
||||
r.unwrap_err().iter().map(Refusal::rule).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scene_relation_reaches_the_lowering_view_and_changes_no_english() {
|
||||
let (store, inv, _dir) = store();
|
||||
let plain = admitted(&at_rev(3), &store);
|
||||
assert!(plain.scene().is_empty());
|
||||
let scene = vec![
|
||||
feat(&status(&inv), FeatValue::Option(lab("above"))),
|
||||
feat(&id_of(&inv, "intimacy"), FeatValue::Grade(grade(200))),
|
||||
feat(&id_of(&inv, "in_group"), FeatValue::Flag(false)),
|
||||
];
|
||||
let with = plain.clone().with_scene(&inv, scene.clone()).unwrap();
|
||||
assert_eq!(with.scene(), scene.as_slice());
|
||||
assert_eq!(lower(&with, &inv).unwrap(), lower(&plain, &inv).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_relation_needs_an_addressee() {
|
||||
let (store, inv, _dir) = store();
|
||||
let mut p = at_rev(3);
|
||||
p.discourse.addressee = None;
|
||||
let view = admitted(&p, &store);
|
||||
let scene = vec![feat(&id_of(&inv, "in_group"), FeatValue::Flag(true))];
|
||||
assert_eq!(rules(view.clone().with_scene(&inv, scene)), ["V13"]);
|
||||
assert!(view.with_scene(&inv, vec![]).unwrap().scene().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_relation_value_is_checked_like_any_feature() {
|
||||
let (store, inv, _dir) = store();
|
||||
let view = admitted(&at_rev(3), &store);
|
||||
let status = status(&inv);
|
||||
let intimacy = id_of(&inv, "intimacy");
|
||||
let take = |scene| view.clone().with_scene(&inv, scene);
|
||||
// A label the feature does not declare.
|
||||
assert_eq!(
|
||||
rules(take(vec![feat(&status, FeatValue::Option(lab("lordly")))])),
|
||||
["V7"]
|
||||
);
|
||||
// A value of the wrong kind.
|
||||
assert_eq!(
|
||||
rules(take(vec![feat(&intimacy, FeatValue::Flag(true))])),
|
||||
["V7"]
|
||||
);
|
||||
// A feature declared for another slot: seed feature 1 sits in Predication.
|
||||
assert_eq!(
|
||||
rules(take(vec![feat(
|
||||
"feature:1",
|
||||
FeatValue::Option(lab("negative"))
|
||||
)])),
|
||||
["V8"]
|
||||
);
|
||||
// An id that is not a feature at all.
|
||||
assert_eq!(
|
||||
rules(take(vec![feat("concept:3", FeatValue::Flag(true))])),
|
||||
["V6"]
|
||||
);
|
||||
// Twice in one list.
|
||||
let twice = vec![
|
||||
feat(&intimacy, FeatValue::Grade(grade(1))),
|
||||
feat(&intimacy, FeatValue::Grade(grade(2))),
|
||||
];
|
||||
assert_eq!(rules(take(twice)), ["V11"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_view_of_a_revision_before_the_scene_vocabulary_cannot_carry_one() {
|
||||
let (store, inv, _dir) = store();
|
||||
// Revision 2 predates the Scene features, so their ids do not resolve there.
|
||||
let view = admitted(&at_rev(2), &store);
|
||||
let scene = vec![feat(&id_of(&inv, "intimacy"), FeatValue::Grade(grade(1)))];
|
||||
assert_eq!(rules(view.with_scene(&inv, scene)), ["V5"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stance_feature_is_not_a_scene_feature() {
|
||||
let (store, inv, _dir) = store();
|
||||
let view = admitted(&at_rev(3), &store);
|
||||
let anger = id_of(&inv, "anger");
|
||||
assert_eq!(
|
||||
rules(view.with_scene(&inv, vec![feat(&anger, FeatValue::Grade(grade(1)))])),
|
||||
["V8"]
|
||||
);
|
||||
}
|
||||
|
|
@ -394,6 +394,7 @@ One line per cut; specs exist for the cuts about to run (2d and 4, siblings on `
|
|||
2d. `entity-door-lows`: invisible format characters refused in entity forms (ZWJ, ZWNJ, CGJ and variation selectors kept); the form length bound per question `form-word-bound` (recommended: bytes only); attested origins (`Cast { persona, fixture }`, `Live { persona }`) and `CastAuthored.author`; empty provenance strings refused at the store; S6 under NFKC plus case fold and no-alphanumeric texts refused; `next_key` refuses at `u32::MAX`; CLI flags that do not apply refuse and no error echoes a value. Owns follow-ups `entity-door-lows` and `s6-unicode-normal-form`. (spec r1)
|
||||
3b. `lowerer-first-cut` (follow-up `golf-loop-lowerer-cut`): the section above. `lower`, `weksa lower`, `weksa score`, `weksa packet proof`, inventory rev 2 for `nibu-proof-001`.
|
||||
4. `speaker-state`: packet `stance` (felt affect and restraint, slot `Stance`, packet schema v4); formality, warmth and intensity out of the seed and the standard; only `PacketStore::admit` yields a `LoweringInput`, and it refuses packets for withdrawn items (follow-up `lowering-input-from-packet-store`). (spec r1)
|
||||
4a. `speaker-relation-input`: slot `Scene` and its first seed features (`relative_status`, `intimacy`, `in_group`; inventory revision 3, `scene_revision`); `LoweringInput::with_scene` takes the speaker's relation to the addressee as typed `Scene` feature values, validated like every feature list, refused without an addressee (V13). The English lowerer does not render it. 4b's `weksa.scene` document is its future supplier; no store, document or CLI yet. (PR for follow-up `chat-s3352-3352`)
|
||||
4b. `speaker-scene`: slots `Speaker` and `Scene` with their seed features; `weksa.speaker` (`speaker:<entity-slug>@<n>`) and `weksa.scene` (`scene:<group>@<n>`, participants and ordered-pair relations) documents with `Annotator` `Operator` and `Model { family, generator }`, validated by the packet's shared feature check, append-only per key; their stores, `weksa speaker` and `weksa scene` commands, published schemas; Model-annotated cast speakers and scenes for the committed items. Waits on 2d and 4; `pd-speaker-annotation` blocks only the public-domain annotator.
|
||||
5. `language-ontology`: `weksa.community` from `data/cultural-ontology/` (owns follow-up `edda-referent-yaml-parse`), `weksa.ontology_entry` for en-US and ja-JP from `data/target-language-ontology/` (YAML deleted), speaker communities, deterministic `select`, the lowering view (packet without trace, speaker, scene, selected entries). Waits on 4.
|
||||
6. `pd-importer`: public-domain play and fiction dialogue into the corpus with speaker, scene group, edition provenance, and Speaker and Scene annotations by the ruled annotator. Waits on 4, follow-up `gutenberg-terms` and question `pd-speaker-annotation`.
|
||||
|
|
|
|||
|
|
@ -69,13 +69,22 @@
|
|||
},
|
||||
"Slot": {
|
||||
"description": "Where a feature may sit in a packet.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Discourse",
|
||||
"Referent",
|
||||
"Predication",
|
||||
"Relation",
|
||||
"Stance"
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Discourse",
|
||||
"Referent",
|
||||
"Predication",
|
||||
"Relation",
|
||||
"Stance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "The relation of the speaker to the addressee in the scene; see\n`LoweringInput::with_scene`.",
|
||||
"type": "string",
|
||||
"const": "Scene"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ValueKind": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue