speaker-state: packet stance (slot Stance), seed stance features, drop formality/warmth/intensity, admit is the only LoweringInput source, withdrawn items refused (WIP before regeneration)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
869c82b686
commit
86371d2ce4
10 changed files with 324 additions and 37 deletions
|
|
@ -188,6 +188,7 @@ pub enum Slot {
|
||||||
Referent,
|
Referent,
|
||||||
Predication,
|
Predication,
|
||||||
Relation,
|
Relation,
|
||||||
|
Stance,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
|
@ -439,10 +440,28 @@ pub fn seed() -> (InventoryRevision, Vec<InventoryEntry>) {
|
||||||
&pred,
|
&pred,
|
||||||
);
|
);
|
||||||
let nuance = [Slot::Discourse, Slot::Predication];
|
let nuance = [Slot::Discourse, Slot::Predication];
|
||||||
for g in ["hedge", "certainty", "warmth", "formality", "intensity"] {
|
for g in ["hedge", "certainty"] {
|
||||||
add(Family::Feature, g, Some(ValueKind::Grade), &nuance);
|
add(Family::Feature, g, Some(ValueKind::Grade), &nuance);
|
||||||
}
|
}
|
||||||
add(Family::Feature, "irony", Some(ValueKind::Flag), &nuance);
|
add(Family::Feature, "irony", Some(ValueKind::Flag), &nuance);
|
||||||
|
// Felt affect (permille of how strongly it is felt) and the restraint the speaker
|
||||||
|
// puts on it: slot Stance only. Affection covers fondness toward the
|
||||||
|
// addressee. Displayed affect is each language's ontology's business.
|
||||||
|
let stance = [Slot::Stance];
|
||||||
|
for g in [
|
||||||
|
"anger",
|
||||||
|
"fear",
|
||||||
|
"sadness",
|
||||||
|
"joy",
|
||||||
|
"disgust",
|
||||||
|
"surprise",
|
||||||
|
"contempt",
|
||||||
|
"affection",
|
||||||
|
"shame",
|
||||||
|
"restraint",
|
||||||
|
] {
|
||||||
|
add(Family::Feature, g, Some(ValueKind::Grade), &stance);
|
||||||
|
}
|
||||||
let changes = entries.iter().map(|e| Change::Add(e.id)).collect();
|
let changes = entries.iter().map(|e| Change::Add(e.id)).collect();
|
||||||
(
|
(
|
||||||
InventoryRevision {
|
InventoryRevision {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ pub use inventory::{
|
||||||
Slot, ValueKind,
|
Slot, ValueKind,
|
||||||
};
|
};
|
||||||
pub use packet::{
|
pub use packet::{
|
||||||
EncoderId, EntityId, Grade, NodeId, Packet, PacketId, PacketKind, SCHEMA_VERSION,
|
EncoderId, EntityId, Grade, NodeId, Packet, PacketId, PacketKind, SCHEMA_VERSION, Stance,
|
||||||
};
|
};
|
||||||
pub use score::{Embedder, ItemScore, OllamaEmbedder, Scorer, chrf};
|
pub use score::{Embedder, ItemScore, OllamaEmbedder, Scorer, chrf};
|
||||||
pub use split::{Split, SplitScheme};
|
pub use split::{Split, SplitScheme};
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ pub use crate::inventory::InvId;
|
||||||
use crate::inventory::OptionLabel;
|
use crate::inventory::OptionLabel;
|
||||||
|
|
||||||
/// Bumped by a commit that changes these types; the published schema moves with it.
|
/// Bumped by a commit that changes these types; the published schema moves with it.
|
||||||
pub const SCHEMA_VERSION: u32 = 3;
|
pub const SCHEMA_VERSION: u32 = 4;
|
||||||
|
|
||||||
/// Id of a referent or predication within one packet: `n<k>`, `k` a canonical
|
/// Id of a referent or predication within one packet: `n<k>`, `k` a canonical
|
||||||
/// decimal 1..=9999.
|
/// decimal 1..=9999.
|
||||||
|
|
@ -208,6 +208,8 @@ pub struct Packet {
|
||||||
pub extensions: Vec<Extension>,
|
pub extensions: Vec<Extension>,
|
||||||
#[cultcache(key = 12)]
|
#[cultcache(key = 12)]
|
||||||
pub trace: Trace,
|
pub trace: Trace,
|
||||||
|
#[cultcache(key = 13)]
|
||||||
|
pub stance: Stance,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Packet {
|
impl Packet {
|
||||||
|
|
@ -262,6 +264,14 @@ pub struct Discourse {
|
||||||
pub features: Vec<FeatureValue>,
|
pub features: Vec<FeatureValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The speaker's felt affect and restraint, never displayed affect: how the
|
||||||
|
/// speaker shows a feeling is each language's ontology's business. A
|
||||||
|
/// held-back feeling is not a claim, so it is never a predication.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct Stance {
|
||||||
|
pub features: Vec<FeatureValue>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct FeatureValue {
|
pub struct FeatureValue {
|
||||||
/// Feature id.
|
/// Feature id.
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use crate::entity::Entity;
|
||||||
use crate::form::is_language_tag;
|
use crate::form::is_language_tag;
|
||||||
use crate::inventory::{Change, InvId, Inventory, InventoryEntry, InventoryRevision, revision_key};
|
use crate::inventory::{Change, InvId, Inventory, InventoryEntry, InventoryRevision, revision_key};
|
||||||
use crate::packet::{EntityId, Packet};
|
use crate::packet::{EntityId, Packet};
|
||||||
use crate::validate::{Refusal, validate};
|
use crate::validate::{LoweringInput, Refusal, admissible};
|
||||||
|
|
||||||
/// A broken store rule, named.
|
/// A broken store rule, named.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -73,6 +73,9 @@ pub enum StoreRefusal {
|
||||||
/// A corpus packet's referent cites an entity that is neither named on its
|
/// A corpus packet's referent cites an entity that is neither named on its
|
||||||
/// item nor the item's speaker or a prior-turn speaker.
|
/// item nor the item's speaker or a prior-turn speaker.
|
||||||
EntityNotOnItem { entity: EntityId, item: String },
|
EntityNotOnItem { entity: EntityId, item: String },
|
||||||
|
/// A new packet names a corpus item already withdrawn. Packets admitted
|
||||||
|
/// before the withdrawal stay stored and readable.
|
||||||
|
WithdrawnCorpusItem { key: String },
|
||||||
/// Any other malformed document or change.
|
/// Any other malformed document or change.
|
||||||
Malformed { key: String, why: &'static str },
|
Malformed { key: String, why: &'static str },
|
||||||
}
|
}
|
||||||
|
|
@ -598,9 +601,10 @@ impl PacketStore {
|
||||||
self.cache.get::<Packet>(key)
|
self.cache.get::<Packet>(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The only door: a packet exists only if `validate` passes against the
|
/// The only door, and the only source of a [`LoweringInput`]: a packet
|
||||||
/// inventory the store holds at the packet's revision, its `corpus_item`
|
/// exists only if the validator's rules pass against the inventory the
|
||||||
/// resolves, every referent's entity is in the entity store and is named on
|
/// store holds at the packet's revision, its `corpus_item` resolves to an
|
||||||
|
/// item that is not withdrawn, every referent's entity is in the entity store and is named on
|
||||||
/// that item or is its speaker or a prior-turn speaker (an id-set check). A
|
/// that item or is its speaker or a prior-turn speaker (an id-set check). A
|
||||||
/// key holds its packet for good. A packet that names no corpus item has no
|
/// key holds its packet for good. A packet that names no corpus item has no
|
||||||
/// door here.
|
/// door here.
|
||||||
|
|
@ -610,9 +614,15 @@ impl PacketStore {
|
||||||
inventory: &InventoryStore,
|
inventory: &InventoryStore,
|
||||||
corpus: &CorpusStore,
|
corpus: &CorpusStore,
|
||||||
entities: &EntityStore,
|
entities: &EntityStore,
|
||||||
) -> Result<(), StoreError> {
|
) -> Result<LoweringInput, StoreError> {
|
||||||
validate(&packet, &inventory.inventory()?).map_err(StoreError::Invalid)?;
|
let view = admissible(&packet, &inventory.inventory()?).map_err(StoreError::Invalid)?;
|
||||||
let item = corpus.resolve(&packet.provenance.corpus_item)?;
|
let item = corpus.resolve(&packet.provenance.corpus_item)?;
|
||||||
|
if item.withdrawn {
|
||||||
|
return Err(StoreRefusal::WithdrawnCorpusItem {
|
||||||
|
key: item.id.clone(),
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
for entity in packet.referents.iter().filter_map(|r| r.entity.as_ref()) {
|
for entity in packet.referents.iter().filter_map(|r| r.entity.as_ref()) {
|
||||||
if entities.get(entity)?.is_none() {
|
if entities.get(entity)?.is_none() {
|
||||||
return Err(StoreRefusal::UnknownEntity {
|
return Err(StoreRefusal::UnknownEntity {
|
||||||
|
|
@ -634,11 +644,11 @@ impl PacketStore {
|
||||||
let key = packet.id.to_string();
|
let key = packet.id.to_string();
|
||||||
if let Some(existing) = self.get(&key)? {
|
if let Some(existing) = self.get(&key)? {
|
||||||
if existing == packet {
|
if existing == packet {
|
||||||
return Ok(());
|
return Ok(view);
|
||||||
}
|
}
|
||||||
return Err(StoreRefusal::PacketImmutable { key }.into());
|
return Err(StoreRefusal::PacketImmutable { key }.into());
|
||||||
}
|
}
|
||||||
self.cache.put(key, &packet)?;
|
self.cache.put(key, &packet)?;
|
||||||
Ok(())
|
Ok(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
//! The validator owns whether a packet exists. Every path that admits or lowers a
|
//! The validator owns the inventory and form rules a packet must meet. Every
|
||||||
//! packet calls `validate`, and its Ok value, [`LoweringInput`], is the only
|
//! path that admits a packet applies them, but `validate` only judges: it
|
||||||
//! thing a lowerer sees. The rules are V1..V8 and V10..V12: the shape of an id
|
//! 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..V12: the shape of an id
|
||||||
//! or a grade is its type's parse, not a rule, so V9 is retired and not
|
//! or a grade is its type's parse, not a rule, so V9 is retired and not
|
||||||
//! renumbered.
|
//! renumbered.
|
||||||
//!
|
//!
|
||||||
|
|
@ -25,7 +28,7 @@ use std::fmt;
|
||||||
use crate::inventory::{Family, InvId, Inventory, InventoryEntry, Slot, ValueKind};
|
use crate::inventory::{Family, InvId, Inventory, InventoryEntry, Slot, ValueKind};
|
||||||
use crate::packet::{
|
use crate::packet::{
|
||||||
Constraints, Context, Discourse, Extension, FeatValue, FeatureValue, NodeId, Packet,
|
Constraints, Context, Discourse, Extension, FeatValue, FeatureValue, NodeId, Packet,
|
||||||
PacketKind, Predication, Referent, Relation, SCHEMA_VERSION,
|
PacketKind, Predication, Referent, Relation, SCHEMA_VERSION, Stance,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A broken rule, named.
|
/// A broken rule, named.
|
||||||
|
|
@ -91,9 +94,34 @@ impl fmt::Display for Refusal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `packet` may exist under `inventory`. All broken rules are returned;
|
/// Whether `packet` meets the rules under `inventory`. All broken rules are
|
||||||
/// a packet that passes yields the lowering view, which only this function builds.
|
/// returned. A pass carries no lowering view: that comes from
|
||||||
pub fn validate(packet: &Packet, inventory: &Inventory) -> Result<LoweringInput, Vec<Refusal>> {
|
/// `PacketStore::admit` only.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use weksa::{Inventory, Refusal, packet::Packet};
|
||||||
|
/// fn judge(p: &Packet, i: &Inventory) -> Result<(), Vec<Refusal>> { weksa::validate(p, i) }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// use weksa::{Inventory, LoweringInput, Refusal, packet::Packet};
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// ```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();
|
let mut out = Vec::new();
|
||||||
if packet.schema_version != SCHEMA_VERSION {
|
if packet.schema_version != SCHEMA_VERSION {
|
||||||
out.push(Refusal::V1SchemaVersion {
|
out.push(Refusal::V1SchemaVersion {
|
||||||
|
|
@ -221,6 +249,7 @@ impl<'a> Inventoried<'a> {
|
||||||
predications,
|
predications,
|
||||||
relations,
|
relations,
|
||||||
extensions,
|
extensions,
|
||||||
|
stance,
|
||||||
..
|
..
|
||||||
} = p;
|
} = p;
|
||||||
let Context {
|
let Context {
|
||||||
|
|
@ -238,6 +267,7 @@ impl<'a> Inventoried<'a> {
|
||||||
self.slot(id, Family::SpeechAct, "speech act");
|
self.slot(id, Family::SpeechAct, "speech act");
|
||||||
}
|
}
|
||||||
self.features(features, Slot::Discourse);
|
self.features(features, Slot::Discourse);
|
||||||
|
self.features(&stance.features, Slot::Stance);
|
||||||
for Referent {
|
for Referent {
|
||||||
kind,
|
kind,
|
||||||
concept,
|
concept,
|
||||||
|
|
@ -349,8 +379,9 @@ fn fits(kind: Option<&ValueKind>, value: &FeatValue) -> bool {
|
||||||
|
|
||||||
/// What the lowerer sees: a validated packet's meaning, with no packet id, no
|
/// 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
|
/// provenance (so no corpus item key, encoder or source) and no trace. Its
|
||||||
/// fields are private and `validate` is the only constructor, so a lowerer
|
/// fields are private and the constructor is crate-private, called only by
|
||||||
/// cannot be handed an unvalidated packet or the key to an answer text.
|
/// `PacketStore::admit`, so a lowerer cannot be handed an unadmitted packet or
|
||||||
|
/// the key to an answer text.
|
||||||
///
|
///
|
||||||
/// Each field has a doctest that reads it from outside the crate. A read
|
/// Each field has a doctest that reads it from outside the crate. A read
|
||||||
/// compiles exactly when the field is `pub`, so the doctest (which must fail to
|
/// compiles exactly when the field is `pub`, so the doctest (which must fail to
|
||||||
|
|
@ -394,6 +425,10 @@ fn fits(kind: Option<&ValueKind>, value: &FeatValue) -> bool {
|
||||||
/// fn read(l: weksa::LoweringInput) { let _ = l.extensions; }
|
/// fn read(l: weksa::LoweringInput) { let _ = l.extensions; }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// fn read(l: weksa::LoweringInput) { let _ = l.stance; }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
/// The constructor is private: a call from outside the crate compiles only if
|
/// 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
|
/// `LoweringInput::of` is `pub`. The first block is the same code without the
|
||||||
/// call, so the second fails for that call and nothing else.
|
/// call, so the second fails for that call and nothing else.
|
||||||
|
|
@ -430,6 +465,7 @@ pub struct LoweringInput {
|
||||||
relations: Vec<Relation>,
|
relations: Vec<Relation>,
|
||||||
constraints: Constraints,
|
constraints: Constraints,
|
||||||
extensions: Vec<Extension>,
|
extensions: Vec<Extension>,
|
||||||
|
stance: Stance,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoweringInput {
|
impl LoweringInput {
|
||||||
|
|
@ -444,6 +480,7 @@ impl LoweringInput {
|
||||||
relations: p.relations.clone(),
|
relations: p.relations.clone(),
|
||||||
constraints: p.constraints.clone(),
|
constraints: p.constraints.clone(),
|
||||||
extensions: p.extensions.clone(),
|
extensions: p.extensions.clone(),
|
||||||
|
stance: p.stance.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -474,4 +511,7 @@ impl LoweringInput {
|
||||||
pub fn extensions(&self) -> &[Extension] {
|
pub fn extensions(&self) -> &[Extension] {
|
||||||
&self.extensions
|
&self.extensions
|
||||||
}
|
}
|
||||||
|
pub fn stance(&self) -> &Stance {
|
||||||
|
&self.stance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,11 @@ pub const WARN: &str = "speech_act:5";
|
||||||
pub const POLARITY: &str = "feature:1";
|
pub const POLARITY: &str = "feature:1";
|
||||||
pub const MODALITY: &str = "feature:4";
|
pub const MODALITY: &str = "feature:4";
|
||||||
pub const HEDGE: &str = "feature:6";
|
pub const HEDGE: &str = "feature:6";
|
||||||
pub const IRONY: &str = "feature:11";
|
pub const IRONY: &str = "feature:8";
|
||||||
|
/// Stance features, slot Stance only (Grade).
|
||||||
|
pub const ANGER: &str = "feature:9";
|
||||||
|
pub const AFFECTION: &str = "feature:16";
|
||||||
|
pub const RESTRAINT: &str = "feature:18";
|
||||||
/// Introduced at rev 2.
|
/// Introduced at rev 2.
|
||||||
pub const CONCEPT_A: &str = "concept:1";
|
pub const CONCEPT_A: &str = "concept:1";
|
||||||
pub const CONCEPT_B: &str = "concept:2";
|
pub const CONCEPT_B: &str = "concept:2";
|
||||||
|
|
@ -211,6 +215,7 @@ pub fn world() -> World {
|
||||||
Slot::Referent,
|
Slot::Referent,
|
||||||
Slot::Predication,
|
Slot::Predication,
|
||||||
Slot::Relation,
|
Slot::Relation,
|
||||||
|
Slot::Stance,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -308,5 +313,17 @@ pub fn good() -> Packet {
|
||||||
notes: vec!["a note".into()],
|
notes: vec!["a note".into()],
|
||||||
rejected_readings: vec![],
|
rejected_readings: vec![],
|
||||||
},
|
},
|
||||||
|
stance: Stance {
|
||||||
|
features: vec![
|
||||||
|
FeatureValue {
|
||||||
|
feature: iv(ANGER),
|
||||||
|
value: FeatValue::Grade(grade(300)),
|
||||||
|
},
|
||||||
|
FeatureValue {
|
||||||
|
feature: iv(RESTRAINT),
|
||||||
|
value: FeatValue::Grade(grade(700)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -391,7 +391,7 @@ fn packet_entities_resolve() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn packet_schema_v2() {
|
fn packet_schema_v2() {
|
||||||
assert_eq!(SCHEMA_VERSION, 3);
|
assert_eq!(SCHEMA_VERSION, 4);
|
||||||
let mut p = good();
|
let mut p = good();
|
||||||
p.schema_version = 2;
|
p.schema_version = 2;
|
||||||
let Err(refused) = validate(&p, &inventory()) else {
|
let Err(refused) = validate(&p, &inventory()) else {
|
||||||
|
|
@ -416,4 +416,7 @@ fn packet_schema_v2() {
|
||||||
serde_json::from_str(&published["weksa.corpus_item.schema.json"]).unwrap();
|
serde_json::from_str(&published["weksa.corpus_item.schema.json"]).unwrap();
|
||||||
assert!(item["properties"].get("names").is_some());
|
assert!(item["properties"].get("names").is_some());
|
||||||
assert!(published.contains_key("weksa.entity.schema.json"));
|
assert!(published.contains_key("weksa.entity.schema.json"));
|
||||||
|
// v4: the packet carries the speaker's stance, a bare feature list.
|
||||||
|
assert!(packet["properties"].get("stance").is_some(), "{packet}");
|
||||||
|
assert_eq!(packet["$defs"]["Stance"]["properties"]["features"]["type"], "array");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use common::*;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use weksa::corpus::{CorpusItem, ModelFamily, Source, Turn};
|
use weksa::corpus::{CorpusItem, ModelFamily, Source, Turn};
|
||||||
use weksa::inventory::{Change, seed};
|
use weksa::inventory::{Change, seed};
|
||||||
|
use weksa::packet::PacketId;
|
||||||
use weksa::{
|
use weksa::{
|
||||||
AddOutcome, CorpusStore, EntityStore, Family, InventoryEntry, InventoryRevision,
|
AddOutcome, CorpusStore, EntityStore, Family, InventoryEntry, InventoryRevision,
|
||||||
InventoryStore, PacketStore, Refusal, Slot, SplitScheme, StoreError, StoreRefusal, ValueKind,
|
InventoryStore, PacketStore, Refusal, Slot, SplitScheme, StoreError, StoreRefusal, ValueKind,
|
||||||
|
|
@ -564,7 +565,7 @@ fn the_seed_is_the_standards_enumerations_and_the_nuance_features() {
|
||||||
assert_eq!(count(Family::Relation), 16);
|
assert_eq!(count(Family::Relation), 16);
|
||||||
assert_eq!(count(Family::Role), 8);
|
assert_eq!(count(Family::Role), 8);
|
||||||
assert_eq!(count(Family::SpeechAct), 8);
|
assert_eq!(count(Family::SpeechAct), 8);
|
||||||
assert_eq!(count(Family::Feature), 11);
|
assert_eq!(count(Family::Feature), 18);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
count(Family::Concept) + count(Family::Frame) + count(Family::Extension),
|
count(Family::Concept) + count(Family::Frame) + count(Family::Extension),
|
||||||
0
|
0
|
||||||
|
|
@ -582,6 +583,46 @@ fn the_seed_is_the_standards_enumerations_and_the_nuance_features() {
|
||||||
inv.entry(&iv(IRONY)).unwrap().value_kind,
|
inv.entry(&iv(IRONY)).unwrap().value_kind,
|
||||||
Some(ValueKind::Flag)
|
Some(ValueKind::Flag)
|
||||||
);
|
);
|
||||||
|
// Felt affect and restraint: Grade features in slot Stance alone.
|
||||||
|
for (id, name) in [
|
||||||
|
(ANGER, "anger"),
|
||||||
|
(AFFECTION, "affection"),
|
||||||
|
(RESTRAINT, "restraint"),
|
||||||
|
] {
|
||||||
|
let e = inv.entry(&iv(id)).unwrap();
|
||||||
|
assert_eq!(e.gloss, name);
|
||||||
|
assert_eq!(e.value_kind, Some(ValueKind::Grade));
|
||||||
|
assert_eq!(e.applies_to, vec![Slot::Stance]);
|
||||||
|
}
|
||||||
|
let stance_names: Vec<&str> = inv
|
||||||
|
.entries()
|
||||||
|
.filter(|e| e.applies_to == [Slot::Stance])
|
||||||
|
.map(|e| e.gloss.as_str())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
stance_names,
|
||||||
|
[
|
||||||
|
"anger",
|
||||||
|
"fear",
|
||||||
|
"sadness",
|
||||||
|
"joy",
|
||||||
|
"disgust",
|
||||||
|
"surprise",
|
||||||
|
"contempt",
|
||||||
|
"affection",
|
||||||
|
"shame",
|
||||||
|
"restraint"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// Hedge, certainty and irony stay on content; displayed formality, warmth
|
||||||
|
// and intensity are each language's ontology's, not the seed's.
|
||||||
|
for e in inv.entries() {
|
||||||
|
assert!(
|
||||||
|
!["warmth", "formality", "intensity"].contains(&e.gloss.as_str()),
|
||||||
|
"{}",
|
||||||
|
e.gloss
|
||||||
|
);
|
||||||
|
}
|
||||||
assert!(
|
assert!(
|
||||||
inv.entries()
|
inv.entries()
|
||||||
.all(|e| e.well_formed() && e.introduced_rev == 1 && e.retired_rev.is_none())
|
.all(|e| e.well_formed() && e.introduced_rev == 1 && e.retired_rev.is_none())
|
||||||
|
|
@ -618,7 +659,7 @@ fn gloss_is_bounded_text() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn packet_admission_goes_through_the_validator() {
|
fn packet_admission_goes_through_the_validator() {
|
||||||
let w = world();
|
let mut w = world();
|
||||||
let mut store = PacketStore::open(&w.dir.path().join("p.cc")).unwrap();
|
let mut store = PacketStore::open(&w.dir.path().join("p.cc")).unwrap();
|
||||||
let mut bad = good();
|
let mut bad = good();
|
||||||
bad.schema_version = 5;
|
bad.schema_version = 5;
|
||||||
|
|
@ -633,13 +674,18 @@ fn packet_admission_goes_through_the_validator() {
|
||||||
other => panic!("{other:?}"),
|
other => panic!("{other:?}"),
|
||||||
}
|
}
|
||||||
assert!(store.all().unwrap().is_empty());
|
assert!(store.all().unwrap().is_empty());
|
||||||
store
|
// The door yields the lowering view, stance included.
|
||||||
|
let view = store
|
||||||
.admit(good(), &w.inventory, &w.corpus, &w.entities)
|
.admit(good(), &w.inventory, &w.corpus, &w.entities)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// The same packet is idempotent; a different one under the same key is refused.
|
assert_eq!(view.stance(), &good().stance);
|
||||||
store
|
assert!(!view.stance().features.is_empty());
|
||||||
|
// The same packet is idempotent and yields the same view; a different one
|
||||||
|
// under the same key is refused.
|
||||||
|
let replay = store
|
||||||
.admit(good(), &w.inventory, &w.corpus, &w.entities)
|
.admit(good(), &w.inventory, &w.corpus, &w.entities)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assert_eq!(replay, view);
|
||||||
let mut other = good();
|
let mut other = good();
|
||||||
other.provenance.confidence = grade(1);
|
other.provenance.confidence = grade(1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -650,6 +696,24 @@ fn packet_admission_goes_through_the_validator() {
|
||||||
);
|
);
|
||||||
assert_eq!(store.all().unwrap().len(), 1);
|
assert_eq!(store.all().unwrap().len(), 1);
|
||||||
assert_eq!(store.get(PACKET_ID).unwrap(), Some(good()));
|
assert_eq!(store.get(PACKET_ID).unwrap(), Some(good()));
|
||||||
|
// A withdrawn item admits no packet, a replay included; the packet admitted
|
||||||
|
// before the withdrawal stays stored and readable.
|
||||||
|
w.corpus.withdraw("item:nibu:1").unwrap();
|
||||||
|
let mut fresh = good();
|
||||||
|
fresh.id = PacketId::parse("packet:00000000000000aa").unwrap();
|
||||||
|
let withdrawn = StoreRefusal::WithdrawnCorpusItem {
|
||||||
|
key: "item:nibu:1".into(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
refusal(store.admit(fresh, &w.inventory, &w.corpus, &w.entities)),
|
||||||
|
withdrawn
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
refusal(store.admit(good(), &w.inventory, &w.corpus, &w.entities)),
|
||||||
|
withdrawn
|
||||||
|
);
|
||||||
|
assert_eq!(store.all().unwrap().len(), 1);
|
||||||
|
assert_eq!(store.get(PACKET_ID).unwrap(), Some(good()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -862,6 +926,19 @@ fn seed_reseeded() {
|
||||||
.all()
|
.all()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(items.len(), 3);
|
assert_eq!(items.len(), 3);
|
||||||
|
// The committed inventory is the seed, reseeded: stance features in, the
|
||||||
|
// displayed-affect features out.
|
||||||
|
let committed = InventoryStore::open(&root.join("data/interlingua/inventory.cc"))
|
||||||
|
.unwrap()
|
||||||
|
.inventory()
|
||||||
|
.unwrap();
|
||||||
|
let (rev, entries) = seed();
|
||||||
|
assert!(committed.has_revision(rev.n));
|
||||||
|
let mut have: Vec<_> = committed.entries().cloned().collect();
|
||||||
|
let mut want = entries;
|
||||||
|
have.sort_by_key(|e| e.id);
|
||||||
|
want.sort_by_key(|e| e.id);
|
||||||
|
assert_eq!(have, want);
|
||||||
let d = dir();
|
let d = dir();
|
||||||
let mut replay = CorpusStore::open(&d.path().join("c.cc")).unwrap();
|
let mut replay = CorpusStore::open(&d.path().join("c.cc")).unwrap();
|
||||||
for i in items {
|
for i in items {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
mod common;
|
mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
use weksa::packet::*;
|
use weksa::packet::*;
|
||||||
use weksa::{Family, Refusal, Slot, validate};
|
use weksa::{Family, PacketStore, Refusal, Slot, validate};
|
||||||
|
|
||||||
fn refusals(p: &Packet) -> Vec<Refusal> {
|
fn refusals(p: &Packet) -> Vec<Refusal> {
|
||||||
validate(p, &inventory()).err().unwrap_or_default()
|
validate(p, &inventory()).err().unwrap_or_default()
|
||||||
|
|
@ -334,6 +334,83 @@ fn v8_feature_applies_to() {
|
||||||
assert_eq!(refusals(&pred), vec![]);
|
assert_eq!(refusals(&pred), vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stance_features_apply_to_the_stance_only() {
|
||||||
|
// Affect and restraint sit in slot Stance and nowhere else.
|
||||||
|
assert_eq!(refusals(&good()), vec![]);
|
||||||
|
for id in [ANGER, RESTRAINT] {
|
||||||
|
let mut p = good();
|
||||||
|
p.discourse
|
||||||
|
.features
|
||||||
|
.push(feat(id, FeatValue::Grade(grade(1))));
|
||||||
|
assert_eq!(
|
||||||
|
refusals(&p),
|
||||||
|
vec![Refusal::V8NotApplicable {
|
||||||
|
feature: iv(id),
|
||||||
|
slot: Slot::Discourse
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut referent = good();
|
||||||
|
referent.referents[0]
|
||||||
|
.features
|
||||||
|
.push(feat(AFFECTION, FeatValue::Grade(grade(1))));
|
||||||
|
assert_eq!(
|
||||||
|
refusals(&referent),
|
||||||
|
vec![Refusal::V8NotApplicable {
|
||||||
|
feature: iv(AFFECTION),
|
||||||
|
slot: Slot::Referent
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
let mut predication = good();
|
||||||
|
predication.predications[0]
|
||||||
|
.features
|
||||||
|
.push(feat(ANGER, FeatValue::Grade(grade(1))));
|
||||||
|
assert_eq!(
|
||||||
|
refusals(&predication),
|
||||||
|
vec![Refusal::V8NotApplicable {
|
||||||
|
feature: iv(ANGER),
|
||||||
|
slot: Slot::Predication
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
// Hedge is a stance on content, not a felt state: not in the stance.
|
||||||
|
let mut p = good();
|
||||||
|
p.stance
|
||||||
|
.features
|
||||||
|
.push(feat(HEDGE, FeatValue::Grade(grade(1))));
|
||||||
|
assert_eq!(
|
||||||
|
refusals(&p),
|
||||||
|
vec![Refusal::V8NotApplicable {
|
||||||
|
feature: iv(HEDGE),
|
||||||
|
slot: Slot::Stance
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
// A stance feature takes its inventory's value kind (V7) and must exist (V5).
|
||||||
|
let mut flag = good();
|
||||||
|
flag.stance.features = vec![feat(ANGER, FeatValue::Flag(true))];
|
||||||
|
assert_eq!(
|
||||||
|
refusals(&flag),
|
||||||
|
vec![Refusal::V7ValueKind { what: ANGER.into() }]
|
||||||
|
);
|
||||||
|
let mut missing = good();
|
||||||
|
missing.stance.features = vec![feat("feature:99", FeatValue::Grade(grade(1)))];
|
||||||
|
assert_eq!(refusals(&missing), vec![unresolved("feature:99")]);
|
||||||
|
// A default stance is valid: the packet records no felt state.
|
||||||
|
let mut none = good();
|
||||||
|
none.stance = Stance::default();
|
||||||
|
assert_eq!(refusals(&none), vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn displayed_formality_warmth_and_intensity_are_not_in_the_inventory() {
|
||||||
|
// Felt affect is the packet's; how it is displayed is each language's
|
||||||
|
// ontology's, so no feature carries it.
|
||||||
|
let inv = inventory();
|
||||||
|
for gone in ["warmth", "formality", "intensity"] {
|
||||||
|
assert!(inv.entries().all(|e| e.gloss != gone), "{gone}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn v10_rev_exists() {
|
fn v10_rev_exists() {
|
||||||
let mut p = good();
|
let mut p = good();
|
||||||
|
|
@ -358,7 +435,12 @@ fn lowering_input_carries_no_trace_id_or_provenance() {
|
||||||
let mut p = good();
|
let mut p = good();
|
||||||
p.trace.notes = vec!["SECRET-NOTE".into()];
|
p.trace.notes = vec!["SECRET-NOTE".into()];
|
||||||
p.trace.rejected_readings = vec!["SECRET-READING".into()];
|
p.trace.rejected_readings = vec!["SECRET-READING".into()];
|
||||||
let input = validate(&p, &inventory()).unwrap();
|
// Only the packet store's door yields the view.
|
||||||
|
let w = world();
|
||||||
|
let mut store = PacketStore::open(&w.dir.path().join("p.cc")).unwrap();
|
||||||
|
let input = store
|
||||||
|
.admit(p.clone(), &w.inventory, &w.corpus, &w.entities)
|
||||||
|
.unwrap();
|
||||||
let seen = format!("{input:?}");
|
let seen = format!("{input:?}");
|
||||||
for hidden in [PACKET_ID, ENCODER_ID, "item:nibu:1", "SECRET"] {
|
for hidden in [PACKET_ID, ENCODER_ID, "item:nibu:1", "SECRET"] {
|
||||||
assert!(!seen.contains(hidden), "{hidden} reached the input: {seen}");
|
assert!(!seen.contains(hidden), "{hidden} reached the input: {seen}");
|
||||||
|
|
@ -373,6 +455,9 @@ fn lowering_input_carries_no_trace_id_or_provenance() {
|
||||||
assert_eq!(input.context(), &p.context);
|
assert_eq!(input.context(), &p.context);
|
||||||
assert_eq!(input.discourse(), &p.discourse);
|
assert_eq!(input.discourse(), &p.discourse);
|
||||||
assert_eq!(input.constraints(), &p.constraints);
|
assert_eq!(input.constraints(), &p.constraints);
|
||||||
|
// The speaker's stance is meaning, not provenance: it reaches the lowerer.
|
||||||
|
assert_eq!(input.stance(), &p.stance);
|
||||||
|
assert!(!p.stance.features.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -435,6 +520,14 @@ fn v11_duplicate_feature_each_slot() {
|
||||||
refusals(&relation),
|
refusals(&relation),
|
||||||
vec![v11(Some(Slot::Relation), FEAT_ALL)]
|
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
|
||||||
|
.push(feat(ANGER, FeatValue::Grade(grade(5))));
|
||||||
|
assert_eq!(refusals(&felt), vec![v11(Some(Slot::Stance), ANGER)]);
|
||||||
let mut extension = good();
|
let mut extension = good();
|
||||||
extension.extensions.push(Extension {
|
extension.extensions.push(Extension {
|
||||||
key: iv(EXT),
|
key: iv(EXT),
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ Packet
|
||||||
constraints { preserve, deterministic, max_latency_ms }
|
constraints { preserve, deterministic, max_latency_ms }
|
||||||
extensions [ { key: InvId (family extension), value: Grade | Option | Flag } ]
|
extensions [ { key: InvId (family extension), value: Grade | Option | Flag } ]
|
||||||
trace { notes, rejected_readings }
|
trace { notes, rejected_readings }
|
||||||
|
stance { features }
|
||||||
```
|
```
|
||||||
|
|
||||||
`schema_version` and `inventory_rev` name the exact schema and inventory
|
`schema_version` and `inventory_rev` name the exact schema and inventory
|
||||||
|
|
@ -135,8 +136,10 @@ revision a packet was written against. A grade is an integer permille,
|
||||||
|
|
||||||
### Validation
|
### Validation
|
||||||
|
|
||||||
A packet exists only if `validate` passes; every path that admits or lowers a
|
A packet exists only if the validator's rules pass and the packet store admits
|
||||||
packet calls it. A refusal names the rule.
|
it. `validate` judges and returns `Ok(())` or the refusals; the packet store's
|
||||||
|
`admit` is the one door and the only source of the lowering view. A refusal
|
||||||
|
names the rule.
|
||||||
|
|
||||||
- V1: `schema_version` is the version these types define.
|
- V1: `schema_version` is the version these types define.
|
||||||
- V2: at least one of `referents`, `predications`, `relations` is present.
|
- V2: at least one of `referents`, `predications`, `relations` is present.
|
||||||
|
|
@ -158,9 +161,11 @@ packet calls it. A refusal names the rule.
|
||||||
(a calibration constant, capacity and not meaning).
|
(a calibration constant, capacity and not meaning).
|
||||||
|
|
||||||
V9 is retired and not renumbered: the form of a key or a grade is its type, and
|
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. `validate` returns the lowering view of a
|
an extension key's family is V6. `PacketStore::admit` returns the lowering view of a
|
||||||
packet that passes; it carries no packet id, no provenance and no trace, and is
|
packet it admits; the view carries the meaning and the stance, no packet id, no
|
||||||
built nowhere else. A packet is admitted against the inventory the packet store
|
provenance and no trace, and is built nowhere else. The store also refuses a
|
||||||
|
packet that names a withdrawn corpus item (packets admitted before the
|
||||||
|
withdrawal stay stored). A packet is admitted against the inventory the packet store
|
||||||
reads at the packet's revision, never one the caller supplies.
|
reads at the packet's revision, never one the caller supplies.
|
||||||
|
|
||||||
### `kind`
|
### `kind`
|
||||||
|
|
@ -182,9 +187,22 @@ ids.
|
||||||
### `discourse`
|
### `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 node id,
|
||||||
and `features` carry source-side nuance (hedge, certainty, warmth, formality,
|
and `features` carry source-side nuance on content (hedge, certainty, irony and
|
||||||
intensity, irony and whatever the inventory grows). This is source-side
|
whatever the inventory grows). This is source-side communicative pressure, not
|
||||||
communicative pressure, not target-language register.
|
target-language register.
|
||||||
|
|
||||||
|
### `stance`
|
||||||
|
|
||||||
|
The speaker's felt affect and how much of it the speaker holds back. `features`
|
||||||
|
is a feature list in slot `Stance`, checked like every other (V5 to V8, V11).
|
||||||
|
The seed carries felt affect as grades of how strongly it is felt (anger, fear, sadness,
|
||||||
|
joy, disgust, surprise, contempt, affection, shame) and `restraint`, a grade for
|
||||||
|
how much of the felt affect is suppressed (0 none, 1000 fully). The stance is
|
||||||
|
what is felt, never what is displayed: how a feeling shows, and how much of a
|
||||||
|
restrained one leaks, are cultural, and each language's ontology maps felt
|
||||||
|
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.
|
||||||
|
|
||||||
### `referents`
|
### `referents`
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue