Compare commits

...
Author SHA1 Message Date
Weksa
218962fcf6 rust-to-dust: draft imperial language design spec (r0)
Speech community and status ladder, phonology, stance/restraint marking,
meaning decomposition, and the packet fields the lowerer must provide.
Evidence and invention tagged throughout.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 19:27:40 +00:00
157892a95a Merge pull request 'speaker-state onto main (stance in packet, admit-only LoweringInput) + build notes' (#3) from weksa-lowering/speaker-state-on-main into main 2026-11-03 19:27:33 +00:00
Weksa
c1b2778b15 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>
2026-11-03 15:54:42 +00:00
4fb817b18f validate: positive twin for the admissible compile_fail doctest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:39:42 +01:00
ad02feeeb6 speaker-state: rev 1 now has 61 entries (stance features in, three display features out)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:38:51 +01:00
283678a58b speaker-state: reseed inventory rev 1, regenerate schemas (packet v4), fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:38:05 +01:00
86371d2ce4 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>
2026-10-29 17:37:02 +01:00
17 changed files with 806 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": {

View file

@ -0,0 +1,402 @@
# Rust to Dust Imperial Language: Design Spec
Status: **DRAFT r0.** First conlang project for Rust to Dust. Nothing here is
canon until the operator rules on it.
The language has no name. Its working slug is `rtd-imperial`. A name is owed
and should come from the ontology and phonology below, as
[setting.md](../setting.md) asks, not from an English label. Nothing here
assigns one.
## How To Read This
Every claim carries a tag:
- **[E]** evidence. The source material says it, and the file is cited.
- **[I]** invention. This draft proposes it. The operator can overrule it, and
it is the part most worth attacking.
- **[E→I]** an extension. The source gives a seed and this draft develops it.
Sources read: everything under `settings/rust-to-dust/` (`README`, `setting`,
`aliens`, `story`, `battle-royale`, `language-design`), the house method in
`settings/eusocial-interbeing/language-design.md`, and Weksa's
`docs/language-model.md`, `interlingua-standard.md`,
`best-modern-approach.md`, `multilingual-utterance-lowering.md` and
`schemas/interlingua-packet.md`. The stance design is read from the unmerged
branch `weksa-lowering/speaker-state` (see Dependencies). The forge repos and
Weksa `state/` and `docs/` hold no other Rust to Dust material: a forge search
for the setting returned nothing outside Weksa. This draft therefore rests on
about 440 lines of setting prose, and the invention share is large.
## 1. Speech Community
### Who speaks it [E]
The empire's aliens: reptilian, scale-decorating, aesthetics-as-moral-truth,
honor as devotion to aesthetic perfection in a chosen field (`aliens.md`). The
empire is galaxy-spanning, so the language is a **network language**. Its
community is defined by the imperial cultural network (media, prestige,
commerce), not by a homeland. Humans are not native speakers. They meet it
through subtitles, memes and broadcasts, and their contaminated English is a
separate register (`language-design.md`, Registers).
### Speech situations [E→I]
`language-design.md` lists registers. This draft turns each into a situation
with a speaker, an addressee and a stake, because a register is a relationship
and not a vocabulary list:
| Situation | Speaker → addressee | Stake | Source |
|---|---|---|---|
| Broadcast commentary | commentator → mass imperial audience, about a performer | rendering an aesthetic verdict in real time | E (battle-royale) |
| High honor speech | warrior or master → rival, patron or witness | binding a vow, naming a performance | E (register list) |
| Warrior discourse | practitioner ↔ practitioner of one field | form, discipline, challenge | E (register list) |
| Commercial acquisition | buyer ↔ seller, or buyer → cultural object | access and integration phrased as uplift | E (setting: commerce as delivery rail) |
| Panel-show media | host ↔ guest before an audience | turning the guest's stance into performance | E (story: the panel) |
| Tourist admiration | tourist → anything | consuming a safe spectacle | E (aliens: tourists) |
| Address to humans | any → human | uplift, fascination, appraisal | I |
The last row is added. The story's central irony is that the empire
understands the father's refusal better than he understands the empire, so
how imperial speakers address humans is a design surface, not an afterthought.
The first proving slice is **broadcast commentary** [E]: `battle-royale.md`
names it as the runtime use, and it needs the fewest situations to be
distinguishable by a reader.
### Status ladder [I]
The source gives status *signals* (scale decoration marks profession, class,
rank, grief, celebrity, courtship, school, combat discipline, public
obligation, commitments) and a *measure* (the more elaborate the decoration,
the more visible the claim of virtue). It gives no ladder. The invention:
status is two axes, not one.
- **Field.** The discipline one is devoted to (combat, speech, hosting, trade,
politics, art). Honor is field-relative [E]. A master of one field has no
rank in another.
- **Standing in the field**, by *claim and demonstrated constraint*, not by
force:
1. **Unclaimed.** No decoration in a field. This is shameless, not neutral
[E: unpainted = shamelessness].
2. **Claimant.** Decorated, undemonstrated. Tourists live here: the claim is
bought and the constraint never chosen [E: tourists buy a safe spectacle].
3. **Practitioner.** Decorated, with constraint demonstrated in the field.
4. **Master.** Constraint demonstrated before witnesses.
5. **Exemplar.** Master whose form others cite.
- **Bare-by-claim** is an exceptional state off the ladder, not a step on it.
Going unpainted as an artistic claim that the performance alone is art is a
provocation [E]. The grammar must be able to say it without saying
"shameless".
The language's address and agreement system tracks **claim versus
demonstrated constraint**, because that is what the source treats as the
moral measure. A tourist's loud, expensive decoration is a *mismatch* the
language can state, and the language carries that mismatch in the grammar
itself.
Decoration is the primary status channel in the world. In speech it surfaces
as address terms, honorific-like marking and metaphor (Section 4), not as a
phoneme-level trick.
## 2. Phonology (provisional)
The source gives no phonetic profile. It poses the question
(`language-design.md`, Open Questions) and says naming should follow from
phonetics. House method (Zyphos) assigns Airawa hard dorsal and coronal
edges and Sa'auei'a soft flow. This language must not collapse onto either
[I].
### Design stance [I]
Three pressures from the source, turned into sound:
1. **Aesthetic devotion**: the language should sound *composed*. Prosody is
deliberate, with controlled length, no clutter.
2. **Chosen constraint is mastery**: constraint should be audible. Phonotactics
are *tight*, so that a speaker visibly refrains from the permitted-in-vulgar
alternatives.
3. **Reptilian body** [E: reptilian]: the inventory leans on sustained
fricatives, hisses and trills that a non-human vocal tract makes easy. The
source does not specify a vocal tract. This is the weakest-evidenced
choice and a human-pronounceable romanization is therefore required.
### Inventory (draft)
- **Stops** (voiceless only): `t`, `k`, `ʔ`. No voiced stops. This separates it
from Airawa's `g d q`.
- **Fricatives**: `s`, `ʃ`, `x`, `h`, with voiced `z`, `ʒ`. Fricatives
contrast in **length**: short and held. Held fricatives are the language's
main prosodic ornament and carry register weight (Section 3).
- **Sonorants**: `n`, `m`, `l`, trilled `r`.
- **Vowels**: `a`, `i`, `u`, `e`, each short or long. A long vowel is the
other register-bearing length.
- **Syllables**: `(C)V(C)`, coda `s n l ʔ` only. No clusters. The absence of
clusters is the phonotactic form of "chosen constraint".
### Prosody [I]
- Stress and length are the speaker's expressive budget. In formal
registers, *less* length variation reads as more command of the form. A
commentator in broadcast register may spend length on a performer's
moment, and an honor-speech speaker withholds it. Restraint is therefore
audible without a phoneme change (Section 3).
- Broadcast commentary has a lengthened, sibilant "announcer" delivery. Tourists
over-lengthen. Master speech under-lengthens.
### Romanization and rendering [I]
An ASCII romanization is owed before any lowering output is scored: the
speaker-state lowering renders text, and the renderer for the audience needs
a stable orthography. `ʃ ʒ x ʔ` need ASCII digraphs. The choice follows
the Weksa pronunciation-lowering docs and belongs to a later cut.
### Open
- The inventory is a sketch, drawn from three pressures. It should be rerun
after at least a hundred roots exist, not defended.
- The source calls for names to "emerge from ontology and phonetics". This
draft supplies phonetics, and the name waits for the first root set.
## 3. Register, Status and Restraint: Stance Explicit
### Weksa's constraint [E]
On the unmerged `speaker-state` branch, `docs/interlingua-standard.md` adds
a packet `stance`: **felt affect** (anger, fear, sadness, joy, disgust, surprise,
contempt, affection, shame, as grades) and **`restraint`**, how much of the
felt affect is held back (0 none, 1000 fully). It is felt, never displayed.
How a feeling shows and how much leaks is cultural; each language's ontology
maps felt affect and restraint to its own register. A held-back feeling is not
a claim, so it is never a predication.
This language must therefore **mark stance explicitly in its own grammar**,
and the marking is derived from the packet's `stance`, never from English
register words in the source text.
### Why this language is the hard case for restraint [E→I]
For the empire, restraint is not suppression of a shameful interior. It is
**the visible form of chosen constraint**, which is the moral proof of
mastery [E]. So the map from `restraint` to surface is *inverted* relative to
most human defaults:
- High restraint is the *display* of virtue. A fully restrained speaker with
strong felt affect is exhibiting form.
- Low restraint with strong affect is not honest. It is *vulgar* [E: "vulgar
violence as failed form"; "collapse into mere function"].
- Zero affect is not neutrality. It can read as an absence of devotion.
The deliberate consequence is that stance cannot be flattened to a politeness
level. Two axes are needed, and the source's lens questions (elegant,
disciplined, sincere, vulgar, memorable) ask for both.
### Obligatory categories [I]
Proposed grammatical commitments (the target-language selections that step 4
of `language-model.md` demands). Each names the packet field that feeds it.
1. **Finish marking** on predicates: an obligatory grade-class drawn from
`stance.restraint`, coarsened to a few classes. Working classes:
*raw*, *held*, *finished*. The unmarked form does not exist: leaving it
off is not neutrality but a gap that the community reads as vulgar.
Feeds from: `stance.restraint`.
2. **Leak channel**: how much felt affect shows *through* a high-restraint
frame. Leak is not mapped to emotion words. It is mapped to **form
choices**: clipped versus full inflection, length spent on a fricative,
word choice within a field. This follows `speaker-state`'s own example
(anger leaking through clipped syntax is stance). Feeds from: felt affect
grades against `restraint`.
3. **Standing agreement** on referents: field and standing-in-field of a
named performer, with the **claim/demonstrated** mismatch statable. Feeds
from: speaker and scene features (Section 6).
4. **Witness and form-evaluation**. Broadcast commentary is evaluative by
situation. Evaluation is expressed as a **separate discourse layer**
(a particle or clause layer), not as adjectives in the content: the
lens questions [E] (elegant, disciplined, sincere, vulgar, memorable,
promising, mastery, collapse) are the layer's open slots. It is
kept apart from content because a held-back evaluation is not a claim
about the world, and the interlingua keeps content and attitude
separate. Feeds from: `discourse` features and the situation.
5. **Evidential**: commentary asserts what the camera sees and *infers* the
performer's sincerity. The inference must be marked, because the source
puts sincerity in the performer, who is not the speaker and not available to
telemetry. Feeds from: the evidentiality feature the standard already
names.
### Register is a function of three inputs [I]
Register is not a setting on the speaker. It is selected from the pair
(speaker's standing and community, scene relation to addressee and subject)
and the situation, with stance as the final modulator. This is what
`persona-drives-ontology` implies on the speaker-state branch and what the
`language-ontology` cut will implement as deterministic selection
`select(profile, stance, speaker, scene)`. This spec supplies the **content
of that selection table** for this language; it does not write the table.
Cases the table must cover first:
| Case | Expected pull |
|---|---|
| Commentator, high restraint, strong approval of a restrained low-means act | finished marking; evaluative layer foregrounded; length withheld |
| Commentator, low restraint, strong approval | raw marking; the speaker is performing delight. Only permitted in tourist-adjacent registers |
| Master addressing rival | held or finished; address by field and standing |
| Tourist, any affect | claimant standing; over-lengthened; evaluation layer present but misapplied |
| Host to a human guest on a panel | warmth and appraisal mixed: affection plus contempt at high restraint is the *normal* pairing. The guest's rejection is absorbed as form |
The last row is the story's turn written as a stance mapping, and is worth
testing first: contempt and affection together, restrained, with the evaluation
layer calling the rejection *sincere*.
## 4. Meaning Before Surface
The language is never produced by substituting English words with alien
ones. The method is the Weksa path (`language-model.md`): packet, then
ontology projection that can split, collapse or reweight concepts, then
grammar commitments, morphology, phonology, surface.
### Concept splits and collapses [E→I]
The source names distinctions the language must make (`language-design.md`,
Open Questions). Each is decomposed below into components so a lowerer can
select a form from the packet's facts and not from a gloss.
| Distinction | Decomposition (hypothesis) | Needs from the packet |
|---|---|---|
| **victory** vs **mastery** vs **spectacle** vs **vulgar overmatch** | victory: outcome of a contest. mastery: outcome *plus* demonstrated chosen constraint. spectacle: attention drawn, independent of constraint. vulgar overmatch: means exceed the opposing means with no constraint chosen. | outcome; each party's means; whether the means were chosen |
| **crude** vs **failed** | crude: sincere, unrefined, with form attempted. failed: form collapsed into mere function. | actor's means and discipline as facts; the stance of the performer is not available, so the ontology reads from conduct |
| **resistance** admired and absorbable | resistance read as: threat, folk art, breakout talent (`battle-royale.md` commentary list). Choice depends on whether it centers a performer or a practice. | performer-centered vs distributed agency |
| **public spectacle** vs **living practice** [E] | spectacle: meaning lives in a central performer. practice: meaning lives in relationships. The empire reads distributed cooperation as noise or misattributes it to a performer. | number and relation of agents; presence of a focal agent |
| **unpainted**, split | shame / danger / contempt / rawness / refusal / purity (the source lists these senses). Different surface forms: the sense is selected by relation and stance, not by a gloss. | decoration state, scene relation, speaker stance |
### Scale decoration in grammar and metaphor [E→I]
Decoration is the language's central metaphor domain [E: used for rank,
grief, celebrity and more]. Proposed uses, to be tested not asserted:
- **Address**: terms of address name a decoration claim ("the one marked for
...") instead of a rank noun.
- **Insult**: lack, mismatch or fraud of decoration (a claim not backed by
constraint). The strongest insult is the claimant who is also vulgar.
- **Praise**: finish, as in a surface completed.
- **Metaphor**: grief and obligation are *worn*. A human speaking of loss
reads, in this language, as a person visibly undecorated by it.
The source does not say whether the language has these metaphors. They are
the draft's strongest invention and the easiest to overrule.
### Root families [I]
House method: faint family resemblance, never a nametag. No roots are
proposed here. The first root set should come from the field concepts
(combat, speech, hosting, trade, art) and the constraint/finish axis, and
should be minted against corpus items, not authored in a vacuum.
### Contamination direction [E]
What leaks into human English first (`language-design.md`) is an open
question. [I] Candidates are evaluation-layer particles (the empire's form
verdicts become meme tags) and the claim/finish vocabulary, because the
story's kids absorb *prestige codes* first. The subtitle renderer is a
separate target profile and not this spec's job.
## 5. Evidence and Invention, Summarized
**Evidence-backed (E):**
- reptilian, aesthetic-honor, decoration-as-status speakers; unpainted as
profound shamelessness; unpainted-as-provocation for a rare few
- mastery requires chosen constraint; overmatch and tourists are vulgar; the
lens questions
- the empire is a network; commerce carries, culture conquers; the registers
listed
- broadcast commentary and flavor-first subtitles as the runtime use
- the packet has `stance` with felt affect and restraint; the ontology maps
them (speaker-state branch)
**Invention (I):**
- the two-axis status ladder and the claim/demonstrated-constraint mismatch
- the whole phonological inventory and prosody
- the five obligatory categories and the inverted restraint reading
- the decoration-grammar and metaphor uses
- the speech situations table, notably the address-to-humans row
- the contempt-plus-affection panel pairing
**Not settled in the source and left open:**
- the language's and speakers' names; the vocal tract; whether the empire is
one language community or a family of dialects; whether non-reptilian
imperial subjects speak it and how it marks them
## 6. What The Lowerer Must Provide
Interlingua rule [E]: the packet carries neutral meaning plus source-side
communicative pressure, and target-language concept splits belong
downstream. The earlier Rust to Dust sketch in `language-design.md` puts an
`aesthetic_reading` block (form, discipline, spectacle, honor status) **in
the packet**. This draft recommends moving it: those are the *imperial
ontology's reading* of neutral facts, so they belong in the ontology
projection, and the packet carries the facts the reading is derived from.
[I] This is a change to existing content and the operator should rule on it.
Fields, in order of need. "Seed" means the standard's inventory already has
a slot. "Gap" means the standard has no field yet; per the Promotion Rule,
the gap is first an `rtd.*` extension and is promoted only after use in two
real packets.
| Need | Why | Status |
|---|---|---|
| `stance` with felt affect and `restraint` | the whole of Section 3 | seed (unmerged: `speaker-state`) |
| **speaker** axes: field, standing in field, school or lineage, decoration state, community | status and register selection | seed slot `Speaker` (unmerged); the *values* are the gap |
| **scene**: relative status of speaker, subject and audience; audience composition (imperial, tourist, human via subtitles) | register and address | seed slot `Scene` (unmerged) |
| `medium` and `setting` ids for broadcast commentary | situation selection | seed (`context`) |
| `instrument` role on predications, **for each party** | overmatch is derived from both parties' means | seed role |
| **volition of the constraint**: chosen / compelled / unknown, as a predication feature | the line between mastery and mere lacking | gap (`rtd.` extension first) |
| **ordering of means** (power or means gap) | vulgar overmatch | not a packet field: ontology-side order over concept ids, so the packet carries concepts and the ontology compares them |
| referent **decoration state** (decorated / unpainted / bare-by-claim / not applicable) | status, insult, the human-undecorated reading | gap (`rtd.` extension first) |
| referent **community** or `member_of` relation | tourist versus warrior; human versus imperial | seed relation `member_of`; community ids come from the `language-ontology` cut |
| **focal agent** presence | spectacle versus living practice | derivable: a predication with plural or no agent role; not a new field |
| `evidentiality` | witnessed versus inferred sincerity | seed feature (to confirm its option set) |
| `polarity`, tense, aspect | ordinary | seed |
| `preserve` | which nodes the commentary must keep | seed (`constraints`) |
Fields the draft deliberately does **not** request: `aesthetic_reading`,
`honor_status`, `spectacle_value` or any value judgment. The performer's
sincerity is not telemetry, and the commentator infers it. These are
outputs of the imperial ontology and must not be put into the packet by the
client, or the client is deciding the language's concepts.
## 7. Dependencies, Risks and Next Cuts
**Dependencies.** Stance and the `Speaker` and `Scene` slots exist only on
the `weksa-lowering/speaker-state` branch. The `language-ontology` cut
(community and ontology-entry documents) is not landed. This spec states
the content those cuts will hold, and does not substitute for them.
**Risks.**
- The inventory and the grammar are heavily invented on thin evidence. Mitigation:
tag everything (done) and test the five obligatory categories against the
lowerer before building roots.
- The claim-versus-constraint ladder risks making the language a *status-code
detector*. The source's hold is that honor is devotion, not rank. Test:
can the grammar say a bare-by-claim performance without calling it low?
- Draft collides with `language-design.md`'s packet-side `aesthetic_reading`.
Resolved by the recommendation in Section 6 if ruled.
**Next cuts, in order.**
1. Operator rulings on the forks below.
2. A first corpus slice: ten broadcast-commentary utterances, each decomposed
to a packet (neutral facts plus stance), with the expected surface *category*
(finish class, evaluation layer) and no words yet.
3. The selection table for Section 3's first five cases.
4. A root set from the field concepts, and a naming brief for the language.
5. An orthography and a pronunciation lowering.
**Forks for the operator (recommendations given).**
- Move `aesthetic_reading` out of the packet into the ontology. *Recommend:
yes.*
- Is a two-axis status ladder right, or should honor be flat within a field?
*Recommend: two axes, because tourists need a place.*
- Is inverted restraint (restraint as display of virtue) the intended reading?
*Recommend: yes, it follows from "chosen constraint is mastery".*
- Is the vocal tract reptilian-literal or a human-stylized invention?
*Recommend: human-pronounceable stylization with length as ornament, so
the first corpus does not wait on phonetics.*