speaker-scene: weksa.speaker and weksa.scene documents, with_scene reads the store #26
19 changed files with 1895 additions and 216 deletions
|
|
@ -189,9 +189,11 @@ pub enum Slot {
|
|||
Predication,
|
||||
Relation,
|
||||
Stance,
|
||||
/// The relation of the speaker to the addressee in the scene; see
|
||||
/// `LoweringInput::with_scene`.
|
||||
/// The speaker's relation to one party of the scene, held by `weksa.scene`
|
||||
/// documents; see `LoweringInput::with_scene`.
|
||||
Scene,
|
||||
/// The speaker's stable axes, held by `weksa.speaker` documents.
|
||||
Speaker,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
|
|
@ -228,10 +230,21 @@ impl InventoryEntry {
|
|||
}
|
||||
|
||||
/// Whether the entry's shape matches its family: features carry a value kind
|
||||
/// and slots, extensions a value kind and no slots, nothing else either.
|
||||
/// and slots, extensions a value kind and no slots, nothing else either. A
|
||||
/// feature of slot `Stance`, `Speaker` or `Scene` sits in that slot alone: each
|
||||
/// speaker-state layer has one writer and one lifetime, so its features sit
|
||||
/// nowhere else.
|
||||
pub fn well_formed(&self) -> bool {
|
||||
match self.family() {
|
||||
Family::Feature => self.value_kind.is_some() && !self.applies_to.is_empty(),
|
||||
Family::Feature => {
|
||||
self.value_kind.is_some()
|
||||
&& !self.applies_to.is_empty()
|
||||
&& (self.applies_to.len() == 1
|
||||
|| !self
|
||||
.applies_to
|
||||
.iter()
|
||||
.any(|s| matches!(s, Slot::Stance | Slot::Speaker | Slot::Scene)))
|
||||
}
|
||||
Family::Extension => self.value_kind.is_some() && self.applies_to.is_empty(),
|
||||
_ => self.value_kind.is_none() && self.applies_to.is_empty(),
|
||||
}
|
||||
|
|
@ -523,28 +536,49 @@ pub fn proof_revision() -> (InventoryRevision, Vec<InventoryEntry>) {
|
|||
)
|
||||
}
|
||||
|
||||
/// 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. It mints the three
|
||||
/// Scene features after the seed's features.
|
||||
pub fn scene_revision() -> (InventoryRevision, Vec<InventoryEntry>) {
|
||||
/// Revision 3: the speaker-state vocabulary. First the Scene features, the
|
||||
/// speaker's relation to one party of a scene (slot `Scene`): `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. Then the Speaker features
|
||||
/// (slot `Speaker`): `socioeconomic_standing` is standing in the speaker's own
|
||||
/// society, 0 lowest; `education` is formal schooling against that society;
|
||||
/// `age_cohort` is a life stage. Origin and community are not features; they
|
||||
/// arrive as community ids with cut `language-ontology`. Hand-seeded like
|
||||
/// [`proof_revision`] until the loop's minting round owns the vocabulary; the
|
||||
/// features take the ids after the seed's.
|
||||
pub fn speaker_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 options = |labels: &[&str]| {
|
||||
ValueKind::Options(
|
||||
OptionSet::new(
|
||||
labels
|
||||
.iter()
|
||||
.map(|l| OptionLabel::parse(l).expect("seed labels are well formed"))
|
||||
.collect(),
|
||||
)
|
||||
.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),
|
||||
for (i, (gloss, kind, slot)) in [
|
||||
(
|
||||
"relative_status",
|
||||
options(&["below", "level", "above"]),
|
||||
Slot::Scene,
|
||||
),
|
||||
("intimacy", ValueKind::Grade, Slot::Scene),
|
||||
("in_group", ValueKind::Flag, Slot::Scene),
|
||||
("socioeconomic_standing", ValueKind::Grade, Slot::Speaker),
|
||||
("education", ValueKind::Grade, Slot::Speaker),
|
||||
(
|
||||
"age_cohort",
|
||||
options(&["child", "adolescent", "young_adult", "adult", "elder"]),
|
||||
Slot::Speaker,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
|
|
@ -553,7 +587,7 @@ pub fn scene_revision() -> (InventoryRevision, Vec<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],
|
||||
applies_to: vec![slot],
|
||||
introduced_rev: 3,
|
||||
retired_rev: None,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ pub mod packet;
|
|||
pub mod phones;
|
||||
pub mod schema;
|
||||
pub mod score;
|
||||
pub mod speaker;
|
||||
pub mod split;
|
||||
pub mod store;
|
||||
pub mod validate;
|
||||
|
|
@ -26,14 +27,15 @@ pub use inventory::{
|
|||
Slot, ValueKind,
|
||||
};
|
||||
pub use lower::{EntityForms, LowerError, PhoneLine, TARGET_LANGUAGE, lower, phones, pronounce};
|
||||
pub use phones::{Phone, PhoneInventory, SegmentError, Stress};
|
||||
pub use packet::{
|
||||
EncoderId, EntityId, Grade, NodeId, Packet, PacketId, PacketKind, SCHEMA_VERSION, Stance,
|
||||
};
|
||||
pub use phones::{Phone, PhoneInventory, SegmentError, Stress};
|
||||
pub use score::{Embedder, ItemScore, OllamaEmbedder, Scorer, chrf};
|
||||
pub use speaker::{Annotator, PairRelation, Party, Scene, Speaker};
|
||||
pub use split::{Split, SplitScheme};
|
||||
pub use store::{
|
||||
AddOutcome, CorpusStore, EntityBound, EntityStore, InventoryStore, PacketStore, StoreError,
|
||||
StoreRefusal,
|
||||
AddOutcome, CorpusStore, EntityBound, EntityStore, InventoryStore, PacketStore,
|
||||
SpeakerStateStore, StoreError, StoreRefusal,
|
||||
};
|
||||
pub use validate::{LoweringInput, Refusal, validate};
|
||||
pub use validate::{LoweringInput, Refusal, SceneRead, validate};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use crate::corpus::CorpusItem;
|
|||
use crate::entity::Entity;
|
||||
use crate::inventory::{InventoryEntry, InventoryRevision};
|
||||
use crate::packet::Packet;
|
||||
use crate::speaker::{Scene, Speaker};
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
use schemars::{JsonSchema, schema_for};
|
||||
|
||||
|
|
@ -21,5 +22,7 @@ pub fn published() -> Vec<(String, String)> {
|
|||
published_for::<InventoryRevision>(),
|
||||
published_for::<CorpusItem>(),
|
||||
published_for::<Entity>(),
|
||||
published_for::<Speaker>(),
|
||||
published_for::<Scene>(),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
120
crates/weksa/src/speaker.rs
Normal file
120
crates/weksa/src/speaker.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
//! The speaker-state documents: a speaker's stable axes and a scene's relations.
|
||||
//! Both are append-only revisions under one key per revision and hold typed
|
||||
//! feature values only, never text (ruling `leak-owned-by-flow-and-scorer-3`).
|
||||
//! `SpeakerStateStore` is their only writer; `LoweringInput::with_scene` is the
|
||||
//! only reader that reaches a lowering view.
|
||||
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::corpus::ModelFamily;
|
||||
use crate::packet::{EntityId, FeatureValue};
|
||||
|
||||
/// The most participants one scene holds.
|
||||
pub const MAX_PARTICIPANTS: usize = 64;
|
||||
|
||||
/// Who annotated a document. A model's generator is an attestation string, held
|
||||
/// to the same form as every provenance string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Annotator {
|
||||
Operator,
|
||||
Model {
|
||||
family: ModelFamily,
|
||||
generator: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// The speaker's stable axes. Key `speaker:<entity slug>@<n>`; a revision
|
||||
/// appends and never edits.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, JsonSchema, DatabaseEntry)]
|
||||
#[cultcache(type = "weksa.speaker")]
|
||||
pub struct Speaker {
|
||||
#[cultcache(key = 0)]
|
||||
pub entity: EntityId,
|
||||
/// The revision, from 1.
|
||||
#[cultcache(key = 1)]
|
||||
pub n: u32,
|
||||
#[cultcache(key = 2)]
|
||||
pub inventory_rev: u32,
|
||||
/// Feature values of slot `Speaker`.
|
||||
#[cultcache(key = 3)]
|
||||
pub features: Vec<FeatureValue>,
|
||||
#[cultcache(key = 4)]
|
||||
pub annotator: Annotator,
|
||||
}
|
||||
|
||||
/// The other party of a relation. `Unnamed` is an addressee the scene does not
|
||||
/// name, such as the player or a crowd.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Party {
|
||||
Named(EntityId),
|
||||
Unnamed,
|
||||
}
|
||||
|
||||
/// The speaker's relation to one party: feature values of slot `Scene`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PairRelation {
|
||||
pub from: EntityId,
|
||||
pub to: Party,
|
||||
pub features: Vec<FeatureValue>,
|
||||
}
|
||||
|
||||
/// A scene: its participants and the relations between them. Key
|
||||
/// `scene:<group>@<n>`; a revision appends and never edits.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, JsonSchema, DatabaseEntry)]
|
||||
#[cultcache(type = "weksa.scene")]
|
||||
pub struct Scene {
|
||||
/// The corpus split group of the scene, a slug.
|
||||
#[cultcache(key = 0)]
|
||||
pub group: String,
|
||||
/// The revision, from 1.
|
||||
#[cultcache(key = 1)]
|
||||
pub n: u32,
|
||||
#[cultcache(key = 2)]
|
||||
pub inventory_rev: u32,
|
||||
#[cultcache(key = 3)]
|
||||
pub participants: Vec<EntityId>,
|
||||
#[cultcache(key = 4)]
|
||||
pub relations: Vec<PairRelation>,
|
||||
#[cultcache(key = 5)]
|
||||
pub annotator: Annotator,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
/// The features of the relation from `speaker` to the named `addressee`, or
|
||||
/// to `Unnamed` when there is none; `None` when the scene holds no such
|
||||
/// relation (a relation with no features is `Some(&[])`). A named
|
||||
/// addressee the scene lacks never falls back to `Unnamed`.
|
||||
pub fn relation(
|
||||
&self,
|
||||
speaker: &EntityId,
|
||||
addressee: Option<&EntityId>,
|
||||
) -> Option<&[FeatureValue]> {
|
||||
self.relations
|
||||
.iter()
|
||||
.find(|r| {
|
||||
&r.from == speaker
|
||||
&& match (&r.to, addressee) {
|
||||
(Party::Named(a), Some(b)) => a == b,
|
||||
(Party::Unnamed, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
.map(|r| r.features.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
/// `speaker:<entity slug>@<n>`.
|
||||
pub fn speaker_key(entity: &EntityId, n: u32) -> String {
|
||||
let slug = entity
|
||||
.as_str()
|
||||
.strip_prefix("entity:")
|
||||
.unwrap_or(entity.as_str());
|
||||
format!("speaker:{slug}@{n}")
|
||||
}
|
||||
|
||||
/// `scene:<group>@<n>`.
|
||||
pub fn scene_key(group: &str, n: u32) -> String {
|
||||
format!("scene:{group}@{n}")
|
||||
}
|
||||
|
|
@ -14,11 +14,14 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||
use crate::corpus::{CorpusItem, CorpusItemId, Source, item_key, key_slug};
|
||||
use crate::entity::{Entity, EntityOrigin, Pronunciation};
|
||||
use crate::form::{
|
||||
is_attestation, is_calendar_date, is_joining_only, is_language_tag, is_refused_format,
|
||||
is_attestation, is_calendar_date, is_joining_only, is_language_tag, is_refused_format, is_slug,
|
||||
};
|
||||
use crate::inventory::{
|
||||
Change, InvId, Inventory, InventoryEntry, InventoryRevision, Slot, revision_key,
|
||||
};
|
||||
use crate::inventory::{Change, InvId, Inventory, InventoryEntry, InventoryRevision, revision_key};
|
||||
use crate::packet::{EntityId, Packet};
|
||||
use crate::validate::{LoweringInput, Refusal, admissible};
|
||||
use crate::speaker::{Annotator, MAX_PARTICIPANTS, Party, Scene, Speaker, scene_key, speaker_key};
|
||||
use crate::validate::{LoweringInput, Refusal, feature_refusals, validate};
|
||||
|
||||
/// A broken store rule, named.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -79,6 +82,15 @@ pub enum StoreRefusal {
|
|||
/// A new packet names a corpus item already withdrawn. Packets admitted
|
||||
/// before the withdrawal stay stored and readable.
|
||||
WithdrawnCorpusItem { key: String },
|
||||
/// A speaker or scene revision that is not the entity's or group's latest
|
||||
/// plus one (1 when none).
|
||||
RevisionNotNext {
|
||||
key: String,
|
||||
expected: u32,
|
||||
got: u32,
|
||||
},
|
||||
/// A scene that omits the speaker of a live item of its group.
|
||||
SceneSpeakerMissing { group: String, entity: EntityId },
|
||||
/// Any other malformed document or change.
|
||||
Malformed { key: String, why: &'static str },
|
||||
}
|
||||
|
|
@ -116,6 +128,10 @@ impl fmt::Display for StoreRefusal {
|
|||
EntityNotAppendOnly { .. } => f.write_str("EntityNotAppendOnly"),
|
||||
EntityNotOnItem { .. } => f.write_str("EntityNotOnItem"),
|
||||
WithdrawnCorpusItem { .. } => f.write_str("WithdrawnCorpusItem"),
|
||||
RevisionNotNext { expected, got, .. } => {
|
||||
write!(f, "RevisionNotNext expected {expected} got {got}")
|
||||
}
|
||||
SceneSpeakerMissing { .. } => f.write_str("SceneSpeakerMissing"),
|
||||
Malformed { why, .. } => write!(f, "Malformed: {why}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -943,7 +959,7 @@ impl PacketStore {
|
|||
corpus: &CorpusStore,
|
||||
entities: &EntityStore,
|
||||
) -> Result<LoweringInput, StoreError> {
|
||||
let view = admissible(&packet, &inventory.inventory()?).map_err(StoreError::Invalid)?;
|
||||
validate(&packet, &inventory.inventory()?).map_err(StoreError::Invalid)?;
|
||||
let item = corpus.resolve(&packet.provenance.corpus_item)?;
|
||||
if item.withdrawn {
|
||||
return Err(StoreRefusal::WithdrawnCorpusItem {
|
||||
|
|
@ -967,6 +983,7 @@ impl PacketStore {
|
|||
.into());
|
||||
}
|
||||
}
|
||||
let view = LoweringInput::of(&packet, &item);
|
||||
let key = packet.id.to_string();
|
||||
if let Some(existing) = self.get(&key)? {
|
||||
if existing == packet {
|
||||
|
|
@ -978,3 +995,210 @@ impl PacketStore {
|
|||
Ok(view)
|
||||
}
|
||||
}
|
||||
|
||||
/// The speaker and scene documents: append-only revisions, one key each. The
|
||||
/// only writer of `weksa.speaker` and `weksa.scene`.
|
||||
pub struct SpeakerStateStore {
|
||||
cache: CultCache,
|
||||
}
|
||||
|
||||
impl SpeakerStateStore {
|
||||
pub fn open(path: &Path) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
cache: open_cache(path, |c| {
|
||||
c.register_entry_type::<Speaker>()?;
|
||||
c.register_entry_type::<Scene>()
|
||||
})?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Admits the next revision of a speaker, or an identical replay. The key
|
||||
/// derives from the document. Revision `n` is the entity's latest plus one
|
||||
/// (1 when none). The entity is in the entity store, the features pass
|
||||
/// `feature_refusals` in slot `Speaker` at the document's inventory
|
||||
/// revision, and a model annotator's generator is an attestation.
|
||||
pub fn put_speaker(
|
||||
&mut self,
|
||||
s: Speaker,
|
||||
inventory: &InventoryStore,
|
||||
entities: &EntityStore,
|
||||
) -> Result<AddOutcome, StoreError> {
|
||||
let key = speaker_key(&s.entity, s.n);
|
||||
if self.get_speaker(&key)?.is_some_and(|old| old == s) {
|
||||
return Ok(AddOutcome::Unchanged);
|
||||
}
|
||||
check_annotator(&key, &s.annotator)?;
|
||||
known_entity(entities, &s.entity)?;
|
||||
let out = feature_refusals(
|
||||
&inventory.inventory()?,
|
||||
s.inventory_rev,
|
||||
&s.features,
|
||||
Slot::Speaker,
|
||||
);
|
||||
if !out.is_empty() {
|
||||
return Err(StoreError::Invalid(out));
|
||||
}
|
||||
let latest = self.speaker(&s.entity)?.map_or(0, |l| l.n);
|
||||
check_next(&key, latest, s.n)?;
|
||||
self.cache.put(key, &s)?;
|
||||
Ok(AddOutcome::Added)
|
||||
}
|
||||
|
||||
/// Admits the next revision of a scene, or an identical replay. The group
|
||||
/// is a slug some corpus item (live or withdrawn) holds; there are 1 to
|
||||
/// `MAX_PARTICIPANTS` participants, each named once and in the entity
|
||||
/// store; a relation's `from` is a participant, a named `to` is another
|
||||
/// participant, and an ordered pair has at most one relation; every speaker
|
||||
/// of a live item of the group is a participant. Features pass
|
||||
/// `feature_refusals` in slot `Scene` for every relation.
|
||||
pub fn put_scene(
|
||||
&mut self,
|
||||
s: Scene,
|
||||
inventory: &InventoryStore,
|
||||
corpus: &CorpusStore,
|
||||
entities: &EntityStore,
|
||||
) -> Result<AddOutcome, StoreError> {
|
||||
let key = scene_key(&s.group, s.n);
|
||||
if self.get_scene(&key)?.is_some_and(|old| old == s) {
|
||||
return Ok(AddOutcome::Unchanged);
|
||||
}
|
||||
let bad = |why| malformed(&key, why);
|
||||
if !is_slug(&s.group) {
|
||||
return Err(bad("a scene group is a slug"));
|
||||
}
|
||||
check_annotator(&key, &s.annotator)?;
|
||||
let items: Vec<CorpusItem> = corpus
|
||||
.all()?
|
||||
.into_iter()
|
||||
.filter(|i| i.group == s.group)
|
||||
.collect();
|
||||
if items.is_empty() {
|
||||
return Err(bad("no corpus item holds the scene's group"));
|
||||
}
|
||||
if s.participants.is_empty() || s.participants.len() > MAX_PARTICIPANTS {
|
||||
return Err(bad("a scene has 1 to 64 participants"));
|
||||
}
|
||||
let mut seen = BTreeSet::new();
|
||||
for p in &s.participants {
|
||||
if !seen.insert(p) {
|
||||
return Err(bad("a participant is named once"));
|
||||
}
|
||||
known_entity(entities, p)?;
|
||||
}
|
||||
let mut pairs = BTreeSet::new();
|
||||
for r in &s.relations {
|
||||
if !seen.contains(&r.from) {
|
||||
return Err(bad("a relation's from is a participant"));
|
||||
}
|
||||
if let Party::Named(to) = &r.to
|
||||
&& (!seen.contains(to) || to == &r.from)
|
||||
{
|
||||
return Err(bad("a relation's named to is another participant"));
|
||||
}
|
||||
if !pairs.insert((&r.from, &r.to)) {
|
||||
return Err(bad("an ordered pair has one relation"));
|
||||
}
|
||||
}
|
||||
if let Some(i) = items
|
||||
.iter()
|
||||
.find(|i| !i.withdrawn && !seen.contains(&i.speaker))
|
||||
{
|
||||
return Err(StoreRefusal::SceneSpeakerMissing {
|
||||
group: s.group,
|
||||
entity: i.speaker.clone(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let inv = inventory.inventory()?;
|
||||
let mut out = Vec::new();
|
||||
if s.relations.is_empty() {
|
||||
out = feature_refusals(&inv, s.inventory_rev, &[], Slot::Scene);
|
||||
}
|
||||
for r in &s.relations {
|
||||
out.extend(feature_refusals(
|
||||
&inv,
|
||||
s.inventory_rev,
|
||||
&r.features,
|
||||
Slot::Scene,
|
||||
));
|
||||
}
|
||||
if !out.is_empty() {
|
||||
return Err(StoreError::Invalid(out));
|
||||
}
|
||||
let latest = self.scene(&s.group)?.map_or(0, |l| l.n);
|
||||
check_next(&key, latest, s.n)?;
|
||||
self.cache.put(key, &s)?;
|
||||
Ok(AddOutcome::Added)
|
||||
}
|
||||
|
||||
/// The latest revision (highest `n`) of the entity's speaker document.
|
||||
pub fn speaker(&self, entity: &EntityId) -> anyhow::Result<Option<Speaker>> {
|
||||
Ok(self
|
||||
.all_speakers()?
|
||||
.into_iter()
|
||||
.filter(|s| &s.entity == entity)
|
||||
.max_by_key(|s| s.n))
|
||||
}
|
||||
|
||||
/// The latest revision (highest `n`) of the group's scene document.
|
||||
pub fn scene(&self, group: &str) -> anyhow::Result<Option<Scene>> {
|
||||
Ok(self
|
||||
.all_scenes()?
|
||||
.into_iter()
|
||||
.filter(|s| s.group == group)
|
||||
.max_by_key(|s| s.n))
|
||||
}
|
||||
|
||||
pub fn get_speaker(&self, key: &str) -> anyhow::Result<Option<Speaker>> {
|
||||
self.cache.get::<Speaker>(key)
|
||||
}
|
||||
|
||||
pub fn get_scene(&self, key: &str) -> anyhow::Result<Option<Scene>> {
|
||||
self.cache.get::<Scene>(key)
|
||||
}
|
||||
|
||||
/// Every speaker revision, by entity then revision number.
|
||||
pub fn all_speakers(&self) -> anyhow::Result<Vec<Speaker>> {
|
||||
let mut all = self.cache.get_all::<Speaker>()?;
|
||||
all.sort_by(|a, b| (&a.entity, a.n).cmp(&(&b.entity, b.n)));
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
/// Every scene revision, by group then revision number.
|
||||
pub fn all_scenes(&self) -> anyhow::Result<Vec<Scene>> {
|
||||
let mut all = self.cache.get_all::<Scene>()?;
|
||||
all.sort_by(|a, b| (&a.group, a.n).cmp(&(&b.group, b.n)));
|
||||
Ok(all)
|
||||
}
|
||||
}
|
||||
|
||||
fn known_entity(entities: &EntityStore, id: &EntityId) -> Result<(), StoreError> {
|
||||
match entities.get(id)? {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(StoreRefusal::UnknownEntity { entity: id.clone() }.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_annotator(key: &str, a: &Annotator) -> Result<(), StoreError> {
|
||||
match a {
|
||||
Annotator::Model { generator, .. } if !is_attestation(generator) => {
|
||||
Err(malformed(key, "a model annotator attests its generator"))
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_next(key: &str, latest: u32, got: u32) -> Result<(), StoreError> {
|
||||
let expected = latest
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| malformed(key, "no revision follows the latest"))?;
|
||||
if got != expected {
|
||||
return Err(StoreRefusal::RevisionNotNext {
|
||||
key: key.to_string(),
|
||||
expected,
|
||||
got,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
//! The validator owns the inventory and form rules a packet must meet. Every
|
||||
//! path that admits a packet applies them, but `validate` only judges: it
|
||||
//! 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..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`].
|
||||
//! 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
|
||||
//! 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
|
||||
//! `feature_refusals` is the one feature check it and that store share.
|
||||
//!
|
||||
//! `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
|
||||
|
|
@ -26,11 +28,13 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
|
||||
use crate::corpus::CorpusItem;
|
||||
use crate::inventory::{Family, InvId, Inventory, InventoryEntry, Slot, ValueKind};
|
||||
use crate::packet::{
|
||||
Constraints, Context, Discourse, Extension, FeatValue, FeatureValue, NodeId, Packet,
|
||||
Constraints, Context, Discourse, EntityId, Extension, FeatValue, FeatureValue, NodeId, Packet,
|
||||
PacketKind, Predication, ProvenanceSource, Referent, Relation, SCHEMA_VERSION, Stance,
|
||||
};
|
||||
use crate::store::{SpeakerStateStore, StoreError};
|
||||
|
||||
/// A broken rule, named.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -95,10 +99,6 @@ pub enum Refusal {
|
|||
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,
|
||||
/// 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.
|
||||
|
|
@ -112,6 +112,13 @@ pub enum Refusal {
|
|||
V15ConsentSource {
|
||||
source: ProvenanceSource,
|
||||
},
|
||||
/// V16: `with_scene` found the item's speaker absent from its group's scene.
|
||||
V16SpeakerNotInScene,
|
||||
/// V17: an addressee that names a predication. An act is not someone
|
||||
/// spoken to, so the addressee names a referent.
|
||||
V17AddresseeNotAReferent {
|
||||
id: NodeId,
|
||||
},
|
||||
}
|
||||
|
||||
impl Refusal {
|
||||
|
|
@ -130,11 +137,12 @@ impl Refusal {
|
|||
| Refusal::V11DuplicateNodeRef { .. }
|
||||
| Refusal::V11DuplicateRole { .. } => "V11",
|
||||
Refusal::V12TooManyNodes { .. } => "V12",
|
||||
Refusal::V13SceneWithoutAddressee => "V13",
|
||||
Refusal::V14ConsentWithoutAddressee | Refusal::V14ConsentNotAPredication { .. } => {
|
||||
"V14"
|
||||
}
|
||||
Refusal::V15ConsentSource { .. } => "V15",
|
||||
Refusal::V16SpeakerNotInScene => "V16",
|
||||
Refusal::V17AddresseeNotAReferent { .. } => "V17",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -157,10 +165,11 @@ impl fmt::Display for Refusal {
|
|||
Refusal::V11DuplicateNodeRef { .. } => "a node id repeats in one list",
|
||||
Refusal::V11DuplicateRole { .. } => "a role id repeats in one predication",
|
||||
Refusal::V12TooManyNodes { .. } => "too many nodes",
|
||||
Refusal::V13SceneWithoutAddressee => "scene features given without an addressee",
|
||||
Refusal::V14ConsentWithoutAddressee => "consent given without an addressee",
|
||||
Refusal::V14ConsentNotAPredication { .. } => "consent names no predication",
|
||||
Refusal::V15ConsentSource { .. } => "consent set by a source that only read wording",
|
||||
Refusal::V16SpeakerNotInScene => "the speaker is not a participant of the scene",
|
||||
Refusal::V17AddresseeNotAReferent { .. } => "the addressee names no referent",
|
||||
};
|
||||
write!(f, "{}: {what}", self.rule())
|
||||
}
|
||||
|
|
@ -180,28 +189,6 @@ impl fmt::Display for Refusal {
|
|||
/// fn lower(p: &Packet, i: &Inventory) -> Result<LoweringInput, Vec<Refusal>> { weksa::validate(p, i) }
|
||||
/// ```
|
||||
pub fn validate(packet: &Packet, inventory: &Inventory) -> Result<(), Vec<Refusal>> {
|
||||
admissible(packet, inventory).map(|_| ())
|
||||
}
|
||||
|
||||
/// The one implementation of the rules. Crate-private: `PacketStore::admit` is
|
||||
/// the only caller that keeps the view.
|
||||
///
|
||||
/// The first block is the same call through the public name, so the second
|
||||
/// fails for the private path and nothing else.
|
||||
///
|
||||
/// ```
|
||||
/// use weksa::{Inventory, packet::Packet};
|
||||
/// fn judge(p: &Packet, i: &Inventory) { let _ = weksa::validate::validate(p, i); }
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use weksa::{Inventory, packet::Packet};
|
||||
/// fn lower(p: &Packet, i: &Inventory) { let _ = weksa::validate::admissible(p, i); }
|
||||
/// ```
|
||||
pub(crate) fn admissible(
|
||||
packet: &Packet,
|
||||
inventory: &Inventory,
|
||||
) -> Result<LoweringInput, Vec<Refusal>> {
|
||||
let mut out = Vec::new();
|
||||
if packet.schema_version != SCHEMA_VERSION {
|
||||
out.push(Refusal::V1SchemaVersion {
|
||||
|
|
@ -221,6 +208,7 @@ pub(crate) fn admissible(
|
|||
}
|
||||
let nodes = check_nodes(packet, &mut out);
|
||||
check_refs(packet, &nodes, &mut out);
|
||||
check_addressee(packet, &nodes, &mut out);
|
||||
check_repeats(packet, &mut out);
|
||||
check_consent(packet, &nodes, &mut out);
|
||||
if inventory.has_revision(packet.inventory_rev) {
|
||||
|
|
@ -235,11 +223,7 @@ pub(crate) fn admissible(
|
|||
rev: packet.inventory_rev,
|
||||
});
|
||||
}
|
||||
if out.is_empty() {
|
||||
Ok(LoweringInput::of(packet))
|
||||
} else {
|
||||
Err(out)
|
||||
}
|
||||
if out.is_empty() { Ok(()) } else { Err(out) }
|
||||
}
|
||||
|
||||
fn check_nodes(packet: &Packet, out: &mut Vec<Refusal>) -> BTreeSet<NodeId> {
|
||||
|
|
@ -285,6 +269,17 @@ fn check_refs(packet: &Packet, nodes: &BTreeSet<NodeId>, out: &mut Vec<Refusal>)
|
|||
}
|
||||
}
|
||||
|
||||
/// V17: the addressee, when it names a node, names a referent. A dangling id
|
||||
/// is V4's.
|
||||
fn check_addressee(packet: &Packet, nodes: &BTreeSet<NodeId>, out: &mut Vec<Refusal>) {
|
||||
if let Some(a) = &packet.discourse.addressee
|
||||
&& nodes.contains(a)
|
||||
&& !packet.referents.iter().any(|r| &r.id == a)
|
||||
{
|
||||
out.push(Refusal::V17AddresseeNotAReferent { id: a.clone() });
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
|
|
@ -472,6 +467,29 @@ impl<'a> Inventoried<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The one feature check of the speaker-state documents and `with_scene`: V10
|
||||
/// when `rev` has no record, else the rules the packet's own check applies to a
|
||||
/// feature list in `slot` (V5 to V8, V11).
|
||||
pub(crate) fn feature_refusals(
|
||||
inventory: &Inventory,
|
||||
rev: u32,
|
||||
features: &[FeatureValue],
|
||||
slot: Slot,
|
||||
) -> Vec<Refusal> {
|
||||
let mut out = Vec::new();
|
||||
if !inventory.has_revision(rev) {
|
||||
out.push(Refusal::V10UnknownRevision { rev });
|
||||
} else {
|
||||
Inventoried {
|
||||
inv: inventory,
|
||||
rev,
|
||||
out: &mut out,
|
||||
}
|
||||
.features(features, slot);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn fits(kind: Option<&ValueKind>, value: &FeatValue) -> bool {
|
||||
match (kind, value) {
|
||||
(Some(ValueKind::Grade), FeatValue::Grade(_)) => true,
|
||||
|
|
@ -481,6 +499,28 @@ fn fits(kind: Option<&ValueKind>, value: &FeatValue) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// What `with_scene` found for a view: the scene revision `rev` it read, or why
|
||||
/// it selected no relation. Each case is distinct so a missing annotation or a
|
||||
/// store at the wrong path cannot look like a scene that held nothing.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SceneRead {
|
||||
/// `with_scene` has not run.
|
||||
Unread,
|
||||
/// The store holds no scene for the item's group.
|
||||
NoScene,
|
||||
/// The scene was read and the packet names no addressee.
|
||||
NoAddressee { rev: u32 },
|
||||
/// The addressee referent is the speaker themself.
|
||||
SelfAddressed { rev: u32 },
|
||||
/// The scene holds no relation from the speaker to this addressee.
|
||||
NoRelation { rev: u32 },
|
||||
/// The relation was found; its features may be none.
|
||||
Selected {
|
||||
rev: u32,
|
||||
features: Vec<FeatureValue>,
|
||||
},
|
||||
}
|
||||
|
||||
/// What the lowerer sees: a validated packet's meaning, with no packet id, no
|
||||
/// provenance (so no corpus item key, encoder or source) and no trace. Its
|
||||
/// fields are private and the constructor is crate-private, called only by
|
||||
|
|
@ -537,16 +577,45 @@ fn fits(kind: Option<&ValueKind>, value: &FeatValue) -> bool {
|
|||
/// fn read(l: weksa::LoweringInput) { let _ = l.scene; }
|
||||
/// ```
|
||||
///
|
||||
/// Scene values come only from the store: the call names neither speaker nor
|
||||
/// scene. The first block is the call with the store; the second hands it a
|
||||
/// scene value and a speaker and fails for that alone.
|
||||
///
|
||||
/// ```
|
||||
/// fn pick(l: weksa::LoweringInput, i: &weksa::Inventory, s: &weksa::SpeakerStateStore) {
|
||||
/// let _ = l.with_scene(i, s);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// fn pick(
|
||||
/// l: weksa::LoweringInput,
|
||||
/// i: &weksa::Inventory,
|
||||
/// s: &weksa::speaker::Scene,
|
||||
/// e: &weksa::EntityId,
|
||||
/// ) {
|
||||
/// let _ = l.with_scene(i, s, e);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// fn pick(l: weksa::LoweringInput, i: &weksa::Inventory) {
|
||||
/// let _ = l.with_scene(i, vec![]);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// ```
|
||||
/// fn build(p: &weksa::packet::Packet) { let _ = p; }
|
||||
/// fn build(p: &weksa::packet::Packet, item: &weksa::CorpusItem) { let _ = (p, item); }
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// fn build(p: &weksa::packet::Packet) { let _ = weksa::LoweringInput::of(p); }
|
||||
/// fn build(p: &weksa::packet::Packet, item: &weksa::CorpusItem) {
|
||||
/// let _ = weksa::LoweringInput::of(p, item);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A lowering view is never read from bytes: `Deserialize` would let a caller
|
||||
|
|
@ -574,11 +643,16 @@ pub struct LoweringInput {
|
|||
constraints: Constraints,
|
||||
extensions: Vec<Extension>,
|
||||
stance: Stance,
|
||||
scene: Vec<FeatureValue>,
|
||||
/// The item's speaker and group, as the corpus store holds them: the
|
||||
/// only source of whose relation `with_scene` selects, and from which scene.
|
||||
speaker: EntityId,
|
||||
group: String,
|
||||
scene: SceneRead,
|
||||
}
|
||||
|
||||
impl LoweringInput {
|
||||
fn of(p: &Packet) -> Self {
|
||||
/// The view of an admitted packet and the corpus item it was minted for.
|
||||
pub(crate) fn of(p: &Packet, item: &CorpusItem) -> Self {
|
||||
Self {
|
||||
inventory_rev: p.inventory_rev,
|
||||
kind: p.kind,
|
||||
|
|
@ -590,41 +664,63 @@ impl LoweringInput {
|
|||
constraints: p.constraints.clone(),
|
||||
extensions: p.extensions.clone(),
|
||||
stance: p.stance.clone(),
|
||||
scene: Vec::new(),
|
||||
speaker: item.speaker.clone(),
|
||||
group: item.group.clone(),
|
||||
scene: SceneRead::Unread,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Reads the speaker's relation to the addressee, the only path for scene
|
||||
/// values into a view. The speaker is the item's speaker and the scene is
|
||||
/// the latest the store holds for the item's group; the caller names
|
||||
/// neither. A scene that does not list the item's speaker is V16, with or
|
||||
/// without an addressee. What the read found is recorded on the view as a
|
||||
/// [`SceneRead`], never an empty feature list standing for five cases:
|
||||
/// no scene for the group, no addressee, an addressee who is the speaker,
|
||||
/// no such relation, or the relation from the speaker to the addressee
|
||||
/// referent's entity (to `Unnamed` when the referent has none), whose
|
||||
/// features are checked like every feature list in slot `Scene` at this
|
||||
/// view's inventory revision. A read replaces any earlier one. The English
|
||||
/// lowerer ignores it.
|
||||
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);
|
||||
state: &SpeakerStateStore,
|
||||
) -> Result<Self, StoreError> {
|
||||
self.scene = SceneRead::Unread;
|
||||
let Some(scene) = state.scene(&self.group)? else {
|
||||
self.scene = SceneRead::NoScene;
|
||||
return Ok(self);
|
||||
};
|
||||
let rev = scene.n;
|
||||
if !scene.participants.contains(&self.speaker) {
|
||||
return Err(StoreError::Invalid(vec![Refusal::V16SpeakerNotInScene]));
|
||||
}
|
||||
if !scene.is_empty() && self.discourse.addressee.is_none() {
|
||||
out.push(Refusal::V13SceneWithoutAddressee);
|
||||
let Some(addressee) = self.discourse.addressee.as_ref() else {
|
||||
self.scene = SceneRead::NoAddressee { rev };
|
||||
return Ok(self);
|
||||
};
|
||||
let entity = self
|
||||
.referents
|
||||
.iter()
|
||||
.find(|r| &r.id == addressee)
|
||||
.and_then(|r| r.entity.as_ref());
|
||||
if entity == Some(&self.speaker) {
|
||||
self.scene = SceneRead::SelfAddressed { rev };
|
||||
return Ok(self);
|
||||
}
|
||||
let Some(features) = scene.relation(&self.speaker, entity) else {
|
||||
self.scene = SceneRead::NoRelation { rev };
|
||||
return Ok(self);
|
||||
};
|
||||
let out = feature_refusals(inventory, self.inventory_rev, features, Slot::Scene);
|
||||
if !out.is_empty() {
|
||||
return Err(out);
|
||||
return Err(StoreError::Invalid(out));
|
||||
}
|
||||
self.scene = scene;
|
||||
self.scene = SceneRead::Selected {
|
||||
rev,
|
||||
features: features.to_vec(),
|
||||
};
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
|
|
@ -658,8 +754,17 @@ impl LoweringInput {
|
|||
pub fn stance(&self) -> &Stance {
|
||||
&self.stance
|
||||
}
|
||||
/// The speaker's relation to the addressee; empty until `with_scene`.
|
||||
/// The features of the scene relation from the speaker to the addressee.
|
||||
/// Empty for every [`SceneRead`] but `Selected` (and for a selected
|
||||
/// relation that has no features): ask [`scene_read`](Self::scene_read) which.
|
||||
pub fn scene(&self) -> &[FeatureValue] {
|
||||
match &self.scene {
|
||||
SceneRead::Selected { features, .. } => features,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
/// What `with_scene` read; `Unread` until it ran.
|
||||
pub fn scene_read(&self) -> &SceneRead {
|
||||
&self.scene
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,9 @@ fn schema_writes_the_published_files() {
|
|||
"weksa.entity.schema.json",
|
||||
"weksa.interlingua_packet.schema.json",
|
||||
"weksa.inventory_entry.schema.json",
|
||||
"weksa.inventory_revision.schema.json"
|
||||
"weksa.inventory_revision.schema.json",
|
||||
"weksa.scene.schema.json",
|
||||
"weksa.speaker.schema.json"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ pub const CONCEPT_LATE: &str = "concept:4";
|
|||
pub const FRAME: &str = "frame:1";
|
||||
/// An extension entry of value kind Grade, introduced at rev 2.
|
||||
pub const EXT: &str = "extension:1";
|
||||
/// A Grade feature introduced at rev 2 that sits in every slot.
|
||||
/// A Grade feature introduced at rev 2 in every content slot (Discourse,
|
||||
/// Referent, Predication, Relation).
|
||||
pub const FEAT_ALL: &str = "feature:50";
|
||||
|
||||
pub const PACKET_ID: &str = "packet:0123456789abcdef";
|
||||
|
|
@ -218,7 +219,6 @@ pub fn world() -> World {
|
|||
Slot::Referent,
|
||||
Slot::Predication,
|
||||
Slot::Relation,
|
||||
Slot::Stance,
|
||||
],
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -117,12 +117,13 @@ fn refusal_display_names_the_rule_and_no_identifier() {
|
|||
count: 4242,
|
||||
max: 4242,
|
||||
},
|
||||
Refusal::V13SceneWithoutAddressee,
|
||||
Refusal::V14ConsentWithoutAddressee,
|
||||
Refusal::V14ConsentNotAPredication { id: node() },
|
||||
Refusal::V15ConsentSource {
|
||||
source: ProvenanceSource::ImportedCorpus,
|
||||
},
|
||||
Refusal::V16SpeakerNotInScene,
|
||||
Refusal::V17AddresseeNotAReferent { id: node() },
|
||||
];
|
||||
for r in &all {
|
||||
assert!(r.to_string().starts_with(r.rule()), "{r:?}");
|
||||
|
|
|
|||
|
|
@ -1,156 +1,353 @@
|
|||
//! 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.
|
||||
//! The speaker's relation to the addressee as a lowering input. `with_scene`
|
||||
//! names neither speaker nor scene: the view knows its corpus item, so the
|
||||
//! speaker is the item's speaker and the scene is the store's latest for the
|
||||
//! item's group. The relation is the one from that speaker to the addressee
|
||||
//! referent's entity. Typed feature values only, never wording.
|
||||
|
||||
mod common;
|
||||
use std::path::Path;
|
||||
|
||||
use common::{grade, iv, lab};
|
||||
use weksa::inventory::{proof_revision, scene_revision, seed};
|
||||
use common::{cast, eid, entities_in, grade, item_in, lab, nd};
|
||||
use weksa::inventory::{proof_revision, seed, speaker_scene_revision};
|
||||
use weksa::packet::{FeatValue, FeatureValue, Packet, proof_packet};
|
||||
use weksa::speaker::{Annotator, PairRelation, Party, Scene};
|
||||
use weksa::{
|
||||
CorpusStore, EntityStore, Inventory, InventoryStore, LoweringInput, PacketStore, Refusal, lower,
|
||||
CorpusStore, EntityStore, Inventory, InventoryStore, LoweringInput, NameSpan, PacketStore,
|
||||
Refusal, SceneRead, SpeakerStateStore, StoreError, lower,
|
||||
};
|
||||
|
||||
fn store() -> (InventoryStore, Inventory, tempfile::TempDir) {
|
||||
struct W {
|
||||
dir: tempfile::TempDir,
|
||||
inventory: InventoryStore,
|
||||
corpus: CorpusStore,
|
||||
entities: EntityStore,
|
||||
state: SpeakerStateStore,
|
||||
}
|
||||
|
||||
/// Group `g` holds `item:nibu:1`: Nibu says "Brokkr, come here." Group `h`
|
||||
/// holds `item:nibu:2`, also spoken by Nibu.
|
||||
fn world() -> W {
|
||||
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 mut inventory = InventoryStore::open(&dir.path().join("i.cc")).unwrap();
|
||||
for (rev, entries) in [seed(), proof_revision(), speaker_scene_revision()] {
|
||||
inventory.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,
|
||||
let entities = entities_in(dir.path());
|
||||
let mut corpus = CorpusStore::open(&dir.path().join("c.cc")).unwrap();
|
||||
let mut first = item_in("item:nibu:1", "Brokkr, come here.", "g", cast());
|
||||
first.names = vec![NameSpan {
|
||||
entity: eid("brokkr"),
|
||||
start: 0,
|
||||
end: 6,
|
||||
}];
|
||||
corpus.add(first, &entities).unwrap();
|
||||
corpus
|
||||
.add(item_in("item:nibu:2", "Go away.", "h", cast()), &entities)
|
||||
.unwrap();
|
||||
let state = SpeakerStateStore::open(&dir.path().join("s.cc")).unwrap();
|
||||
W {
|
||||
dir,
|
||||
inventory,
|
||||
corpus,
|
||||
entities,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
fn status(inv: &Inventory) -> String {
|
||||
inv.entries()
|
||||
.find(|e| e.gloss == "relative_status")
|
||||
.unwrap()
|
||||
.id
|
||||
.to_string()
|
||||
fn inv(w: &W) -> Inventory {
|
||||
w.inventory.inventory().unwrap()
|
||||
}
|
||||
|
||||
fn id_of(inv: &Inventory, gloss: &str) -> String {
|
||||
inv.entries()
|
||||
fn feat(gloss: &str, value: FeatValue) -> FeatureValue {
|
||||
let feature = seed()
|
||||
.1
|
||||
.into_iter()
|
||||
.chain(speaker_scene_revision().1)
|
||||
.find(|e| e.gloss == gloss)
|
||||
.unwrap()
|
||||
.id
|
||||
.to_string()
|
||||
.unwrap_or_else(|| panic!("no feature {gloss}"))
|
||||
.id;
|
||||
FeatureValue { feature, value }
|
||||
}
|
||||
|
||||
fn rules(r: Result<LoweringInput, Vec<Refusal>>) -> Vec<&'static str> {
|
||||
r.unwrap_err().iter().map(Refusal::rule).collect()
|
||||
fn intimacy(v: u16) -> Vec<FeatureValue> {
|
||||
vec![feat("intimacy", FeatValue::Grade(grade(v)))]
|
||||
}
|
||||
|
||||
fn status() -> Vec<FeatureValue> {
|
||||
vec![
|
||||
feat("relative_status", FeatValue::Option(lab("above"))),
|
||||
feat("intimacy", FeatValue::Grade(grade(200))),
|
||||
feat("in_group", FeatValue::Flag(false)),
|
||||
]
|
||||
}
|
||||
|
||||
fn relation(from: &str, to: Party, features: Vec<FeatureValue>) -> PairRelation {
|
||||
PairRelation {
|
||||
from: eid(from),
|
||||
to,
|
||||
features,
|
||||
}
|
||||
}
|
||||
|
||||
/// Nibu's relations to Brokkr and to an unnamed party, and Brokkr's to Nibu.
|
||||
fn scene(group: &str, n: u32, participants: &[&str], relations: Vec<PairRelation>) -> Scene {
|
||||
Scene {
|
||||
group: group.into(),
|
||||
n,
|
||||
inventory_rev: 3,
|
||||
participants: participants.iter().map(|p| eid(p)).collect(),
|
||||
relations,
|
||||
annotator: Annotator::Operator,
|
||||
}
|
||||
}
|
||||
|
||||
fn standard(group: &str, n: u32) -> Scene {
|
||||
scene(
|
||||
group,
|
||||
n,
|
||||
&["nibu", "brokkr"],
|
||||
vec![
|
||||
relation("nibu", Party::Named(eid("brokkr")), status()),
|
||||
relation("nibu", Party::Unnamed, intimacy(7)),
|
||||
relation("brokkr", Party::Named(eid("nibu")), intimacy(99)),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn put(w: &mut W, s: Scene) {
|
||||
w.state
|
||||
.put_scene(s, &w.inventory, &w.corpus, &w.entities)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// The line of `item:nibu:1` at `rev`, its addressee node `addressee` (`n1`
|
||||
/// has no entity, `n3` is Brokkr), through the packet door.
|
||||
fn view_of(w: &W, rev: u32, addressee: Option<&str>) -> LoweringInput {
|
||||
view_to(w, rev, addressee, "brokkr")
|
||||
}
|
||||
|
||||
/// As `view_of`, with referent `n3` naming `entity` (Nibu is the item's
|
||||
/// speaker, so he may be named on the line; Brokkr is named by its text).
|
||||
fn view_to(w: &W, rev: u32, addressee: Option<&str>, entity: &str) -> LoweringInput {
|
||||
let mut p: Packet = proof_packet();
|
||||
p.inventory_rev = rev;
|
||||
p.referents[2].entity = Some(eid(entity));
|
||||
p.discourse.addressee = addressee.map(nd);
|
||||
// A packet key holds its packet for good, so each view gets a fresh store.
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
PacketStore::open(&scratch.path().join("p.cc"))
|
||||
.unwrap()
|
||||
.admit(p, &w.inventory, &w.corpus, &w.entities)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn rules(r: Result<LoweringInput, StoreError>) -> Vec<&'static str> {
|
||||
match r.map(|_| ()).unwrap_err() {
|
||||
StoreError::Invalid(rs) => rs.iter().map(Refusal::rule).collect(),
|
||||
other => panic!("not a rule refusal: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(w: &W, view: LoweringInput) -> SceneRead {
|
||||
view.with_scene(&inv(w), &w.state)
|
||||
.unwrap()
|
||||
.scene_read()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn selected(rev: u32, features: Vec<FeatureValue>) -> SceneRead {
|
||||
SceneRead::Selected { rev, features }
|
||||
}
|
||||
|
||||
#[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);
|
||||
let mut w = world();
|
||||
put(&mut w, standard("g", 1));
|
||||
let plain = view_of(&w, 3, Some("n3"));
|
||||
assert_eq!(plain.scene_read(), &SceneRead::Unread);
|
||||
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());
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let entities = EntityStore::open(&root.join("data/entity/entity.cc")).unwrap();
|
||||
let with = plain.clone().with_scene(&inv(&w), &w.state).unwrap();
|
||||
assert_eq!(with.scene(), status().as_slice());
|
||||
assert_eq!(with.scene_read(), &selected(1, status()));
|
||||
assert_eq!(
|
||||
lower(&with, &inv, &entities).unwrap(),
|
||||
lower(&plain, &inv, &entities).unwrap()
|
||||
lower(&with, &inv(&w), &w.entities).unwrap(),
|
||||
lower(&plain, &inv(&w), &w.entities).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());
|
||||
fn with_scene_says_which_of_five_things_it_found() {
|
||||
let mut w = world();
|
||||
// No scene for the group.
|
||||
assert_eq!(read(&w, view_of(&w, 3, Some("n3"))), SceneRead::NoScene);
|
||||
put(&mut w, standard("g", 1));
|
||||
// A referent with an entity selects the relation to that entity; a
|
||||
// referent without one, the relation to an unnamed party; and the
|
||||
// speaker's relation, never Brokkr's to Nibu.
|
||||
assert_eq!(read(&w, view_of(&w, 3, Some("n3"))), selected(1, status()));
|
||||
assert_eq!(
|
||||
read(&w, view_of(&w, 3, Some("n1"))),
|
||||
selected(1, intimacy(7))
|
||||
);
|
||||
// The scene was read and the packet names no addressee.
|
||||
assert_eq!(
|
||||
read(&w, view_of(&w, 3, None)),
|
||||
SceneRead::NoAddressee { rev: 1 }
|
||||
);
|
||||
// The addressee is the speaker.
|
||||
assert_eq!(
|
||||
read(&w, view_to(&w, 3, Some("n3"), "nibu")),
|
||||
SceneRead::SelfAddressed { rev: 1 }
|
||||
);
|
||||
// The scene holds no relation to this addressee.
|
||||
put(
|
||||
&mut w,
|
||||
scene(
|
||||
"g",
|
||||
2,
|
||||
&["nibu", "brokkr"],
|
||||
vec![relation("nibu", Party::Unnamed, intimacy(7))],
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
read(&w, view_of(&w, 3, Some("n3"))),
|
||||
SceneRead::NoRelation { rev: 2 }
|
||||
);
|
||||
// A relation with no features is found, not absent.
|
||||
put(
|
||||
&mut w,
|
||||
scene(
|
||||
"g",
|
||||
3,
|
||||
&["nibu", "brokkr"],
|
||||
vec![relation("nibu", Party::Named(eid("brokkr")), vec![])],
|
||||
),
|
||||
);
|
||||
assert_eq!(read(&w, view_of(&w, 3, Some("n3"))), selected(3, vec![]));
|
||||
}
|
||||
|
||||
#[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"]
|
||||
fn a_named_addressee_the_scene_lacks_never_falls_back_to_unnamed() {
|
||||
let mut w = world();
|
||||
// Brokkr is not a participant and Nibu has only an unnamed relation.
|
||||
put(
|
||||
&mut w,
|
||||
scene(
|
||||
"g",
|
||||
1,
|
||||
&["nibu", "player"],
|
||||
vec![relation("nibu", Party::Unnamed, intimacy(7))],
|
||||
),
|
||||
);
|
||||
// A value of the wrong kind.
|
||||
assert_eq!(
|
||||
rules(take(vec![feat(&intimacy, FeatValue::Flag(true))])),
|
||||
["V7"]
|
||||
read(&w, view_of(&w, 3, Some("n3"))),
|
||||
SceneRead::NoRelation { rev: 1 }
|
||||
);
|
||||
// A participant without a relation behaves the same.
|
||||
put(
|
||||
&mut w,
|
||||
scene(
|
||||
"g",
|
||||
2,
|
||||
&["nibu", "brokkr"],
|
||||
vec![relation("nibu", Party::Unnamed, intimacy(7))],
|
||||
),
|
||||
);
|
||||
// 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"]
|
||||
read(&w, view_of(&w, 3, Some("n3"))),
|
||||
SceneRead::NoRelation { rev: 2 }
|
||||
);
|
||||
// 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"]);
|
||||
fn the_speaker_is_the_items_not_the_first_participant() {
|
||||
let mut w = world();
|
||||
// Brokkr is listed first and has an unnamed relation of his own.
|
||||
put(
|
||||
&mut w,
|
||||
scene(
|
||||
"g",
|
||||
1,
|
||||
&["brokkr", "nibu"],
|
||||
vec![
|
||||
relation("brokkr", Party::Unnamed, intimacy(99)),
|
||||
relation("nibu", Party::Unnamed, intimacy(7)),
|
||||
],
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
read(&w, view_of(&w, 3, Some("n1"))),
|
||||
selected(1, intimacy(7))
|
||||
);
|
||||
}
|
||||
|
||||
#[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"]
|
||||
fn the_scene_is_the_items_group_and_its_latest_revision() {
|
||||
let mut w = world();
|
||||
// Group h holds a different relation for the same pair.
|
||||
put(
|
||||
&mut w,
|
||||
scene(
|
||||
"h",
|
||||
1,
|
||||
&["nibu", "brokkr"],
|
||||
vec![relation("nibu", Party::Named(eid("brokkr")), intimacy(1))],
|
||||
),
|
||||
);
|
||||
// No scene for the item's group, whatever other groups hold.
|
||||
assert_eq!(read(&w, view_of(&w, 3, Some("n3"))), SceneRead::NoScene);
|
||||
// Revisions: the highest n is read.
|
||||
put(&mut w, standard("g", 1));
|
||||
let mut second = standard("g", 2);
|
||||
second.relations[0].features = intimacy(5);
|
||||
put(&mut w, second);
|
||||
let got = view_of(&w, 3, Some("n3"))
|
||||
.with_scene(&inv(&w), &w.state)
|
||||
.unwrap();
|
||||
assert_eq!(got.scene_read(), &selected(2, intimacy(5)));
|
||||
assert_eq!(got.scene(), intimacy(5).as_slice());
|
||||
// A read replaces the earlier one: a store without the scene says so.
|
||||
let empty = SpeakerStateStore::open(&w.dir.path().join("empty.cc")).unwrap();
|
||||
let cleared = got.with_scene(&inv(&w), &empty).unwrap();
|
||||
assert_eq!(cleared.scene_read(), &SceneRead::NoScene);
|
||||
assert!(cleared.scene().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_scene_refusals() {
|
||||
let mut w = world();
|
||||
put(&mut w, standard("g", 1));
|
||||
// The scene was admitted at revision 3; a view of revision 2 predates the
|
||||
// Scene features, so the selected relation's ids do not resolve there.
|
||||
assert_eq!(
|
||||
rules(view_of(&w, 2, Some("n3")).with_scene(&inv(&w), &w.state)),
|
||||
["V5", "V5", "V5"]
|
||||
);
|
||||
// A scene that does not list the item's speaker: group k admitted while
|
||||
// Brokkr was its only speaker, then an item of Nibu's joins it.
|
||||
let mut item = item_in("item:brokkr:1", "Hello.", "k", cast());
|
||||
item.speaker = eid("brokkr");
|
||||
w.corpus.add(item, &w.entities).unwrap();
|
||||
put(&mut w, scene("k", 1, &["brokkr"], vec![]));
|
||||
w.corpus
|
||||
.add(
|
||||
item_in("item:nibu:3", "Hello there.", "k", cast()),
|
||||
&w.entities,
|
||||
)
|
||||
.unwrap();
|
||||
for addressee in [Some("n3"), None] {
|
||||
let mut p = proof_packet();
|
||||
p.inventory_rev = 3;
|
||||
p.provenance.corpus_item = weksa::CorpusItemId::parse("item:nibu:3").unwrap();
|
||||
p.referents[2].entity = Some(eid("nibu"));
|
||||
p.discourse.addressee = addressee.map(nd);
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
let view = PacketStore::open(&scratch.path().join("p.cc"))
|
||||
.unwrap()
|
||||
.admit(p, &w.inventory, &w.corpus, &w.entities)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
rules(view.with_scene(&inv(&w), &w.state)),
|
||||
["V16"],
|
||||
"{addressee:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
503
crates/weksa/tests/speaker_state.rs
Normal file
503
crates/weksa/tests/speaker_state.rs
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
//! The speaker-state store: speaker and scene documents are append-only
|
||||
//! revisions, checked against the inventory, the entity store and the corpus.
|
||||
|
||||
mod common;
|
||||
use common::{cast, eid, entities_in, grade, item_in, lab};
|
||||
use weksa::inventory::{proof_revision, seed, speaker_scene_revision};
|
||||
use weksa::packet::{FeatValue, FeatureValue};
|
||||
use weksa::speaker::{
|
||||
Annotator, MAX_PARTICIPANTS, PairRelation, Party, Scene, Speaker, scene_key, speaker_key,
|
||||
};
|
||||
use weksa::{
|
||||
AddOutcome, CorpusStore, EntityStore, InventoryStore, Refusal, SpeakerStateStore, StoreError,
|
||||
StoreRefusal,
|
||||
};
|
||||
|
||||
struct W {
|
||||
dir: tempfile::TempDir,
|
||||
inventory: InventoryStore,
|
||||
corpus: CorpusStore,
|
||||
entities: EntityStore,
|
||||
state: SpeakerStateStore,
|
||||
}
|
||||
|
||||
/// Group `g` holds two live items (speakers nibu and brokkr) and one withdrawn
|
||||
/// item (speaker hero); group `g2` holds one live item of nibu's.
|
||||
fn world() -> W {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut inventory = InventoryStore::open(&dir.path().join("i.cc")).unwrap();
|
||||
for (rev, entries) in [seed(), proof_revision(), speaker_scene_revision()] {
|
||||
inventory.admit_revision(rev, entries).unwrap();
|
||||
}
|
||||
let entities = entities_in(dir.path());
|
||||
let mut corpus = CorpusStore::open(&dir.path().join("c.cc")).unwrap();
|
||||
for (key, text, speaker) in [
|
||||
("item:nibu:1", "Come here.", "nibu"),
|
||||
("item:nibu:2", "Go there.", "brokkr"),
|
||||
("item:nibu:3", "Stay away.", "hero"),
|
||||
("item:nibu:4", "Elsewhere.", "g2"),
|
||||
] {
|
||||
let mut item = item_in(key, text, "g", cast());
|
||||
item.speaker = eid(speaker);
|
||||
if key == "item:nibu:4" {
|
||||
item.group = "g2".into();
|
||||
item.speaker = eid("nibu");
|
||||
}
|
||||
corpus.add(item, &entities).unwrap();
|
||||
}
|
||||
corpus.withdraw("item:nibu:3").unwrap();
|
||||
let state = SpeakerStateStore::open(&dir.path().join("s.cc")).unwrap();
|
||||
W {
|
||||
dir,
|
||||
inventory,
|
||||
corpus,
|
||||
entities,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
fn feat(gloss: &str, value: FeatValue) -> FeatureValue {
|
||||
let feature = seed()
|
||||
.1
|
||||
.into_iter()
|
||||
.chain(speaker_scene_revision().1)
|
||||
.find(|e| e.gloss == gloss)
|
||||
.unwrap_or_else(|| panic!("no feature {gloss}"))
|
||||
.id;
|
||||
FeatureValue { feature, value }
|
||||
}
|
||||
|
||||
fn speaker(n: u32, age: &str) -> Speaker {
|
||||
Speaker {
|
||||
entity: eid("nibu"),
|
||||
n,
|
||||
inventory_rev: 3,
|
||||
features: vec![feat("age_cohort", FeatValue::Option(lab(age)))],
|
||||
annotator: Annotator::Operator,
|
||||
}
|
||||
}
|
||||
|
||||
fn scene(n: u32, intimacy: u16) -> Scene {
|
||||
Scene {
|
||||
group: "g".into(),
|
||||
n,
|
||||
inventory_rev: 3,
|
||||
participants: vec![eid("nibu"), eid("brokkr")],
|
||||
relations: vec![PairRelation {
|
||||
from: eid("nibu"),
|
||||
to: Party::Named(eid("brokkr")),
|
||||
features: vec![feat("intimacy", FeatValue::Grade(grade(intimacy)))],
|
||||
}],
|
||||
annotator: Annotator::Operator,
|
||||
}
|
||||
}
|
||||
|
||||
fn put_speaker(w: &mut W, s: Speaker) -> Result<AddOutcome, StoreError> {
|
||||
w.state.put_speaker(s, &w.inventory, &w.entities)
|
||||
}
|
||||
|
||||
fn put_scene(w: &mut W, s: Scene) -> Result<AddOutcome, StoreError> {
|
||||
w.state.put_scene(s, &w.inventory, &w.corpus, &w.entities)
|
||||
}
|
||||
|
||||
fn refused<T: std::fmt::Debug>(r: Result<T, StoreError>) -> StoreRefusal {
|
||||
match r.unwrap_err() {
|
||||
StoreError::Refused(r) => r,
|
||||
other => panic!("not a store refusal: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn malformed(r: Result<AddOutcome, StoreError>, key: &str, why: &str) {
|
||||
match refused(r) {
|
||||
StoreRefusal::Malformed { key: k, why: w } => assert_eq!((k.as_str(), w), (key, why)),
|
||||
other => panic!("not Malformed: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn rules(r: Result<AddOutcome, StoreError>) -> Vec<&'static str> {
|
||||
match r.unwrap_err() {
|
||||
StoreError::Invalid(rs) => rs.iter().map(Refusal::rule).collect(),
|
||||
other => panic!("not a rule refusal: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn not_next(key: &str, expected: u32, got: u32) -> StoreRefusal {
|
||||
StoreRefusal::RevisionNotNext {
|
||||
key: key.into(),
|
||||
expected,
|
||||
got,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_derive_from_the_document() {
|
||||
assert_eq!(speaker_key(&eid("nibu"), 2), "speaker:nibu@2");
|
||||
assert_eq!(scene_key("g", 3), "scene:g@3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speaker_revisions_append() {
|
||||
let mut w = world();
|
||||
let key = |n| speaker_key(&eid("nibu"), n);
|
||||
assert_eq!(
|
||||
refused(put_speaker(&mut w, speaker(2, "adult"))),
|
||||
not_next(&key(2), 1, 2)
|
||||
);
|
||||
assert!(w.state.speaker(&eid("nibu")).unwrap().is_none());
|
||||
assert_eq!(
|
||||
put_speaker(&mut w, speaker(1, "adult")).unwrap(),
|
||||
AddOutcome::Added
|
||||
);
|
||||
assert_eq!(
|
||||
put_speaker(&mut w, speaker(1, "adult")).unwrap(),
|
||||
AddOutcome::Unchanged
|
||||
);
|
||||
assert_eq!(
|
||||
refused(put_speaker(&mut w, speaker(1, "elder"))),
|
||||
not_next(&key(1), 2, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
refused(put_speaker(&mut w, speaker(3, "elder"))),
|
||||
not_next(&key(3), 2, 3)
|
||||
);
|
||||
assert_eq!(
|
||||
put_speaker(&mut w, speaker(2, "elder")).unwrap(),
|
||||
AddOutcome::Added
|
||||
);
|
||||
assert_eq!(
|
||||
w.state.speaker(&eid("nibu")).unwrap(),
|
||||
Some(speaker(2, "elder"))
|
||||
);
|
||||
assert_eq!(
|
||||
w.state.get_speaker(&key(1)).unwrap(),
|
||||
Some(speaker(1, "adult"))
|
||||
);
|
||||
// Another entity's revisions count from 1 and stay apart.
|
||||
let mut other = speaker(1, "child");
|
||||
other.entity = eid("brokkr");
|
||||
assert_eq!(put_speaker(&mut w, other).unwrap(), AddOutcome::Added);
|
||||
assert_eq!(w.state.speaker(&eid("nibu")).unwrap().unwrap().n, 2);
|
||||
// Revision 10 sorts after 2 in the listing.
|
||||
for n in 3..=10 {
|
||||
put_speaker(&mut w, speaker(n, "adult")).unwrap();
|
||||
}
|
||||
let listed: Vec<_> = w
|
||||
.state
|
||||
.all_speakers()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|s| (s.entity.to_string(), s.n))
|
||||
.collect();
|
||||
assert_eq!(listed.len(), 11);
|
||||
assert_eq!(listed[0], ("entity:brokkr".into(), 1));
|
||||
assert_eq!(listed[10], ("entity:nibu".into(), 10));
|
||||
assert_eq!(w.state.speaker(&eid("nibu")).unwrap().unwrap().n, 10);
|
||||
// The documents survive a reopen.
|
||||
let again = SpeakerStateStore::open(&w.dir.path().join("s.cc")).unwrap();
|
||||
assert_eq!(again.all_speakers().unwrap().len(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_revisions_append() {
|
||||
let mut w = world();
|
||||
let key = |n| scene_key("g", n);
|
||||
assert_eq!(
|
||||
refused(put_scene(&mut w, scene(2, 5))),
|
||||
not_next(&key(2), 1, 2)
|
||||
);
|
||||
assert!(w.state.scene("g").unwrap().is_none());
|
||||
assert_eq!(put_scene(&mut w, scene(1, 5)).unwrap(), AddOutcome::Added);
|
||||
assert_eq!(
|
||||
put_scene(&mut w, scene(1, 5)).unwrap(),
|
||||
AddOutcome::Unchanged
|
||||
);
|
||||
assert_eq!(
|
||||
refused(put_scene(&mut w, scene(1, 6))),
|
||||
not_next(&key(1), 2, 1)
|
||||
);
|
||||
assert_eq!(put_scene(&mut w, scene(2, 6)).unwrap(), AddOutcome::Added);
|
||||
assert_eq!(w.state.scene("g").unwrap(), Some(scene(2, 6)));
|
||||
assert_eq!(w.state.get_scene(&key(1)).unwrap(), Some(scene(1, 5)));
|
||||
for n in 3..=10 {
|
||||
put_scene(&mut w, scene(n, 6)).unwrap();
|
||||
}
|
||||
assert_eq!(w.state.scene("g").unwrap().unwrap().n, 10);
|
||||
let listed: Vec<u32> = w.state.all_scenes().unwrap().iter().map(|s| s.n).collect();
|
||||
assert_eq!(listed, (1..=10).collect::<Vec<_>>());
|
||||
// Numbering is per group: the first scene of another group is revision 1
|
||||
// whatever g holds, and its second is 2.
|
||||
let other = |n| Scene {
|
||||
group: "g2".into(),
|
||||
participants: vec![eid("nibu")],
|
||||
relations: vec![],
|
||||
..scene(n, 1)
|
||||
};
|
||||
assert_eq!(
|
||||
refused(put_scene(&mut w, other(2))),
|
||||
not_next(&scene_key("g2", 2), 1, 2)
|
||||
);
|
||||
assert_eq!(put_scene(&mut w, other(1)).unwrap(), AddOutcome::Added);
|
||||
assert_eq!(
|
||||
refused(put_scene(&mut w, other(3))),
|
||||
not_next(&scene_key("g2", 3), 2, 3)
|
||||
);
|
||||
assert_eq!(put_scene(&mut w, other(2)).unwrap(), AddOutcome::Added);
|
||||
assert_eq!(w.state.scene("g2").unwrap().unwrap().n, 2);
|
||||
assert_eq!(w.state.scene("g").unwrap().unwrap().n, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speaker_needs_a_known_entity() {
|
||||
let mut w = world();
|
||||
let mut s = speaker(1, "adult");
|
||||
s.entity = eid("nobody");
|
||||
assert_eq!(
|
||||
refused(put_speaker(&mut w, s)),
|
||||
StoreRefusal::UnknownEntity {
|
||||
entity: eid("nobody")
|
||||
}
|
||||
);
|
||||
assert!(w.state.all_speakers().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotation_features_are_checked_like_packet_features() {
|
||||
let mut w = world();
|
||||
let age = || feat("age_cohort", FeatValue::Option(lab("adult")));
|
||||
let with = |features: Vec<FeatureValue>, rev| Speaker {
|
||||
features,
|
||||
inventory_rev: rev,
|
||||
..speaker(1, "adult")
|
||||
};
|
||||
// A Scene feature and a Stance feature do not apply to a speaker.
|
||||
let f = vec![feat("intimacy", FeatValue::Grade(grade(1)))];
|
||||
assert_eq!(rules(put_speaker(&mut w, with(f, 3))), ["V8"]);
|
||||
let f = vec![feat("anger", FeatValue::Grade(grade(1)))];
|
||||
assert_eq!(rules(put_speaker(&mut w, with(f, 3))), ["V8"]);
|
||||
// A wrong kind and an undeclared label.
|
||||
let f = vec![feat("education", FeatValue::Flag(true))];
|
||||
assert_eq!(rules(put_speaker(&mut w, with(f, 3))), ["V7"]);
|
||||
let f = vec![feat("age_cohort", FeatValue::Option(lab("ancient")))];
|
||||
assert_eq!(rules(put_speaker(&mut w, with(f, 3))), ["V7"]);
|
||||
// Feature 22 does not exist at revision 2; revision 9 has no record.
|
||||
assert_eq!(rules(put_speaker(&mut w, with(vec![age()], 2))), ["V5"]);
|
||||
assert_eq!(rules(put_speaker(&mut w, with(vec![age()], 9))), ["V10"]);
|
||||
// A repeat.
|
||||
assert_eq!(
|
||||
rules(put_speaker(&mut w, with(vec![age(), age()], 3))),
|
||||
["V11"]
|
||||
);
|
||||
// A Speaker feature in a scene relation.
|
||||
let mut s = scene(1, 1);
|
||||
s.relations[0].features = vec![age()];
|
||||
assert_eq!(rules(put_scene(&mut w, s)), ["V8"]);
|
||||
// Every relation is checked, and a scene of an unknown revision is V10.
|
||||
let mut s = scene(1, 1);
|
||||
s.relations.push(PairRelation {
|
||||
from: eid("brokkr"),
|
||||
to: Party::Unnamed,
|
||||
features: vec![age()],
|
||||
});
|
||||
assert_eq!(rules(put_scene(&mut w, s)), ["V8"]);
|
||||
let mut s = scene(1, 1);
|
||||
s.inventory_rev = 9;
|
||||
assert_eq!(rules(put_scene(&mut w, s)), ["V10"]);
|
||||
let mut s = scene(1, 1);
|
||||
s.inventory_rev = 9;
|
||||
s.relations.clear();
|
||||
assert_eq!(rules(put_scene(&mut w, s)), ["V10"]);
|
||||
assert!(w.state.all_speakers().unwrap().is_empty());
|
||||
assert!(w.state.all_scenes().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_shape_rules() {
|
||||
let mut w = world();
|
||||
let key = scene_key("g", 1);
|
||||
let relation = |from: &str, to: Party| PairRelation {
|
||||
from: eid(from),
|
||||
to,
|
||||
features: vec![],
|
||||
};
|
||||
let named = |to: &str| Party::Named(eid(to));
|
||||
let base = || scene(1, 1);
|
||||
|
||||
let mut s = base();
|
||||
s.group = "Not A Slug".into();
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&scene_key("Not A Slug", 1),
|
||||
"a scene group is a slug",
|
||||
);
|
||||
let mut s = base();
|
||||
s.group = "elsewhere".into();
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&scene_key("elsewhere", 1),
|
||||
"no corpus item holds the scene's group",
|
||||
);
|
||||
let mut s = base();
|
||||
s.participants.clear();
|
||||
s.relations.clear();
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&key,
|
||||
"a scene has 1 to 64 participants",
|
||||
);
|
||||
// 64 participants are fine as far as the count goes (the rest fail later);
|
||||
// 65 are refused for the count.
|
||||
let many = |count: usize| -> Vec<_> { (0..count).map(|i| eid(&format!("p{i}"))).collect() };
|
||||
let mut s = base();
|
||||
s.participants = many(MAX_PARTICIPANTS + 1);
|
||||
s.relations.clear();
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&key,
|
||||
"a scene has 1 to 64 participants",
|
||||
);
|
||||
let mut s = base();
|
||||
s.participants = many(MAX_PARTICIPANTS);
|
||||
s.relations.clear();
|
||||
assert!(matches!(
|
||||
refused(put_scene(&mut w, s)),
|
||||
StoreRefusal::UnknownEntity { .. }
|
||||
));
|
||||
let mut s = base();
|
||||
s.participants.push(eid("nibu"));
|
||||
malformed(put_scene(&mut w, s), &key, "a participant is named once");
|
||||
let mut s = base();
|
||||
s.participants.push(eid("nobody"));
|
||||
assert_eq!(
|
||||
refused(put_scene(&mut w, s)),
|
||||
StoreRefusal::UnknownEntity {
|
||||
entity: eid("nobody")
|
||||
}
|
||||
);
|
||||
let mut s = base();
|
||||
s.relations = vec![relation("hero", named("nibu"))];
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&key,
|
||||
"a relation's from is a participant",
|
||||
);
|
||||
let mut s = base();
|
||||
s.relations = vec![relation("nibu", named("hero"))];
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&key,
|
||||
"a relation's named to is another participant",
|
||||
);
|
||||
let mut s = base();
|
||||
s.relations = vec![relation("nibu", named("nibu"))];
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&key,
|
||||
"a relation's named to is another participant",
|
||||
);
|
||||
let mut s = base();
|
||||
s.relations = vec![
|
||||
relation("nibu", named("brokkr")),
|
||||
relation("nibu", named("brokkr")),
|
||||
];
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&key,
|
||||
"an ordered pair has one relation",
|
||||
);
|
||||
let mut s = base();
|
||||
s.relations = vec![
|
||||
relation("nibu", Party::Unnamed),
|
||||
relation("nibu", Party::Unnamed),
|
||||
];
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&key,
|
||||
"an ordered pair has one relation",
|
||||
);
|
||||
// The reverse pair, and an unnamed party beside a named one, are distinct.
|
||||
let mut s = base();
|
||||
s.relations = vec![
|
||||
relation("nibu", named("brokkr")),
|
||||
relation("brokkr", named("nibu")),
|
||||
relation("nibu", Party::Unnamed),
|
||||
];
|
||||
assert_eq!(put_scene(&mut w, s).unwrap(), AddOutcome::Added);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scene_holds_every_live_speaker_of_its_group() {
|
||||
let mut w = world();
|
||||
// Brokkr speaks a live item of the group.
|
||||
let mut s = scene(1, 1);
|
||||
s.participants = vec![eid("nibu")];
|
||||
s.relations.clear();
|
||||
assert_eq!(
|
||||
refused(put_scene(&mut w, s)),
|
||||
StoreRefusal::SceneSpeakerMissing {
|
||||
group: "g".into(),
|
||||
entity: eid("brokkr")
|
||||
}
|
||||
);
|
||||
// Hero speaks only a withdrawn item, so the scene may omit them.
|
||||
assert_eq!(put_scene(&mut w, scene(1, 1)).unwrap(), AddOutcome::Added);
|
||||
// The group is held by a withdrawn item alone: still a group.
|
||||
w.corpus.withdraw("item:nibu:1").unwrap();
|
||||
w.corpus.withdraw("item:nibu:2").unwrap();
|
||||
let mut s = scene(2, 1);
|
||||
s.participants = vec![eid("player")];
|
||||
s.relations.clear();
|
||||
assert_eq!(put_scene(&mut w, s).unwrap(), AddOutcome::Added);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_annotator_attests_its_generator() {
|
||||
let mut w = world();
|
||||
let model = |generator: &str| Annotator::Model {
|
||||
family: weksa::corpus::ModelFamily::Anthropic,
|
||||
generator: generator.into(),
|
||||
};
|
||||
let key = speaker_key(&eid("nibu"), 1);
|
||||
for bad in ["", " ", "gen\u{7}erator", "gen\nerator", "\u{200d}"] {
|
||||
let mut s = speaker(1, "adult");
|
||||
s.annotator = model(bad);
|
||||
malformed(
|
||||
put_speaker(&mut w, s),
|
||||
&key,
|
||||
"a model annotator attests its generator",
|
||||
);
|
||||
let mut s = scene(1, 1);
|
||||
s.annotator = model(bad);
|
||||
malformed(
|
||||
put_scene(&mut w, s),
|
||||
&scene_key("g", 1),
|
||||
"a model annotator attests its generator",
|
||||
);
|
||||
}
|
||||
let mut s = speaker(1, "adult");
|
||||
s.annotator = model(&"x".repeat(201));
|
||||
malformed(
|
||||
put_speaker(&mut w, s),
|
||||
&key,
|
||||
"a model annotator attests its generator",
|
||||
);
|
||||
let mut s = speaker(1, "adult");
|
||||
s.annotator = model("claude-sonnet 5.5");
|
||||
assert_eq!(put_speaker(&mut w, s).unwrap(), AddOutcome::Added);
|
||||
let mut s = scene(1, 1);
|
||||
s.annotator = model("claude-sonnet 5.5");
|
||||
assert_eq!(put_scene(&mut w, s).unwrap(), AddOutcome::Added);
|
||||
let mut s = speaker(2, "adult");
|
||||
s.annotator = Annotator::Operator;
|
||||
assert_eq!(put_speaker(&mut w, s).unwrap(), AddOutcome::Added);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refusal_displays_print_numbers_and_no_identifiers() {
|
||||
let r = not_next("speaker:secretname@2", 1, 2);
|
||||
assert_eq!(r.to_string(), "RevisionNotNext expected 1 got 2");
|
||||
let r = StoreRefusal::SceneSpeakerMissing {
|
||||
group: "secretgroup".into(),
|
||||
entity: eid("secretname"),
|
||||
};
|
||||
assert_eq!(r.to_string(), "SceneSpeakerMissing");
|
||||
}
|
||||
|
|
@ -1093,6 +1093,51 @@ fn revision_change_rules() {
|
|||
assert!(inv.entry(&iv("role:1")).unwrap().retired_rev.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speaker_state_slots_are_exclusive() {
|
||||
let shape = "entry shape does not match its family";
|
||||
let d = dir();
|
||||
let mut store = seeded(&d.path().join("i.cc"));
|
||||
for slots in [
|
||||
&[Slot::Stance, Slot::Discourse][..],
|
||||
&[Slot::Scene, Slot::Relation],
|
||||
&[Slot::Discourse, Slot::Scene],
|
||||
&[Slot::Scene, Slot::Scene],
|
||||
&[Slot::Speaker, Slot::Scene],
|
||||
&[Slot::Speaker, Slot::Stance],
|
||||
&[Slot::Speaker, Slot::Relation],
|
||||
&[Slot::Speaker, Slot::Speaker],
|
||||
&[Slot::Stance, Slot::Stance],
|
||||
] {
|
||||
let rev = rev_with(vec![Change::Add(iv("feature:50"))], 2, Some(1));
|
||||
let entries = vec![InventoryEntry {
|
||||
introduced_rev: 2,
|
||||
..feature("feature:50", ValueKind::Flag, slots)
|
||||
}];
|
||||
assert_eq!(
|
||||
refusal(store.admit_revision(rev, entries)),
|
||||
malformed("feature:50", shape),
|
||||
"{slots:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(store.inventory().unwrap().latest().unwrap().n, 1);
|
||||
// One slot alone, and several content slots, are admitted.
|
||||
for (n, slots) in [
|
||||
(2, &[Slot::Scene][..]),
|
||||
(3, &[Slot::Stance]),
|
||||
(4, &[Slot::Speaker]),
|
||||
(5, &[Slot::Discourse, Slot::Predication]),
|
||||
] {
|
||||
let id = format!("feature:{}", 50 + n);
|
||||
let rev = rev_with(vec![Change::Add(iv(&id))], n, Some(n - 1));
|
||||
let entries = vec![InventoryEntry {
|
||||
introduced_rev: n,
|
||||
..feature(&id, ValueKind::Flag, slots)
|
||||
}];
|
||||
store.admit_revision(rev, entries).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_well_formed_extension_entry_is_admitted() {
|
||||
let d = dir();
|
||||
|
|
|
|||
|
|
@ -520,9 +520,6 @@ fn v11_duplicate_feature_each_slot() {
|
|||
refusals(&relation),
|
||||
vec![v11(Some(Slot::Relation), FEAT_ALL)]
|
||||
);
|
||||
let mut stance = good();
|
||||
stance.stance.features = vec![grade_feature(1), grade_feature(2)];
|
||||
assert_eq!(refusals(&stance), vec![v11(Some(Slot::Stance), FEAT_ALL)]);
|
||||
let mut felt = good();
|
||||
felt.stance
|
||||
.features
|
||||
|
|
@ -742,3 +739,26 @@ fn a_source_that_only_read_wording_cannot_set_consent() {
|
|||
assert_eq!(refusals(&p), vec![Refusal::V15ConsentSource { source }]);
|
||||
}
|
||||
}
|
||||
|
||||
// V17: the addressee is someone spoken to, a referent.
|
||||
|
||||
#[test]
|
||||
fn the_addressee_names_a_referent_not_a_predication() {
|
||||
let mut p = good();
|
||||
p.discourse.addressee = Some(nd("n2"));
|
||||
assert_eq!(
|
||||
refusals(&p),
|
||||
vec![Refusal::V17AddresseeNotAReferent { id: nd("n2") }]
|
||||
);
|
||||
// A referent passes, and a dangling id is V4's alone.
|
||||
p.discourse.addressee = Some(nd("n1"));
|
||||
assert_eq!(refusals(&p), vec![]);
|
||||
p.discourse.addressee = Some(nd("n99"));
|
||||
assert_eq!(
|
||||
refusals(&p),
|
||||
vec![Refusal::V4DanglingRef {
|
||||
id: nd("n99"),
|
||||
at: "addressee"
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ names the rule.
|
|||
- V15: an `addressee_consent` is set only by provenance `Authored` or
|
||||
`GameTelemetry`; a decomposition of text or imported corpus text cannot mint
|
||||
it.
|
||||
- V17: an `addressee` names a referent, not a predication.
|
||||
|
||||
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
|
||||
|
|
@ -190,7 +191,7 @@ ids.
|
|||
|
||||
### `discourse`
|
||||
|
||||
Why the packet is said: `speech_act` is a speech-act id, `addressee` a node id,
|
||||
Why the packet is said: `speech_act` is a speech-act id, `addressee` a referent's node id (V17),
|
||||
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
|
||||
|
|
@ -213,6 +214,27 @@ affect and restraint to its own register. A held-back feeling is not a claim
|
|||
about the world, so it is never a predication. An empty stance is valid. The
|
||||
stance reaches the lowerer in the lowering view.
|
||||
|
||||
### Speaker and scene documents
|
||||
|
||||
Two documents outside the packet hold the speaker's standing, in the store that
|
||||
owns them (`SpeakerStateStore`). `weksa.speaker` (key `speaker:<entity slug>@<n>`)
|
||||
holds one entity's stable axes as feature values of slot `Speaker`.
|
||||
`weksa.scene` (key `scene:<group>@<n>`) holds a corpus group's participants and,
|
||||
per ordered pair, the speaker's relation to a party (slot `Scene`); the party is
|
||||
a named participant or `Party::Unnamed`, an addressee the scene does not name,
|
||||
such as the player or a crowd. A revision appends and never edits: `n` is the
|
||||
latest plus one, and the latest revision is the highest `n`. Each carries an
|
||||
`Annotator`, `Operator` or `Model` with an attestation string for its generator.
|
||||
Both hold typed feature values only, never text. A feature of slot `Stance`,
|
||||
`Speaker` or `Scene` sits in that slot alone (each layer has one writer and one
|
||||
lifetime), which `admit_revision` enforces. `LoweringInput::with_scene` takes the
|
||||
store and names neither speaker nor scene: the view knows its corpus item, so
|
||||
the speaker is the item's speaker and the scene is the store's latest for the
|
||||
item's group. The speaker must be a participant (V16), and the relation
|
||||
selected is the speaker's to the entity of the addressee referent (to
|
||||
`Unnamed` when that referent has no entity, to nothing when the packet has no
|
||||
addressee or the scene holds no such relation). V13 is retired.
|
||||
|
||||
### `referents`
|
||||
|
||||
Entities, groups, places, abstractions and the rest. `kind` is a referent-kind
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ plus a separately recorded chosen gloss per lowering).
|
|||
- `data/daodejing/`: its own stores (inventory revisions 1 to 3, one entity, five
|
||||
corpus items, five packets), apart from the production stores in `data/`.
|
||||
Revision 3 is `weksa::daodejing::revision()`; the packets are
|
||||
`weksa::daodejing::packets()`. The scene revision (`scene_revision()`, also
|
||||
`weksa::daodejing::packets()`. The scene revision (`speaker_scene_revision()`, also
|
||||
numbered 3) is not applied in these stores and will need renumbering when it
|
||||
is applied anywhere both meet.
|
||||
- Rebuild: `weksa --corpus C --inventory I --entities E` with `inventory seed`,
|
||||
|
|
|
|||
|
|
@ -263,7 +263,8 @@ operator-attested annotations alone. Who annotates is question
|
|||
- Seed inventory (2026-11-10, base `5e10d7c`): features 6-8 are hedge, certainty
|
||||
and irony; 9-18 are the stance affects and restraint (slot Stance). The
|
||||
committed inventory holds revisions 1-2 only. Nothing calls `with_scene`
|
||||
except tests, and the proof packet's addressee carries no entity.
|
||||
except tests, and the proof packet's addressee carries no entity (its
|
||||
referent `n3` does: `entity:nibu`).
|
||||
- Yggdrasil's Ollama (`127.0.0.1:11434`) is the Docker container `ollama-cpu`,
|
||||
Huginn's embedder (runbook `gamecult-ops/runbooks/huginn-yggdrasil.md`,
|
||||
ruling `eureka-body:ruling:huginn-embedder-yggdrasil-primary`). `/api/embed`
|
||||
|
|
@ -398,8 +399,8 @@ 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` (spec r1; `speaker-scene-seeds` is its cut 4c: CLI, inventory revision 3, cast speakers and scenes; `Party::Unnamed` covers an addressee with no entity; Speaker and Scene share one `SpeakerStateStore`): 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.
|
||||
4a. `speaker-relation-input`: slot `Scene` and its first seed features (`relative_status`, `intimacy`, `in_group`; inventory revision 3, now `speaker_scene_revision`); `LoweringInput::with_scene` carries the speaker's relation to the addressee as typed `Scene` feature values. Since 4b it takes the `SpeakerStateStore`, reads the scene of the view's own item group for the item's speaker, and selects the relation by the addressee referent's entity (V16 when the item's speaker is not a participant; V17 refuses an addressee that names a predication; V13 is retired). The English lowerer does not render it. (PR for follow-up `chat-s3352-3352`)
|
||||
4b. `speaker-scene` (spec r1; `speaker-scene-seeds` is its cut 4c: CLI, inventory revision 3, cast speakers and scenes; `Party::Unnamed` covers an addressee with no entity; Speaker and Scene share one `SpeakerStateStore`): 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. Part 1 landed (branch `weksa-lowering/speaker-scene`): `speaker.rs`, `SpeakerStateStore`, slots `Speaker` and `Scene` exclusive to their own feature lists, `speaker_scene_revision()` (Speaker features 22-24), `with_scene` on the store, schemas. The CLI, the committed inventory revision 3 and the seeds stay with 4c.
|
||||
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`.
|
||||
7a. `loop-context-builder`: `context.rs` (`TrainItems`, `EncoderOffer`, `Prompt`: corpus text reaches a model call only through `Prompt::mint` over live Train items or `Prompt::encode` over one offered item; instructions are the consts `MINT_INSTRUCTIONS` and `ENCODE_INSTRUCTIONS` in `context.rs`; `Checkpoint` has no public constructor, offers each item once, and its offers borrow it (the gate is typed `TestReadings` and Test offers on the round path; operator tools such as `weksa corpus dump` and `weksa score` still read corpus text of any split); prior turns print the held Train text), `SplitScheme::live`, `CorpusItem::cited_entities` (one owner of the id set `PacketStore::admit` and the offer share), and checkpoint-only Test readings (`Checkpoint`, `TestReadings`; `Aggregate` and `leak_diagnostic` carry no Test). Library only. Merges follow-ups `loop-context-builder-2` and `scorer-test-readings-type`. (spec r1)
|
||||
|
|
|
|||
|
|
@ -81,9 +81,14 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"description": "The relation of the speaker to the addressee in the scene; see\n`LoweringInput::with_scene`.",
|
||||
"description": "The speaker's relation to one party of the scene, held by `weksa.scene`\ndocuments; see `LoweringInput::with_scene`.",
|
||||
"type": "string",
|
||||
"const": "Scene"
|
||||
},
|
||||
{
|
||||
"description": "The speaker's stable axes, held by `weksa.speaker` documents.",
|
||||
"type": "string",
|
||||
"const": "Speaker"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
224
schemas/cultnet/weksa.scene.schema.json
Normal file
224
schemas/cultnet/weksa.scene.schema.json
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Scene",
|
||||
"description": "A scene: its participants and the relations between them. Key\n`scene:<group>@<n>`; a revision appends and never edits.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotator": {
|
||||
"$ref": "#/$defs/Annotator"
|
||||
},
|
||||
"group": {
|
||||
"description": "The corpus split group of the scene, a slug.",
|
||||
"type": "string"
|
||||
},
|
||||
"inventory_rev": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
},
|
||||
"n": {
|
||||
"description": "The revision, from 1.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
},
|
||||
"participants": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/EntityId"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PairRelation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"group",
|
||||
"n",
|
||||
"inventory_rev",
|
||||
"participants",
|
||||
"relations",
|
||||
"annotator"
|
||||
],
|
||||
"$defs": {
|
||||
"Annotator": {
|
||||
"description": "Who annotated a document. A model's generator is an attestation string, held\nto the same form as every provenance string.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Operator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Model": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"family": {
|
||||
"$ref": "#/$defs/ModelFamily"
|
||||
},
|
||||
"generator": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"family",
|
||||
"generator"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Model"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"EntityId": {
|
||||
"type": "string",
|
||||
"pattern": "^entity:[a-z0-9]([a-z0-9-]{0,46}[a-z0-9])?$"
|
||||
},
|
||||
"FeatValue": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Grade": {
|
||||
"$ref": "#/$defs/Grade"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Grade"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A label the feature's inventory entry declares.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Option": {
|
||||
"$ref": "#/$defs/OptionLabel"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Option"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Flag": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Flag"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"FeatureValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"feature": {
|
||||
"description": "Feature id.",
|
||||
"$ref": "#/$defs/InvId"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/$defs/FeatValue"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"feature",
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"Grade": {
|
||||
"type": "integer",
|
||||
"maximum": 1000,
|
||||
"minimum": 0
|
||||
},
|
||||
"InvId": {
|
||||
"type": "string",
|
||||
"pattern": "^(concept|frame|role|relation|speech_act|feature|referent_kind|extension):[1-9][0-9]*$"
|
||||
},
|
||||
"ModelFamily": {
|
||||
"description": "The family of the model that wrote a line.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Anthropic",
|
||||
"OpenAi",
|
||||
"Google",
|
||||
"Alibaba",
|
||||
"Meta",
|
||||
"Mistral"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Nothing records which family wrote the line.",
|
||||
"type": "string",
|
||||
"const": "Unrecorded"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OptionLabel": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]{0,23}$"
|
||||
},
|
||||
"PairRelation": {
|
||||
"description": "The speaker's relation to one party: feature values of slot `Scene`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"features": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/FeatureValue"
|
||||
}
|
||||
},
|
||||
"from": {
|
||||
"$ref": "#/$defs/EntityId"
|
||||
},
|
||||
"to": {
|
||||
"$ref": "#/$defs/Party"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"from",
|
||||
"to",
|
||||
"features"
|
||||
]
|
||||
},
|
||||
"Party": {
|
||||
"description": "The other party of a relation. `Unnamed` is an addressee the scene does not\nname, such as the player or a crowd.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Unnamed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Named": {
|
||||
"$ref": "#/$defs/EntityId"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Named"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
171
schemas/cultnet/weksa.speaker.schema.json
Normal file
171
schemas/cultnet/weksa.speaker.schema.json
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Speaker",
|
||||
"description": "The speaker's stable axes. Key `speaker:<entity slug>@<n>`; a revision\nappends and never edits.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotator": {
|
||||
"$ref": "#/$defs/Annotator"
|
||||
},
|
||||
"entity": {
|
||||
"$ref": "#/$defs/EntityId"
|
||||
},
|
||||
"features": {
|
||||
"description": "Feature values of slot `Speaker`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/FeatureValue"
|
||||
}
|
||||
},
|
||||
"inventory_rev": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
},
|
||||
"n": {
|
||||
"description": "The revision, from 1.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entity",
|
||||
"n",
|
||||
"inventory_rev",
|
||||
"features",
|
||||
"annotator"
|
||||
],
|
||||
"$defs": {
|
||||
"Annotator": {
|
||||
"description": "Who annotated a document. A model's generator is an attestation string, held\nto the same form as every provenance string.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Operator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Model": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"family": {
|
||||
"$ref": "#/$defs/ModelFamily"
|
||||
},
|
||||
"generator": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"family",
|
||||
"generator"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Model"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"EntityId": {
|
||||
"type": "string",
|
||||
"pattern": "^entity:[a-z0-9]([a-z0-9-]{0,46}[a-z0-9])?$"
|
||||
},
|
||||
"FeatValue": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Grade": {
|
||||
"$ref": "#/$defs/Grade"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Grade"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A label the feature's inventory entry declares.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Option": {
|
||||
"$ref": "#/$defs/OptionLabel"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Option"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Flag": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Flag"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"FeatureValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"feature": {
|
||||
"description": "Feature id.",
|
||||
"$ref": "#/$defs/InvId"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/$defs/FeatValue"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"feature",
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"Grade": {
|
||||
"type": "integer",
|
||||
"maximum": 1000,
|
||||
"minimum": 0
|
||||
},
|
||||
"InvId": {
|
||||
"type": "string",
|
||||
"pattern": "^(concept|frame|role|relation|speech_act|feature|referent_kind|extension):[1-9][0-9]*$"
|
||||
},
|
||||
"ModelFamily": {
|
||||
"description": "The family of the model that wrote a line.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Anthropic",
|
||||
"OpenAi",
|
||||
"Google",
|
||||
"Alibaba",
|
||||
"Meta",
|
||||
"Mistral"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Nothing records which family wrote the line.",
|
||||
"type": "string",
|
||||
"const": "Unrecorded"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OptionLabel": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]{0,23}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue