Weksa/crates/weksa/src/inventory.rs
2026-11-09 10:07:52 +00:00

571 lines
16 KiB
Rust

//! The inventory: the ids packets name, as CultCache documents under appended
//! revisions. An entry is never edited in place; a changed meaning is a retire
//! plus an add, and a retired id still resolves for packets of earlier revisions.
use std::collections::BTreeMap;
use std::fmt;
use std::num::NonZeroU32;
use cultcache_rs::DatabaseEntry;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::form::{FormError, string_form};
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
pub enum Family {
Concept,
Frame,
Role,
Relation,
SpeechAct,
Feature,
ReferentKind,
/// A game- or loop-declared packet field; the entry carries its value kind.
Extension,
}
impl Family {
pub const ALL: [Family; 8] = [
Family::Concept,
Family::Frame,
Family::Role,
Family::Relation,
Family::SpeechAct,
Family::Feature,
Family::ReferentKind,
Family::Extension,
];
pub fn prefix(self) -> &'static str {
match self {
Family::Concept => "concept",
Family::Frame => "frame",
Family::Role => "role",
Family::Relation => "relation",
Family::SpeechAct => "speech_act",
Family::Feature => "feature",
Family::ReferentKind => "referent_kind",
Family::Extension => "extension",
}
}
/// The id of the `n`th entry of this family.
pub fn id(self, n: NonZeroU32) -> InvId {
InvId { family: self, n }
}
}
/// Id of an inventory entry: `<family prefix>:<n>`, `n` a canonical decimal
/// (no sign, no leading zero) of at least 1.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct InvId {
family: Family,
n: NonZeroU32,
}
impl InvId {
pub fn parse(s: &str) -> Result<Self, FormError> {
let bad = || FormError::new("InvId", s);
let (prefix, n) = s.split_once(':').ok_or_else(bad)?;
let family = Family::ALL
.into_iter()
.find(|f| f.prefix() == prefix)
.ok_or_else(bad)?;
if n.starts_with('0') || !n.bytes().all(|b| b.is_ascii_digit()) {
return Err(bad());
}
let n = n.parse::<NonZeroU32>().map_err(|_| bad())?;
Ok(Self { family, n })
}
pub fn family(self) -> Family {
self.family
}
}
impl fmt::Display for InvId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.family.prefix(), self.n)
}
}
string_form!(InvId, { "type": "string", "pattern": "^(concept|frame|role|relation|speech_act|feature|referent_kind|extension):[1-9][0-9]*$" });
/// A value label of an options feature: `^[a-z][a-z0-9_]{0,23}$`.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct OptionLabel(String);
impl OptionLabel {
pub fn parse(s: &str) -> Result<Self, FormError> {
let mut bytes = s.bytes();
let ok = s.len() <= 24
&& bytes.next().is_some_and(|b| b.is_ascii_lowercase())
&& bytes.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_');
if ok {
Ok(Self(s.to_string()))
} else {
Err(FormError::new("OptionLabel", s))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for OptionLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
string_form!(OptionLabel, { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,23}$" });
/// 1 to 16 distinct labels.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "Vec<OptionLabel>", into = "Vec<OptionLabel>")]
pub struct OptionSet(Vec<OptionLabel>);
impl OptionSet {
pub const MAX: usize = 16;
pub fn new(labels: Vec<OptionLabel>) -> Result<Self, FormError> {
let mut distinct: Vec<&OptionLabel> = labels.iter().collect();
distinct.sort();
distinct.dedup();
if (1..=Self::MAX).contains(&labels.len()) && distinct.len() == labels.len() {
Ok(Self(labels))
} else {
Err(FormError::new(
"OptionSet",
format!("{} labels", labels.len()),
))
}
}
pub fn contains(&self, label: &OptionLabel) -> bool {
self.0.contains(label)
}
}
impl TryFrom<Vec<OptionLabel>> for OptionSet {
type Error = FormError;
fn try_from(v: Vec<OptionLabel>) -> Result<Self, FormError> {
Self::new(v)
}
}
impl From<OptionSet> for Vec<OptionLabel> {
fn from(s: OptionSet) -> Self {
s.0
}
}
impl JsonSchema for OptionSet {
fn schema_name() -> std::borrow::Cow<'static, str> {
"OptionSet".into()
}
fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "array",
"items": g.subschema_for::<OptionLabel>(),
"minItems": 1,
"maxItems": 16,
"uniqueItems": true
})
}
}
/// Where a feature may sit in a packet.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum Slot {
Discourse,
Referent,
Predication,
Relation,
Stance,
/// The relation of the speaker to the addressee in the scene; see
/// `LoweringInput::with_scene`.
Scene,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum ValueKind {
Grade,
Options(OptionSet),
Flag,
}
/// Key: the entry's id, `<family>:<n>`.
#[derive(Clone, Debug, PartialEq, Eq, JsonSchema, DatabaseEntry)]
#[cultcache(type = "weksa.inventory_entry")]
pub struct InventoryEntry {
#[cultcache(key = 0)]
pub id: InvId,
/// 1 to 120 bytes, no control character (checked at admission).
#[cultcache(key = 1)]
pub gloss: String,
/// Present exactly on features and extensions.
#[cultcache(key = 2)]
pub value_kind: Option<ValueKind>,
/// Non-empty exactly on features.
#[cultcache(key = 3)]
pub applies_to: Vec<Slot>,
#[cultcache(key = 4)]
pub introduced_rev: u32,
#[cultcache(key = 5)]
pub retired_rev: Option<u32>,
}
impl InventoryEntry {
pub fn family(&self) -> Family {
self.id.family()
}
/// 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.
pub fn well_formed(&self) -> bool {
match self.family() {
Family::Feature => self.value_kind.is_some() && !self.applies_to.is_empty(),
Family::Extension => self.value_kind.is_some() && self.applies_to.is_empty(),
_ => self.value_kind.is_none() && self.applies_to.is_empty(),
}
}
/// Whether a packet of inventory revision `rev` may name this entry.
pub fn live_at(&self, rev: u32) -> bool {
self.introduced_rev <= rev && self.retired_rev.is_none_or(|r| r > rev)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum Change {
Add(InvId),
Retire(InvId),
}
/// Key: `rev:<n>`.
#[derive(Clone, Debug, PartialEq, Eq, JsonSchema, DatabaseEntry)]
#[cultcache(type = "weksa.inventory_revision")]
pub struct InventoryRevision {
#[cultcache(key = 0)]
pub n: u32,
#[cultcache(key = 1)]
pub parent: Option<u32>,
#[cultcache(key = 2)]
pub changes: Vec<Change>,
#[cultcache(key = 3)]
pub frozen: bool,
}
pub fn revision_key(n: u32) -> String {
format!("rev:{n}")
}
/// The whole inventory as the store holds it.
#[derive(Clone, Debug, Default)]
pub struct Inventory {
entries: BTreeMap<InvId, InventoryEntry>,
revisions: BTreeMap<u32, InventoryRevision>,
}
impl Inventory {
/// An inventory exists only as `InventoryStore::inventory` returns it, so
/// no caller can validate or admit against a fabricated one.
///
/// ```compile_fail,E0624
/// // `Inventory::new` is not callable outside the crate.
/// let _ = weksa::Inventory::new(vec![], vec![]);
/// ```
pub(crate) fn new(entries: Vec<InventoryEntry>, revisions: Vec<InventoryRevision>) -> Self {
Self {
entries: entries.into_iter().map(|e| (e.id, e)).collect(),
revisions: revisions.into_iter().map(|r| (r.n, r)).collect(),
}
}
pub fn entries(&self) -> impl Iterator<Item = &InventoryEntry> {
self.entries.values()
}
pub fn entry(&self, id: &InvId) -> Option<&InventoryEntry> {
self.entries.get(id)
}
pub fn revisions(&self) -> impl Iterator<Item = &InventoryRevision> {
self.revisions.values()
}
pub fn has_revision(&self, n: u32) -> bool {
self.revisions.contains_key(&n)
}
pub fn latest(&self) -> Option<&InventoryRevision> {
self.revisions.values().next_back()
}
/// The entry `id`, if a packet of revision `rev` may name it.
pub fn resolve(&self, id: &InvId, rev: u32) -> Option<&InventoryEntry> {
self.entry(id).filter(|e| e.live_at(rev))
}
}
/// Revision 1: the standard's enumerated values and the nuance features the
/// loop starts from. Concepts start empty.
pub fn seed() -> (InventoryRevision, Vec<InventoryEntry>) {
let mut entries: Vec<InventoryEntry> = Vec::new();
let mut add = |family: Family, gloss: &str, kind: Option<ValueKind>, slots: &[Slot]| {
let taken = entries.iter().filter(|e| e.family() == family).count() as u32;
entries.push(InventoryEntry {
id: family.id(NonZeroU32::MIN.saturating_add(taken)),
gloss: gloss.to_string(),
value_kind: kind,
applies_to: slots.to_vec(),
introduced_rev: 1,
retired_rev: None,
});
};
for g in [
"person",
"group",
"organism",
"place",
"artifact",
"institution",
"ecology",
"route",
"memory",
"event",
"abstract",
] {
add(Family::ReferentKind, g, None, &[]);
}
for g in [
"coreference",
"part_of",
"member_of",
"located_at",
"near",
"before",
"after",
"causes",
"enables",
"prevents",
"owns",
"owes",
"knows",
"names",
"represents",
"contrasts_with",
] {
add(Family::Relation, g, None, &[]);
}
for g in [
"agent",
"patient",
"experiencer",
"instrument",
"source",
"goal",
"location",
"beneficiary",
] {
add(Family::Role, g, None, &[]);
}
for g in [
"assert",
"ask",
"request",
"promise",
"warn",
"greet",
"thank",
"apologise",
] {
add(Family::SpeechAct, g, None, &[]);
}
let options = |labels: &[&str]| {
let labels = labels
.iter()
.map(|l| OptionLabel::parse(l).expect("seed labels are well formed"))
.collect();
Some(ValueKind::Options(
OptionSet::new(labels).expect("seed option sets are well formed"),
))
};
let pred = [Slot::Predication];
add(
Family::Feature,
"polarity",
options(&["positive", "negative"]),
&pred,
);
add(
Family::Feature,
"tense",
options(&["past", "present", "future", "timeless"]),
&pred,
);
add(
Family::Feature,
"aspect",
options(&[
"perfective",
"imperfective",
"progressive",
"habitual",
"perfect",
]),
&pred,
);
add(
Family::Feature,
"modality",
options(&[
"actual",
"potential",
"possible",
"necessary",
"desired",
"obligated",
]),
&pred,
);
add(
Family::Feature,
"evidentiality",
options(&["direct", "inferred", "reported", "assumed"]),
&pred,
);
let nuance = [Slot::Discourse, Slot::Predication];
for g in ["hedge", "certainty"] {
add(Family::Feature, g, Some(ValueKind::Grade), &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();
(
InventoryRevision {
n: 1,
parent: None,
changes,
frozen: false,
},
entries,
)
}
/// Revision 2: the concepts and frames the `nibu-proof-001` packet names, which
/// the seed leaves open. Hand-seeded like [`seed`] until the golf loop's
/// minting round owns concepts and frames.
pub fn proof_revision() -> (InventoryRevision, Vec<InventoryEntry>) {
let mut entries: Vec<InventoryEntry> = Vec::new();
for (family, glosses) in [
(
Family::Concept,
[
"endanger",
"prevent",
"player",
"control",
"ship_mind",
"request",
]
.as_slice(),
),
(
Family::Frame,
["hazard_warning", "intervention", "request_intervention"].as_slice(),
),
] {
for (i, gloss) in glosses.iter().enumerate() {
entries.push(InventoryEntry {
id: family.id(NonZeroU32::MIN.saturating_add(i as u32)),
gloss: gloss.to_string(),
value_kind: None,
applies_to: vec![],
introduced_rev: 2,
retired_rev: None,
});
}
}
let changes = entries.iter().map(|e| Change::Add(e.id)).collect();
(
InventoryRevision {
n: 2,
parent: Some(1),
changes,
frozen: false,
},
entries,
)
}
/// Revision 3: the speaker's relation to the addressee, features of slot
/// `Scene`. Hand-seeded like [`proof_revision`] until the loop's minting round
/// and cut `speaker-scene` own the Scene vocabulary. `relative_status` is the
/// speaker's standing against the addressee's, not a register word:
/// what it selects is each language's ontology's business. It mints the three
/// Scene features after the seed's features.
pub fn scene_revision() -> (InventoryRevision, Vec<InventoryEntry>) {
let taken = seed()
.1
.iter()
.filter(|e| e.family() == Family::Feature)
.count() as u32;
let labels = ["below", "level", "above"]
.map(|l| OptionLabel::parse(l).expect("seed labels are well formed"));
let relative_status = ValueKind::Options(
OptionSet::new(labels.to_vec()).expect("seed option sets are well formed"),
);
let mut entries = Vec::new();
for (i, (gloss, kind)) in [
("relative_status", relative_status),
("intimacy", ValueKind::Grade),
("in_group", ValueKind::Flag),
]
.into_iter()
.enumerate()
{
entries.push(InventoryEntry {
id: Family::Feature.id(NonZeroU32::MIN.saturating_add(taken + i as u32)),
gloss: gloss.to_string(),
value_kind: Some(kind),
applies_to: vec![Slot::Scene],
introduced_rev: 3,
retired_rev: None,
});
}
let changes = entries.iter().map(|e| Change::Add(e.id)).collect();
(
InventoryRevision {
n: 3,
parent: Some(2),
changes,
frozen: false,
},
entries,
)
}