Merge weksa-lowering/speaker-state into main; adapt the lowerer to admit-only LoweringInput

No textual conflict except data/interlingua/inventory.cc (binary): regenerated as
the branch's rev 1 (61 entries) plus `weksa inventory proof` (rev 2, 7 entries).
Semantic breaks fixed: proof_packet() carries an empty Stance; `weksa lower`
re-admits the stored packet (idempotent) because validate no longer returns the
view; tests/lower.rs builds views through PacketStore::admit; seed_reseeded
expects seed plus proof revision. AGENTS.md records the CARGO_HOME requirement
and the proof packet key.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Weksa 2026-11-03 15:54:42 +00:00
commit c1b2778b15
16 changed files with 404 additions and 55 deletions

View file

@ -51,3 +51,13 @@ The live state architecture is ported from EpiphanyAgent in reduced form:
Project truth does not belong in agent personality memory. If Weksa later grows
role dossiers or heartbeat scheduling, those surfaces must remain separate from
language-project truth.
## Building And Verifying
- `crates/weksa` depends on CultLib's `cultcache-rs` as a git dependency pinned by
`rev`, fetched from GitHub. With the default `CARGO_HOME` that fetch fails on a
Mind's host. Use the pre-fetched home:
`CARGO_HOME=/var/lib/mind-weksa-self/cargo-home cargo test --workspace`.
- `weksa packet proof <store>` admits the nibu proof packet under the key
`packet:6e6962752d303031` (hex of `nibu-001`), not `nibu-proof-001`. Use that key
with `weksa lower <store> <key>`.

View file

@ -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<InventoryEntry>) {
&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 {

View file

@ -24,7 +24,7 @@ pub use inventory::{
};
pub use lower::{LowerError, lower};
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};

View file

@ -486,20 +486,22 @@ fn main() -> Result<()> {
println!("admitted {key}");
}
Command::Lower { store, key } => {
let inventory = InventoryStore::open(&cli.inventory)?.inventory()?;
let packet = PacketStore::open(&store)?
let inventory_store = InventoryStore::open(&cli.inventory)?;
let inventory = inventory_store.inventory()?;
let mut packets = PacketStore::open(&store)?;
let packet = packets
.get(&key)?
.with_context(|| format!("no packet {key}"))?;
let view = validate(&packet, &inventory).map_err(|refusals| {
anyhow::anyhow!(
"{}",
refusals
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ")
// Admission is the only source of a LoweringInput; re-admitting the
// stored packet is a no-op write that re-runs every door check.
let view = packets
.admit(
packet,
&inventory_store,
&CorpusStore::open(&cli.corpus)?,
&EntityStore::open(&cli.entities)?,
)
})?;
.map_err(refused)?;
println!("{}", lower(&view, &inventory)?);
}
Command::Score(args) => {

View file

@ -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>`, `k` a canonical
/// decimal 1..=9999.
@ -208,6 +208,8 @@ pub struct Packet {
pub extensions: Vec<Extension>,
#[cultcache(key = 12)]
pub trace: Trace,
#[cultcache(key = 13)]
pub stance: Stance,
}
impl Packet {
@ -262,6 +264,14 @@ pub struct Discourse {
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)]
pub struct FeatureValue {
/// Feature id.
@ -417,5 +427,6 @@ pub fn proof_packet() -> Packet {
notes: vec!["First proof packet for Nibu flavored-English lowering.".to_string()],
rejected_readings: vec![],
},
stance: Stance::default(),
}
}

View file

@ -16,7 +16,7 @@ use crate::entity::{Entity, EntityOrigin};
use crate::form::{is_attestation, is_joining_only, is_language_tag, is_refused_format};
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)]
@ -74,6 +74,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 },
}
@ -666,9 +669,10 @@ impl PacketStore {
self.cache.get::<Packet>(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.
@ -678,9 +682,15 @@ impl PacketStore {
inventory: &InventoryStore,
corpus: &CorpusStore,
entities: &EntityStore,
) -> Result<(), StoreError> {
validate(&packet, &inventory.inventory()?).map_err(StoreError::Invalid)?;
) -> Result<LoweringInput, StoreError> {
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 {
@ -702,11 +712,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)
}
}

View file

@ -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,42 @@ 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<LoweringInput, Vec<Refusal>> {
/// 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<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.
///
/// The first block is the same call through the public name, so the second
/// fails for the private path and nothing else.
///
/// ```
/// use weksa::{Inventory, packet::Packet};
/// fn judge(p: &Packet, i: &Inventory) { let _ = weksa::validate::validate(p, i); }
/// ```
///
/// ```compile_fail
/// use weksa::{Inventory, packet::Packet};
/// fn lower(p: &Packet, i: &Inventory) { let _ = weksa::validate::admissible(p, i); }
/// ```
pub(crate) fn admissible(
packet: &Packet,
inventory: &Inventory,
) -> Result<LoweringInput, Vec<Refusal>> {
let mut out = Vec::new();
if packet.schema_version != SCHEMA_VERSION {
out.push(Refusal::V1SchemaVersion {
@ -221,6 +257,7 @@ impl<'a> Inventoried<'a> {
predications,
relations,
extensions,
stance,
..
} = p;
let Context {
@ -238,6 +275,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 +387,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 +433,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 +473,7 @@ pub struct LoweringInput {
relations: Vec<Relation>,
constraints: Constraints,
extensions: Vec<Extension>,
stance: Stance,
}
impl LoweringInput {
@ -444,6 +488,7 @@ impl LoweringInput {
relations: p.relations.clone(),
constraints: p.constraints.clone(),
extensions: p.extensions.clone(),
stance: p.stance.clone(),
}
}
@ -474,4 +519,7 @@ impl LoweringInput {
pub fn extensions(&self) -> &[Extension] {
&self.extensions
}
pub fn stance(&self) -> &Stance {
&self.stance
}
}

View file

@ -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";
@ -213,6 +217,7 @@ pub fn world() -> World {
Slot::Referent,
Slot::Predication,
Slot::Relation,
Slot::Stance,
],
)
},
@ -310,5 +315,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)),
},
],
},
}
}

