From 86371d2ce4a5f699b047e2a9cc6b5f7a03c331a4 Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 29 Oct 2026 17:37:02 +0100 Subject: [PATCH 1/4] 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 --- crates/weksa/src/inventory.rs | 21 ++++++- crates/weksa/src/lib.rs | 2 +- crates/weksa/src/packet.rs | 12 +++- crates/weksa/src/store.rs | 26 ++++++--- crates/weksa/src/validate.rs | 58 ++++++++++++++++--- crates/weksa/tests/common/mod.rs | 19 ++++++- crates/weksa/tests/entities.rs | 5 +- crates/weksa/tests/stores.rs | 87 ++++++++++++++++++++++++++-- crates/weksa/tests/validate.rs | 97 +++++++++++++++++++++++++++++++- docs/interlingua-standard.md | 34 ++++++++--- 10 files changed, 324 insertions(+), 37 deletions(-) diff --git a/crates/weksa/src/inventory.rs b/crates/weksa/src/inventory.rs index 1106229..792cad4 100644 --- a/crates/weksa/src/inventory.rs +++ b/crates/weksa/src/inventory.rs @@ -188,6 +188,7 @@ pub enum Slot { Referent, Predication, Relation, + Stance, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -439,10 +440,28 @@ pub fn seed() -> (InventoryRevision, Vec) { &pred, ); 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, "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 { diff --git a/crates/weksa/src/lib.rs b/crates/weksa/src/lib.rs index ddd36c6..479366b 100644 --- a/crates/weksa/src/lib.rs +++ b/crates/weksa/src/lib.rs @@ -22,7 +22,7 @@ pub use inventory::{ Slot, ValueKind, }; 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 split::{Split, SplitScheme}; diff --git a/crates/weksa/src/packet.rs b/crates/weksa/src/packet.rs index 1708707..73199a2 100644 --- a/crates/weksa/src/packet.rs +++ b/crates/weksa/src/packet.rs @@ -16,7 +16,7 @@ pub use crate::inventory::InvId; use crate::inventory::OptionLabel; /// 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` a canonical /// decimal 1..=9999. @@ -208,6 +208,8 @@ pub struct Packet { pub extensions: Vec, #[cultcache(key = 12)] pub trace: Trace, + #[cultcache(key = 13)] + pub stance: Stance, } impl Packet { @@ -262,6 +264,14 @@ pub struct Discourse { pub features: Vec, } +/// 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, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct FeatureValue { /// Feature id. diff --git a/crates/weksa/src/store.rs b/crates/weksa/src/store.rs index 6b0c858..abf66fc 100644 --- a/crates/weksa/src/store.rs +++ b/crates/weksa/src/store.rs @@ -15,7 +15,7 @@ use crate::entity::Entity; use crate::form::is_language_tag; use crate::inventory::{Change, InvId, Inventory, InventoryEntry, InventoryRevision, revision_key}; use crate::packet::{EntityId, Packet}; -use crate::validate::{Refusal, validate}; +use crate::validate::{LoweringInput, Refusal, admissible}; /// A broken store rule, named. #[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 /// item nor the item's speaker or a prior-turn speaker. 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. Malformed { key: String, why: &'static str }, } @@ -598,9 +601,10 @@ impl PacketStore { self.cache.get::(key) } - /// The only door: a packet exists only if `validate` passes against the - /// inventory the store holds at the packet's revision, its `corpus_item` - /// resolves, every referent's entity is in the entity store and is named on + /// The only door, and the only source of a [`LoweringInput`]: a packet + /// exists only if the validator's rules pass against the inventory the + /// 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 /// key holds its packet for good. A packet that names no corpus item has no /// door here. @@ -610,9 +614,15 @@ impl PacketStore { inventory: &InventoryStore, corpus: &CorpusStore, entities: &EntityStore, - ) -> Result<(), StoreError> { - validate(&packet, &inventory.inventory()?).map_err(StoreError::Invalid)?; + ) -> Result { + let view = admissible(&packet, &inventory.inventory()?).map_err(StoreError::Invalid)?; 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()) { if entities.get(entity)?.is_none() { return Err(StoreRefusal::UnknownEntity { @@ -634,11 +644,11 @@ impl PacketStore { let key = packet.id.to_string(); if let Some(existing) = self.get(&key)? { if existing == packet { - return Ok(()); + return Ok(view); } return Err(StoreRefusal::PacketImmutable { key }.into()); } self.cache.put(key, &packet)?; - Ok(()) + Ok(view) } } diff --git a/crates/weksa/src/validate.rs b/crates/weksa/src/validate.rs index a88c18e..c816ccd 100644 --- a/crates/weksa/src/validate.rs +++ b/crates/weksa/src/validate.rs @@ -1,6 +1,9 @@ -//! The validator owns whether a packet exists. Every path that admits or lowers a -//! packet calls `validate`, and its Ok value, [`LoweringInput`], is the only -//! thing a lowerer sees. The rules are V1..V8 and V10..V12: the shape of an id +//! The validator owns the inventory and form rules a packet must meet. Every +//! path that admits a packet applies them, but `validate` only judges: it +//! returns `Ok(())` or the refusals. [`LoweringInput`], the only thing a +//! lowerer sees, comes from `PacketStore::admit` alone, which builds it through +//! the crate-private `admissible`. The stance is checked like every feature +//! list, in slot `Stance`. The rules are V1..V8 and V10..V12: the shape of an id //! or a grade is its type's parse, not a rule, so V9 is retired and not //! renumbered. //! @@ -25,7 +28,7 @@ use std::fmt; use crate::inventory::{Family, InvId, Inventory, InventoryEntry, Slot, ValueKind}; use crate::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. @@ -91,9 +94,34 @@ impl fmt::Display for Refusal { } } -/// Whether `packet` may exist under `inventory`. All broken rules are returned; -/// a packet that passes yields the lowering view, which only this function builds. -pub fn validate(packet: &Packet, inventory: &Inventory) -> Result> { +/// Whether `packet` meets the rules under `inventory`. All broken rules are +/// returned. A pass carries no lowering view: that comes from +/// `PacketStore::admit` only. +/// +/// ``` +/// use weksa::{Inventory, Refusal, packet::Packet}; +/// fn judge(p: &Packet, i: &Inventory) -> Result<(), Vec> { weksa::validate(p, i) } +/// ``` +/// +/// ```compile_fail +/// use weksa::{Inventory, LoweringInput, Refusal, packet::Packet}; +/// fn lower(p: &Packet, i: &Inventory) -> Result> { weksa::validate(p, i) } +/// ``` +pub fn validate(packet: &Packet, inventory: &Inventory) -> Result<(), Vec> { + 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> { let mut out = Vec::new(); if packet.schema_version != SCHEMA_VERSION { out.push(Refusal::V1SchemaVersion { @@ -221,6 +249,7 @@ impl<'a> Inventoried<'a> { predications, relations, extensions, + stance, .. } = p; let Context { @@ -238,6 +267,7 @@ impl<'a> Inventoried<'a> { self.slot(id, Family::SpeechAct, "speech act"); } self.features(features, Slot::Discourse); + self.features(&stance.features, Slot::Stance); for Referent { kind, 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 /// 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 -/// cannot be handed an unvalidated packet or the key to an answer text. +/// fields are private and the constructor is crate-private, called only by +/// `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 /// 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; } /// ``` /// +/// ```compile_fail +/// fn read(l: weksa::LoweringInput) { let _ = l.stance; } +/// ``` +/// /// 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. @@ -430,6 +465,7 @@ pub struct LoweringInput { relations: Vec, constraints: Constraints, extensions: Vec, + stance: Stance, } impl LoweringInput { @@ -444,6 +480,7 @@ impl LoweringInput { relations: p.relations.clone(), constraints: p.constraints.clone(), extensions: p.extensions.clone(), + stance: p.stance.clone(), } } @@ -474,4 +511,7 @@ impl LoweringInput { pub fn extensions(&self) -> &[Extension] { &self.extensions } + pub fn stance(&self) -> &Stance { + &self.stance + } } diff --git a/crates/weksa/tests/common/mod.rs b/crates/weksa/tests/common/mod.rs index a26c462..aecf5d0 100644 --- a/crates/weksa/tests/common/mod.rs +++ b/crates/weksa/tests/common/mod.rs @@ -19,7 +19,11 @@ pub const WARN: &str = "speech_act:5"; pub const POLARITY: &str = "feature:1"; pub const MODALITY: &str = "feature:4"; 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. pub const CONCEPT_A: &str = "concept:1"; pub const CONCEPT_B: &str = "concept:2"; @@ -211,6 +215,7 @@ pub fn world() -> World { Slot::Referent, Slot::Predication, Slot::Relation, + Slot::Stance, ], ) }, @@ -308,5 +313,17 @@ pub fn good() -> Packet { notes: vec!["a note".into()], 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)), + }, + ], + }, } } diff --git a/crates/weksa/tests/entities.rs b/crates/weksa/tests/entities.rs index 30d43e3..11e3d7d 100644 --- a/crates/weksa/tests/entities.rs +++ b/crates/weksa/tests/entities.rs @@ -391,7 +391,7 @@ fn packet_entities_resolve() { #[test] fn packet_schema_v2() { - assert_eq!(SCHEMA_VERSION, 3); + assert_eq!(SCHEMA_VERSION, 4); let mut p = good(); p.schema_version = 2; 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(); assert!(item["properties"].get("names").is_some()); 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"); } diff --git a/crates/weksa/tests/stores.rs b/crates/weksa/tests/stores.rs index 2c6ab7c..1e540e7 100644 --- a/crates/weksa/tests/stores.rs +++ b/crates/weksa/tests/stores.rs @@ -3,6 +3,7 @@ use common::*; use tempfile::TempDir; use weksa::corpus::{CorpusItem, ModelFamily, Source, Turn}; use weksa::inventory::{Change, seed}; +use weksa::packet::PacketId; use weksa::{ AddOutcome, CorpusStore, EntityStore, Family, InventoryEntry, InventoryRevision, 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::Role), 8); assert_eq!(count(Family::SpeechAct), 8); - assert_eq!(count(Family::Feature), 11); + assert_eq!(count(Family::Feature), 18); assert_eq!( count(Family::Concept) + count(Family::Frame) + count(Family::Extension), 0 @@ -582,6 +583,46 @@ fn the_seed_is_the_standards_enumerations_and_the_nuance_features() { inv.entry(&iv(IRONY)).unwrap().value_kind, 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!( inv.entries() .all(|e| e.well_formed() && e.introduced_rev == 1 && e.retired_rev.is_none()) @@ -618,7 +659,7 @@ fn gloss_is_bounded_text() { #[test] 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 bad = good(); bad.schema_version = 5; @@ -633,13 +674,18 @@ fn packet_admission_goes_through_the_validator() { other => panic!("{other:?}"), } 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) .unwrap(); - // The same packet is idempotent; a different one under the same key is refused. - store + assert_eq!(view.stance(), &good().stance); + 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) .unwrap(); + assert_eq!(replay, view); let mut other = good(); other.provenance.confidence = grade(1); assert_eq!( @@ -650,6 +696,24 @@ fn packet_admission_goes_through_the_validator() { ); assert_eq!(store.all().unwrap().len(), 1); 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] @@ -862,6 +926,19 @@ fn seed_reseeded() { .all() .unwrap(); 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 mut replay = CorpusStore::open(&d.path().join("c.cc")).unwrap(); for i in items { diff --git a/crates/weksa/tests/validate.rs b/crates/weksa/tests/validate.rs index cdf7644..3453376 100644 --- a/crates/weksa/tests/validate.rs +++ b/crates/weksa/tests/validate.rs @@ -1,7 +1,7 @@ mod common; use common::*; use weksa::packet::*; -use weksa::{Family, Refusal, Slot, validate}; +use weksa::{Family, PacketStore, Refusal, Slot, validate}; fn refusals(p: &Packet) -> Vec { validate(p, &inventory()).err().unwrap_or_default() @@ -334,6 +334,83 @@ fn v8_feature_applies_to() { 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] fn v10_rev_exists() { let mut p = good(); @@ -358,7 +435,12 @@ fn lowering_input_carries_no_trace_id_or_provenance() { let mut p = good(); p.trace.notes = vec!["SECRET-NOTE".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:?}"); for hidden in [PACKET_ID, ENCODER_ID, "item:nibu:1", "SECRET"] { 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.discourse(), &p.discourse); 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] @@ -435,6 +520,14 @@ fn v11_duplicate_feature_each_slot() { refusals(&relation), vec![v11(Some(Slot::Relation), FEAT_ALL)] ); + let mut stance = good(); + stance.stance.features = vec![grade_feature(1), grade_feature(2)]; + assert_eq!(refusals(&stance), vec![v11(Some(Slot::Stance), FEAT_ALL)]); + let mut felt = good(); + felt.stance + .features + .push(feat(ANGER, FeatValue::Grade(grade(5)))); + assert_eq!(refusals(&felt), vec![v11(Some(Slot::Stance), ANGER)]); let mut extension = good(); extension.extensions.push(Extension { key: iv(EXT), diff --git a/docs/interlingua-standard.md b/docs/interlingua-standard.md index a59a963..8b36983 100644 --- a/docs/interlingua-standard.md +++ b/docs/interlingua-standard.md @@ -127,6 +127,7 @@ Packet constraints { preserve, deterministic, max_latency_ms } extensions [ { key: InvId (family extension), value: Grade | Option | Flag } ] trace { notes, rejected_readings } + stance { features } ``` `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 -A packet exists only if `validate` passes; every path that admits or lowers a -packet calls it. A refusal names the rule. +A packet exists only if the validator's rules pass and the packet store admits +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. - 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). 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 -packet that passes; it carries no packet id, no provenance and no trace, and is -built nowhere else. A packet is admitted against the inventory the packet store +an extension key's family is V6. `PacketStore::admit` returns the lowering view of a +packet it admits; the view carries the meaning and the stance, no packet id, no +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. ### `kind` @@ -182,9 +187,22 @@ ids. ### `discourse` 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, -intensity, irony and whatever the inventory grows). This is source-side -communicative pressure, not target-language register. +and `features` carry source-side nuance on content (hedge, certainty, irony and +whatever the inventory grows). This is source-side communicative pressure, not +target-language register. + +### `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` From 283678a58be7191e0c2b4fc56461280344682e73 Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 29 Oct 2026 17:38:05 +0100 Subject: [PATCH 2/4] speaker-state: reseed inventory rev 1, regenerate schemas (packet v4), fmt Co-Authored-By: Claude Opus 5.5 --- crates/weksa/tests/entities.rs | 5 ++++- data/interlingua/inventory.cc | Bin 6342 -> 7052 bytes .../weksa.interlingua_packet.schema.json | 21 +++++++++++++++++- .../cultnet/weksa.inventory_entry.schema.json | 3 ++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/weksa/tests/entities.rs b/crates/weksa/tests/entities.rs index 11e3d7d..4f8f2f2 100644 --- a/crates/weksa/tests/entities.rs +++ b/crates/weksa/tests/entities.rs @@ -418,5 +418,8 @@ fn packet_schema_v2() { 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"); + assert_eq!( + packet["$defs"]["Stance"]["properties"]["features"]["type"], + "array" + ); } diff --git a/data/interlingua/inventory.cc b/data/interlingua/inventory.cc index 037a2cc110d4361f2c15535582c384dedd240113..b7b5dc4f7107d6a1fd507480c589b650cc024d6c 100644 GIT binary patch delta 1374 zcmaKsJ8aWX6oyGmaN<0jCVNW)QkT*+jh&`IDUXInd9^&MZdP*B22{KCbtDi2LSh7@ z$fg4eLkVe^Dlx#oP*7k;RIspgWMQXLc|?N$UOagF&hPV||JXY6hui~oW6Kvg>U3&E zvl=ETc%R;!VURX22xe6=?~#en_X z0JTLIwI$ExFG<-Uc=_;R$0XD;wYu)w-X0K!aUgTTE$|ni2{!gRU@34P{5YV2cLOAi z=tHYG!J_@kKV-M}W%<*?)3KJOFA(?XiK2qJC&eq^ryTEy*Pvey&VV<9SHY1G(@BWw zZ$oqF>6h62I}+>Pk}jZUFnkewH+&xaDSQT;i7=*-v(UR7r=x7n%_z(2Mp=J4HVyA$ zjAgxvm7z;>D~9f9pGkIpZld=Ml%-scHG&J-ruxI=;=Xdyd#2)2Wv;X)3JTTbry_ XxΜ&b#VSS6y(`imO&#b@APAv`4&{ delta 807 zcmeA%KW4b$3M04ql$B|zi6x~)saBIWGO4f|S{a#G8Jkb$Vk(q_vZIcePFn?2WN5H5 zGp{5yuQ+pZ9f#6nUnVY)HZizT106QFwux}|Ew(9eb}4%c zj6I1X5yqUH!I3Hhx5aQ8+|?G#$`gxnOEM-maA-~z;8c-?D>6YSvRs*#UzD4e195Z$ zr!m~vS)3to_E#i!09P1X{34I=vXY(V>ImnN&OG*Gqjerka?>+&9 zT|$EHaPb5|guNRCJ>cU1k;MIl+~DGKgdE`P$3jkUwvI4DO{K6aT>Pl8Bb?1Ef(XN4 z5ofsgToHsjKZqdQ=^~2o%T!TB7`{Sc+lzU_&6y~MFy{#p+g#j70G48oGWShkJaC6$ PvY>?M<`v?_%#4QsoyFTA diff --git a/schemas/cultnet/weksa.interlingua_packet.schema.json b/schemas/cultnet/weksa.interlingua_packet.schema.json index 24d7507..d839203 100644 --- a/schemas/cultnet/weksa.interlingua_packet.schema.json +++ b/schemas/cultnet/weksa.interlingua_packet.schema.json @@ -55,6 +55,9 @@ "format": "uint32", "minimum": 0 }, + "stance": { + "$ref": "#/$defs/Stance" + }, "trace": { "$ref": "#/$defs/Trace" } @@ -72,7 +75,8 @@ "relations", "constraints", "extensions", - "trace" + "trace", + "stance" ], "$defs": { "Constraints": { @@ -446,6 +450,21 @@ "filler" ] }, + "Stance": { + "description": "The speaker's felt affect and restraint, never displayed affect: how the\nspeaker shows a feeling is each language's ontology's business. A\nheld-back feeling is not a claim, so it is never a predication.", + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "$ref": "#/$defs/FeatureValue" + } + } + }, + "required": [ + "features" + ] + }, "Trace": { "description": "Authoring and debug notes. Stripped before lowering.", "type": "object", diff --git a/schemas/cultnet/weksa.inventory_entry.schema.json b/schemas/cultnet/weksa.inventory_entry.schema.json index 6187f02..ffefd95 100644 --- a/schemas/cultnet/weksa.inventory_entry.schema.json +++ b/schemas/cultnet/weksa.inventory_entry.schema.json @@ -74,7 +74,8 @@ "Discourse", "Referent", "Predication", - "Relation" + "Relation", + "Stance" ] }, "ValueKind": { From ad02feeeb6c603ac2fccf32a5b13ea8dc1d6b4f2 Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 29 Oct 2026 17:38:51 +0100 Subject: [PATCH 3/4] speaker-state: rev 1 now has 61 entries (stance features in, three display features out) Co-Authored-By: Claude Opus 5.5 --- crates/weksa/tests/stores.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/weksa/tests/stores.rs b/crates/weksa/tests/stores.rs index 1e540e7..c21e49a 100644 --- a/crates/weksa/tests/stores.rs +++ b/crates/weksa/tests/stores.rs @@ -541,7 +541,7 @@ fn retiring_keeps_the_entry_for_earlier_revisions() { fn revision_one_survives_revision_two() { let inv = inventory(); let revisions: Vec<(u32, usize)> = inv.revisions().map(|r| (r.n, r.changes.len())).collect(); - assert_eq!(revisions, vec![(1, 54), (2, 6), (3, 1), (4, 1)]); + assert_eq!(revisions, vec![(1, 61), (2, 6), (3, 1), (4, 1)]); // A packet of revision 1 is judged by the inventory of revision 1: the // revision is known (no V10), and it names nothing that came later. let mut p = good(); From 4fb817b18fe0e9a0dec02171384f21773212f29c Mon Sep 17 00:00:00 2001 From: Eureka Date: Thu, 29 Oct 2026 17:39:42 +0100 Subject: [PATCH 4/4] validate: positive twin for the admissible compile_fail doctest Co-Authored-By: Claude Opus 5.5 --- crates/weksa/src/validate.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/weksa/src/validate.rs b/crates/weksa/src/validate.rs index c816ccd..7fbf6aa 100644 --- a/crates/weksa/src/validate.rs +++ b/crates/weksa/src/validate.rs @@ -114,6 +114,14 @@ pub fn validate(packet: &Packet, inventory: &Inventory) -> Result<(), Vec