Speaker and Scene documents, SpeakerStateStore, with_scene on a Scene document
Slot Speaker, weksa.speaker and weksa.scene append-only revisions in one store, V16, feature_refusals as the one feature check, speaker_scene_revision (Speaker features 22-24), schemas, docs and map lines. Part 1 of map cut 4b. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0b62d2f3b4
commit
6ef1b7e034
16 changed files with 1638 additions and 38 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)]
|
||||
|
|
@ -229,7 +231,7 @@ 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. A
|
||||
/// feature of slot `Stance` or `Scene` sits in that slot alone: each
|
||||
/// 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 {
|
||||
|
|
@ -241,7 +243,7 @@ impl InventoryEntry {
|
|||
|| !self
|
||||
.applies_to
|
||||
.iter()
|
||||
.any(|s| matches!(s, Slot::Stance | Slot::Scene)))
|
||||
.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(),
|
||||
|
|
@ -534,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()
|
||||
|
|
@ -564,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};
|
||||
|
|
|
|||
|
|
@ -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>(),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
115
crates/weksa/src/speaker.rs
Normal file
115
crates/weksa/src/speaker.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
//! 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; empty when the scene holds no such
|
||||
/// relation. A named addressee the scene lacks never falls back to `Unnamed`.
|
||||
pub fn relation(&self, speaker: &EntityId, addressee: Option<&EntityId>) -> &[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_or(&[], |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, admissible, feature_refusals};
|
||||
|
||||
/// 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}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -978,3 +994,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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
//! 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, V10..V12 and V14..V15: the
|
||||
//! list, in slot `Stance`. The rules are V1..V8, V10..V12 and V14..V16: 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.
|
||||
//! and not renumbered; V13 is retired too. [`LoweringInput::with_scene`] takes
|
||||
//! a [`Scene`](crate::speaker::Scene) document, and `feature_refusals` is the
|
||||
//! one feature check it and the speaker-state 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
|
||||
|
|
@ -27,9 +29,10 @@ use std::fmt;
|
|||
|
||||
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::speaker::Scene;
|
||||
|
||||
/// A broken rule, named.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -107,6 +110,8 @@ pub enum Refusal {
|
|||
V15ConsentSource {
|
||||
source: ProvenanceSource,
|
||||
},
|
||||
/// V16: `with_scene` was given a speaker the scene does not list.
|
||||
V16SpeakerNotInScene,
|
||||
}
|
||||
|
||||
impl Refusal {
|
||||
|
|
@ -129,6 +134,7 @@ impl Refusal {
|
|||
"V14"
|
||||
}
|
||||
Refusal::V15ConsentSource { .. } => "V15",
|
||||
Refusal::V16SpeakerNotInScene => "V16",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +160,7 @@ impl fmt::Display for Refusal {
|
|||
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",
|
||||
};
|
||||
write!(f, "{}: {what}", self.rule())
|
||||
}
|
||||
|
|
@ -465,6 +472,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,
|
||||
|
|
@ -530,6 +560,27 @@ fn fits(kind: Option<&ValueKind>, value: &FeatValue) -> bool {
|
|||
/// fn read(l: weksa::LoweringInput) { let _ = l.scene; }
|
||||
/// ```
|
||||
///
|
||||
/// Scene values come only from a `weksa.scene` document: the raw-values door
|
||||
/// is gone. The first block is the same call with a document; the second hands
|
||||
/// it feature values and fails for that alone.
|
||||
///
|
||||
/// ```
|
||||
/// 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.
|
||||
|
|
@ -587,6 +638,39 @@ impl LoweringInput {
|
|||
}
|
||||
}
|
||||
|
||||
/// Adds the speaker's relation to the addressee, the only path for scene
|
||||
/// values into a view; they come only from a `weksa.scene` document. The
|
||||
/// speaker must be a participant of `scene` (V16). With no addressee, or
|
||||
/// one that names no referent, nothing is selected; otherwise the relation
|
||||
/// from the speaker to the addressee referent's entity (to `Unnamed` when
|
||||
/// the referent has none) is checked like every feature list, in slot
|
||||
/// `Scene` at this view's inventory revision, and replaces any earlier
|
||||
/// scene. The English lowerer ignores it.
|
||||
pub fn with_scene(
|
||||
mut self,
|
||||
inventory: &Inventory,
|
||||
scene: &Scene,
|
||||
speaker: &EntityId,
|
||||
) -> Result<Self, Vec<Refusal>> {
|
||||
if !scene.participants.contains(speaker) {
|
||||
return Err(vec![Refusal::V16SpeakerNotInScene]);
|
||||
}
|
||||
let addressee = self
|
||||
.discourse
|
||||
.addressee
|
||||
.as_ref()
|
||||
.and_then(|id| self.referents.iter().find(|r| &r.id == id));
|
||||
let selected = addressee
|
||||
.map(|r| scene.relation(speaker, r.entity.as_ref()))
|
||||
.unwrap_or_default();
|
||||
let out = feature_refusals(inventory, self.inventory_rev, selected, Slot::Scene);
|
||||
if !out.is_empty() {
|
||||
return Err(out);
|
||||
}
|
||||
self.scene = selected.to_vec();
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn inventory_rev(&self) -> u32 {
|
||||
self.inventory_rev
|
||||
}
|
||||
|
|
@ -617,7 +701,8 @@ 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 until `with_scene`, or when the scene holds none.
|
||||
pub fn scene(&self) -> &[FeatureValue] {
|
||||
&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"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
244
crates/weksa/tests/scene.rs
Normal file
244
crates/weksa/tests/scene.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
//! The speaker's relation to the addressee as a lowering input: the relation of
|
||||
//! a `weksa.scene` document from the speaker to the addressee referent's
|
||||
//! entity, typed feature values of slot `Scene` checked against the inventory
|
||||
//! at the view's revision, never wording.
|
||||
|
||||
mod common;
|
||||
use std::path::Path;
|
||||
|
||||
use common::{eid, grade, iv, 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, EntityId, EntityStore, Inventory, InventoryStore, LoweringInput, PacketStore,
|
||||
Refusal, lower,
|
||||
};
|
||||
|
||||
fn store() -> (InventoryStore, Inventory, tempfile::TempDir) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = InventoryStore::open(&dir.path().join("i.cc")).unwrap();
|
||||
for (rev, entries) in [seed(), proof_revision(), speaker_scene_revision()] {
|
||||
store.admit_revision(rev, entries).unwrap();
|
||||
}
|
||||
let inv = store.inventory().unwrap();
|
||||
(store, inv, dir)
|
||||
}
|
||||
|
||||
fn admitted(packet: &Packet, inventory: &InventoryStore) -> LoweringInput {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let corpus = CorpusStore::open(&root.join("data/corpus/corpus.cc")).unwrap();
|
||||
let entities = EntityStore::open(&root.join("data/entity/entity.cc")).unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
PacketStore::open(&dir.path().join("p.cc"))
|
||||
.unwrap()
|
||||
.admit(packet.clone(), inventory, &corpus, &entities)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The proof packet at `rev`. Its referent `n1` has no entity and `n3` is
|
||||
/// `entity:nibu`; `addressee` names which one is spoken to, or none.
|
||||
fn packet(rev: u32, addressee: Option<&str>) -> Packet {
|
||||
let mut p = proof_packet();
|
||||
p.inventory_rev = rev;
|
||||
p.discourse.addressee = addressee.map(nd);
|
||||
p
|
||||
}
|
||||
|
||||
fn feat(inv: &Inventory, gloss: &str, value: FeatValue) -> FeatureValue {
|
||||
let id = inv
|
||||
.entries()
|
||||
.find(|e| e.gloss == gloss)
|
||||
.unwrap_or_else(|| panic!("no feature {gloss}"))
|
||||
.id;
|
||||
FeatureValue { feature: id, value }
|
||||
}
|
||||
|
||||
fn rules(r: Result<LoweringInput, Vec<Refusal>>) -> Vec<&'static str> {
|
||||
r.unwrap_err().iter().map(Refusal::rule).collect()
|
||||
}
|
||||
|
||||
fn speaker() -> EntityId {
|
||||
eid("brokkr")
|
||||
}
|
||||
|
||||
/// Brokkr speaks; the scene holds a relation to Nibu and one to an unnamed
|
||||
/// party, with different values.
|
||||
fn scene(inv: &Inventory) -> Scene {
|
||||
scene_with(vec![
|
||||
PairRelation {
|
||||
from: speaker(),
|
||||
to: Party::Named(eid("nibu")),
|
||||
features: vec![
|
||||
feat(inv, "relative_status", FeatValue::Option(lab("above"))),
|
||||
feat(inv, "intimacy", FeatValue::Grade(grade(200))),
|
||||
feat(inv, "in_group", FeatValue::Flag(false)),
|
||||
],
|
||||
},
|
||||
PairRelation {
|
||||
from: speaker(),
|
||||
to: Party::Unnamed,
|
||||
features: vec![feat(inv, "intimacy", FeatValue::Grade(grade(7)))],
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
fn scene_with(relations: Vec<PairRelation>) -> Scene {
|
||||
Scene {
|
||||
group: "g".into(),
|
||||
n: 1,
|
||||
inventory_rev: 3,
|
||||
participants: vec![speaker(), eid("nibu")],
|
||||
relations,
|
||||
annotator: Annotator::Operator,
|
||||
}
|
||||
}
|
||||
|
||||
fn selected(view: LoweringInput, inv: &Inventory, s: &Scene) -> Vec<FeatureValue> {
|
||||
view.with_scene(inv, s, &speaker())
|
||||
.unwrap()
|
||||
.scene()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scene_relation_reaches_the_lowering_view_and_changes_no_english() {
|
||||
let (store, inv, _dir) = store();
|
||||
let plain = admitted(&packet(3, Some("n3")), &store);
|
||||
assert!(plain.scene().is_empty());
|
||||
let s = scene(&inv);
|
||||
let with = plain.clone().with_scene(&inv, &s, &speaker()).unwrap();
|
||||
assert_eq!(with.scene(), s.relations[0].features.as_slice());
|
||||
assert!(!with.scene().is_empty());
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let entities = EntityStore::open(&root.join("data/entity/entity.cc")).unwrap();
|
||||
assert_eq!(
|
||||
lower(&with, &inv, &entities).unwrap(),
|
||||
lower(&plain, &inv, &entities).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_scene_selects_by_addressee() {
|
||||
let (store, inv, _dir) = store();
|
||||
let s = scene(&inv);
|
||||
// A referent with an entity selects the relation to that entity.
|
||||
let named = selected(admitted(&packet(3, Some("n3")), &store), &inv, &s);
|
||||
assert_eq!(named, s.relations[0].features);
|
||||
// A referent without one selects the relation to an unnamed party.
|
||||
let unnamed = selected(admitted(&packet(3, Some("n1")), &store), &inv, &s);
|
||||
assert_eq!(unnamed, s.relations[1].features);
|
||||
// No addressee selects nothing.
|
||||
assert!(selected(admitted(&packet(3, None), &store), &inv, &s).is_empty());
|
||||
// A named addressee the scene lacks selects nothing, though an unnamed
|
||||
// relation exists.
|
||||
let only_unnamed = scene_with(vec![s.relations[1].clone()]);
|
||||
assert!(
|
||||
selected(
|
||||
admitted(&packet(3, Some("n3")), &store),
|
||||
&inv,
|
||||
&only_unnamed
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
// Another speaker's relations are not selected.
|
||||
let view = admitted(&packet(3, Some("n3")), &store);
|
||||
let theirs = Scene {
|
||||
participants: vec![speaker(), eid("nibu"), eid("hero")],
|
||||
..s.clone()
|
||||
};
|
||||
assert!(
|
||||
view.with_scene(&inv, &theirs, &eid("hero"))
|
||||
.unwrap()
|
||||
.scene()
|
||||
.is_empty()
|
||||
);
|
||||
// A replacement replaces: an empty selection clears the earlier one.
|
||||
let first = admitted(&packet(3, Some("n3")), &store)
|
||||
.with_scene(&inv, &s, &speaker())
|
||||
.unwrap();
|
||||
assert!(
|
||||
first
|
||||
.with_scene(&inv, &only_unnamed, &speaker())
|
||||
.unwrap()
|
||||
.scene()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_scene_refusals() {
|
||||
let (store, inv, _dir) = store();
|
||||
let view = admitted(&packet(3, Some("n3")), &store);
|
||||
let to_nibu = |features| {
|
||||
scene_with(vec![PairRelation {
|
||||
from: speaker(),
|
||||
to: Party::Named(eid("nibu")),
|
||||
features,
|
||||
}])
|
||||
};
|
||||
let take = |s: &Scene| view.clone().with_scene(&inv, s, &speaker());
|
||||
// A speaker the scene does not list.
|
||||
assert_eq!(
|
||||
view.clone()
|
||||
.with_scene(&inv, &scene(&inv), &eid("hero"))
|
||||
.map(|_| ())
|
||||
.unwrap_err(),
|
||||
[Refusal::V16SpeakerNotInScene]
|
||||
);
|
||||
// A label the feature does not declare.
|
||||
let lordly = feat(&inv, "relative_status", FeatValue::Option(lab("lordly")));
|
||||
assert_eq!(rules(take(&to_nibu(vec![lordly]))), ["V7"]);
|
||||
// A value of the wrong kind.
|
||||
let flag = feat(&inv, "intimacy", FeatValue::Flag(true));
|
||||
assert_eq!(rules(take(&to_nibu(vec![flag]))), ["V7"]);
|
||||
// A stance feature in a relation.
|
||||
let anger = feat(&inv, "anger", FeatValue::Grade(grade(1)));
|
||||
assert_eq!(rules(take(&to_nibu(vec![anger]))), ["V8"]);
|
||||
// A speaker feature in a relation.
|
||||
let age = feat(&inv, "education", FeatValue::Grade(grade(1)));
|
||||
assert_eq!(rules(take(&to_nibu(vec![age]))), ["V8"]);
|
||||
// Twice in one list.
|
||||
let twice = vec![
|
||||
feat(&inv, "intimacy", FeatValue::Grade(grade(1))),
|
||||
feat(&inv, "intimacy", FeatValue::Grade(grade(2))),
|
||||
];
|
||||
assert_eq!(rules(take(&to_nibu(twice))), ["V11"]);
|
||||
// Revision 2 predates the Scene features, so their ids do not resolve there.
|
||||
let old = admitted(&packet(2, Some("n3")), &store);
|
||||
assert_eq!(
|
||||
rules(old.with_scene(&inv, &scene(&inv), &speaker())),
|
||||
["V5", "V5", "V5"]
|
||||
);
|
||||
// An unselected relation is not checked: only the selected one is.
|
||||
let bad_elsewhere = scene_with(vec![
|
||||
PairRelation {
|
||||
from: speaker(),
|
||||
to: Party::Unnamed,
|
||||
features: vec![feat(&inv, "anger", FeatValue::Grade(grade(1)))],
|
||||
},
|
||||
PairRelation {
|
||||
from: speaker(),
|
||||
to: Party::Named(eid("nibu")),
|
||||
features: vec![],
|
||||
},
|
||||
]);
|
||||
assert!(take(&bad_elsewhere).unwrap().scene().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_ids_are_stable() {
|
||||
let (_store, inv, _dir) = store();
|
||||
let ids: Vec<_> = [
|
||||
"relative_status",
|
||||
"intimacy",
|
||||
"in_group",
|
||||
"socioeconomic_standing",
|
||||
"education",
|
||||
"age_cohort",
|
||||
]
|
||||
.map(|g| feat(&inv, g, FeatValue::Flag(true)).feature)
|
||||
.into();
|
||||
let want: Vec<_> = (19..=24).map(|n| iv(&format!("feature:{n}"))).collect();
|
||||
assert_eq!(ids, want);
|
||||
}
|
||||
478
crates/weksa/tests/speaker_state.rs
Normal file
478
crates/weksa/tests/speaker_state.rs
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
//! 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).
|
||||
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"),
|
||||
] {
|
||||
let mut item = item_in(key, text, "g", cast());
|
||||
item.speaker = eid(speaker);
|
||||
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<_>>());
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
|
|
@ -1103,6 +1103,10 @@ fn speaker_state_slots_are_exclusive() {
|
|||
&[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));
|
||||
|
|
@ -1121,7 +1125,8 @@ fn speaker_state_slots_are_exclusive() {
|
|||
for (n, slots) in [
|
||||
(2, &[Slot::Scene][..]),
|
||||
(3, &[Slot::Stance]),
|
||||
(4, &[Slot::Discourse, Slot::Predication]),
|
||||
(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));
|
||||
|
|
|
|||
|
|
@ -213,6 +213,25 @@ 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 a
|
||||
scene document and the speaker; 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). 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 a `weksa.scene` document and selects the relation by the addressee referent's entity (V16 when the speaker is not a participant; 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 a Scene document, 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 `&'static str`), `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