View file

@ -395,7 +395,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 {
@ -420,4 +420,10 @@ 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"
);
}

View file

@ -22,8 +22,24 @@ fn committed() -> (Inventory, tempfile::TempDir) {
(inv.inventory().unwrap(), tempfile::tempdir().unwrap())
}
fn view(packet: &Packet, inv: &Inventory) -> LoweringInput {
validate(packet, inv).unwrap()
/// A `LoweringInput` has one source, `PacketStore::admit`, so every packet
/// under test goes through a throwaway store backed by the committed stores.
fn view(packet: &Packet, _inv: &Inventory) -> LoweringInput {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let inventory = InventoryStore::open(&root.join("data/interlingua/inventory.cc")).unwrap();
admitted(packet, &inventory)
}
/// The same door against an inventory store the test built.
fn admitted(packet: &Packet, inventory: &InventoryStore) -> LoweringInput {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let corpus = CorpusStore::open(&root.join("data/corpus/corpus.cc")).unwrap();
let entities = EntityStore::open(&root.join("data/entity/entity.cc")).unwrap();
let dir = tempfile::tempdir().unwrap();
PacketStore::open(&dir.path().join("p.cc"))
.unwrap()
.admit(packet.clone(), inventory, &corpus, &entities)
.unwrap()
}
fn lowered(packet: &Packet, inv: &Inventory) -> Result<String, LowerError> {
@ -137,7 +153,7 @@ fn a_packet_carrying_what_the_lowerer_does_not_render_is_refused() {
let mut p = proof_packet();
edit(&mut p);
assert_eq!(
lower(&view(&p, &inv), &inv),
lower(&admitted(&p, &store), &inv),
Err(LowerError::Unrendered { what }),
"{what}"
);
@ -187,7 +203,7 @@ fn a_row_cannot_speak_for_an_id_whose_gloss_differs() {
store.admit_revision(rev2, entries).unwrap();
let inv = store.inventory().unwrap();
assert_eq!(
lowered(&proof_packet(), &inv),
lower(&admitted(&proof_packet(), &store), &inv),
Err(LowerError::GlossMismatch {
id: iv("concept:4"),
row: "control"

View file

@ -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,
@ -867,7 +868,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();
@ -891,7 +892,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
@ -909,6 +910,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())
@ -945,7 +986,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;
@ -960,13 +1001,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!(
@ -977,6 +1023,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]
@ -1206,6 +1270,21 @@ 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), then the nibu-proof revision.
let committed = InventoryStore::open(&root.join("data/interlingua/inventory.cc"))
.unwrap()
.inventory()
.unwrap();
let (rev, mut entries) = seed();
let (proof, proof_entries) = weksa::inventory::proof_revision();
assert!(committed.has_revision(rev.n) && committed.has_revision(proof.n));
entries.extend(proof_entries);
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 {

View file

@ -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<Refusal> {
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),

Binary file not shown.

View file

@ -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`

View file

@ -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",

View file

@ -74,7 +74,8 @@
"Discourse",
"Referent",
"Predication",
"Relation"
"Relation",
"Stance"
]
},
"ValueKind": {