Merge remote-tracking branch 'origin/main' into weksa-lowering/corpus-intake-lows-2
This commit is contained in:
commit
3cf499dbd4
23 changed files with 1467 additions and 17 deletions
|
|
@ -29,10 +29,19 @@ pub enum Source {
|
|||
OperatorApproved {
|
||||
note: String,
|
||||
},
|
||||
/// A public-domain work, with everything a reader needs to check the claim:
|
||||
/// the edition's licence and the jurisdiction the public-domain status is
|
||||
/// asserted in, who translated it (the translation is the text's copyright,
|
||||
/// not the original's) and when the edition appeared. A work missing any
|
||||
/// is refused at admission.
|
||||
PublicDomain {
|
||||
work: String,
|
||||
author: String,
|
||||
edition_url: String,
|
||||
licence: String,
|
||||
jurisdiction: String,
|
||||
translator: String,
|
||||
edition_year: u16,
|
||||
},
|
||||
/// Labelled model output; never in the test split, never judged by its own family.
|
||||
/// `family` is `Unrecorded` when nothing records one. `generator` is the
|
||||
|
|
|
|||
326
crates/weksa/src/daodejing.rs
Normal file
326
crates/weksa/src/daodejing.rs
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
//! The Dao De Jing test bed: inventory revision 3 of the `data/daodejing`
|
||||
//! stores and the packets for the slice of James Legge's chapters in its corpus.
|
||||
//! It follows `proof_packet`'s precedent: a hand-authored packet the stores
|
||||
//! admit through the same doors as any other.
|
||||
//!
|
||||
//! The ids carry the leak labels the golf loop needs. A frame minted while
|
||||
//! reading a Train chapter is generic evidence; one minted for a Validation or
|
||||
//! Test chapter was written with that chapter's reference in view, so a score
|
||||
//! on it measures the lowerer table's reach and not the interlingua's.
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use crate::corpus::CorpusItemId;
|
||||
use crate::inventory::{Change, Family, InvId, InventoryEntry, InventoryRevision};
|
||||
use crate::packet::{
|
||||
Constraints, Context, Discourse, EncoderId, FeatValue, FeatureValue, Grade, NodeId, Packet,
|
||||
PacketId, PacketKind, Predication, Provenance, ProvenanceSource, Referent, RoleFill,
|
||||
SCHEMA_VERSION, Stance, Trace,
|
||||
};
|
||||
|
||||
const CONCEPTS: &[&str] = &[
|
||||
"sage",
|
||||
"mind",
|
||||
"belly",
|
||||
"will",
|
||||
"bone",
|
||||
"empty",
|
||||
"fill",
|
||||
"weaken",
|
||||
"strengthen",
|
||||
"heaven",
|
||||
"earth",
|
||||
"is_long_enduring",
|
||||
"continues_long",
|
||||
"highest_excellence",
|
||||
"water",
|
||||
"true_words",
|
||||
"seem_paradoxical",
|
||||
"space_between_heaven_and_earth",
|
||||
"bellows",
|
||||
"compare",
|
||||
"resemble",
|
||||
];
|
||||
|
||||
/// Frame glosses, from `frame:4`. 4 is minted from a Train chapter, 5 and 6
|
||||
/// from Validation chapters, 7 and 8 from Test chapters.
|
||||
const FRAMES: &[&str] = &[
|
||||
"act",
|
||||
"intransitive",
|
||||
"likeness",
|
||||
"seems_paradoxical",
|
||||
"may_be_compared",
|
||||
];
|
||||
|
||||
/// Revision 3 of the Dao De Jing stores, after the seed and the proof revision.
|
||||
pub fn revision() -> (InventoryRevision, Vec<InventoryEntry>) {
|
||||
let mut entries = Vec::new();
|
||||
for (family, glosses) in [(Family::Concept, CONCEPTS), (Family::Frame, FRAMES)] {
|
||||
let first = if family == Family::Concept { 7 } else { 4 };
|
||||
for (i, gloss) in glosses.iter().enumerate() {
|
||||
entries.push(InventoryEntry {
|
||||
id: family.id(NonZeroU32::new(first + i as u32).expect("nonzero")),
|
||||
gloss: (*gloss).to_string(),
|
||||
value_kind: None,
|
||||
applies_to: vec![],
|
||||
introduced_rev: 3,
|
||||
retired_rev: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
let changes = entries.iter().map(|e| Change::Add(e.id)).collect();
|
||||
(
|
||||
InventoryRevision {
|
||||
n: 3,
|
||||
parent: Some(2),
|
||||
changes,
|
||||
frozen: false,
|
||||
},
|
||||
entries,
|
||||
)
|
||||
}
|
||||
|
||||
fn concept(gloss: &str) -> &'static str {
|
||||
let at = CONCEPTS
|
||||
.iter()
|
||||
.position(|c| *c == gloss)
|
||||
.expect("known concept");
|
||||
Box::leak(format!("concept:{}", 7 + at).into_boxed_str())
|
||||
}
|
||||
|
||||
fn frame(gloss: &str) -> &'static str {
|
||||
let at = FRAMES
|
||||
.iter()
|
||||
.position(|c| *c == gloss)
|
||||
.expect("known frame");
|
||||
Box::leak(format!("frame:{}", 4 + at).into_boxed_str())
|
||||
}
|
||||
|
||||
fn hex8(ascii: &str) -> String {
|
||||
assert_eq!(ascii.len(), 8);
|
||||
ascii.bytes().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
struct Spec {
|
||||
/// Eight ASCII characters; the packet id is their hex.
|
||||
id: &'static str,
|
||||
item: &'static str,
|
||||
speech_act: &'static str,
|
||||
/// `(node, referent kind id, concept gloss)`.
|
||||
referents: &'static [(&'static str, &'static str, &'static str)],
|
||||
/// `(node, concept gloss, frame gloss, [(role id, filler node)])`.
|
||||
predications: &'static [(
|
||||
&'static str,
|
||||
&'static str,
|
||||
&'static str,
|
||||
&'static [(&'static str, &'static str)],
|
||||
)],
|
||||
/// The author's reading of the speaker's felt state: `(feature gloss, permille)`.
|
||||
stance: &'static [(&'static str, u16)],
|
||||
note: &'static str,
|
||||
}
|
||||
|
||||
const AGENT: &str = "role:1";
|
||||
const PATIENT: &str = "role:2";
|
||||
const EXPERIENCER: &str = "role:3";
|
||||
const GOAL: &str = "role:6";
|
||||
const PERSON: &str = "referent_kind:1";
|
||||
const ORGANISM: &str = "referent_kind:3";
|
||||
const PLACE: &str = "referent_kind:4";
|
||||
const ABSTRACT: &str = "referent_kind:11";
|
||||
|
||||
/// Feature ids of the seed: restraint is the tenth stance feature.
|
||||
fn stance_feature(gloss: &str) -> InvId {
|
||||
let n = [
|
||||
"anger",
|
||||
"fear",
|
||||
"sadness",
|
||||
"joy",
|
||||
"disgust",
|
||||
"surprise",
|
||||
"contempt",
|
||||
"affection",
|
||||
"shame",
|
||||
"restraint",
|
||||
]
|
||||
.iter()
|
||||
.position(|g| *g == gloss)
|
||||
.expect("stance feature");
|
||||
// Features before the stance block: polarity, tense, aspect, modality,
|
||||
// evidentiality, hedge, certainty, irony.
|
||||
InvId::parse(&format!("feature:{}", 9 + n)).expect("feature id")
|
||||
}
|
||||
|
||||
const SPECS: &[Spec] = &[
|
||||
// Chapter 3 section 2. Train.
|
||||
Spec {
|
||||
id: "dao-0302",
|
||||
item: "item:daodejing:1",
|
||||
speech_act: "speech_act:1",
|
||||
referents: &[
|
||||
("n1", PERSON, "sage"),
|
||||
("n2", ABSTRACT, "mind"),
|
||||
("n3", ORGANISM, "belly"),
|
||||
("n4", ABSTRACT, "will"),
|
||||
("n5", ORGANISM, "bone"),
|
||||
],
|
||||
predications: &[
|
||||
("n6", "empty", "act", &[(AGENT, "n1"), (PATIENT, "n2")]),
|
||||
("n7", "fill", "act", &[(AGENT, "n1"), (PATIENT, "n3")]),
|
||||
("n8", "weaken", "act", &[(AGENT, "n1"), (PATIENT, "n4")]),
|
||||
("n9", "strengthen", "act", &[(AGENT, "n1"), (PATIENT, "n5")]),
|
||||
],
|
||||
stance: &[("restraint", 800)],
|
||||
note: "Four acts of one agent; 'in the exercise of his government' and 'their' are not carried.",
|
||||
},
|
||||
// Chapter 7 section 1, first sentence. Validation.
|
||||
Spec {
|
||||
id: "dao-0701",
|
||||
item: "item:daodejing:2",
|
||||
speech_act: "speech_act:1",
|
||||
referents: &[("n1", PLACE, "heaven"), ("n2", PLACE, "earth")],
|
||||
predications: &[
|
||||
(
|
||||
"n3",
|
||||
"is_long_enduring",
|
||||
"intransitive",
|
||||
&[(EXPERIENCER, "n1")],
|
||||
),
|
||||
(
|
||||
"n4",
|
||||
"continues_long",
|
||||
"intransitive",
|
||||
&[(EXPERIENCER, "n2")],
|
||||
),
|
||||
],
|
||||
stance: &[("restraint", 800)],
|
||||
note: "Heaven and earth as places; whether they are entities is a ruling the ontology owes.",
|
||||
},
|
||||
// Chapter 8 section 1, first sentence. Validation.
|
||||
Spec {
|
||||
id: "dao-0801",
|
||||
item: "item:daodejing:3",
|
||||
speech_act: "speech_act:1",
|
||||
referents: &[
|
||||
("n1", ABSTRACT, "highest_excellence"),
|
||||
("n2", ABSTRACT, "water"),
|
||||
],
|
||||
predications: &[(
|
||||
"n3",
|
||||
"resemble",
|
||||
"likeness",
|
||||
&[(EXPERIENCER, "n1"), (GOAL, "n2")],
|
||||
)],
|
||||
stance: &[("restraint", 800), ("affection", 300)],
|
||||
note: "The metaphor is a plain likeness predication; the tenor and vehicle are not marked.",
|
||||
},
|
||||
// Chapter 78 section 4. Test.
|
||||
Spec {
|
||||
id: "dao-7804",
|
||||
item: "item:daodejing:4",
|
||||
speech_act: "speech_act:1",
|
||||
referents: &[("n1", ABSTRACT, "true_words")],
|
||||
predications: &[(
|
||||
"n2",
|
||||
"seem_paradoxical",
|
||||
"seems_paradoxical",
|
||||
&[(EXPERIENCER, "n1")],
|
||||
)],
|
||||
stance: &[("restraint", 800)],
|
||||
note: "The whole paradox is in the referent's concept and the frame; the packet holds no more than the surface does.",
|
||||
},
|
||||
// Chapter 5 section 2, first sentence. Test.
|
||||
Spec {
|
||||
id: "dao-0502",
|
||||
item: "item:daodejing:5",
|
||||
speech_act: "speech_act:2",
|
||||
referents: &[
|
||||
("n1", PLACE, "space_between_heaven_and_earth"),
|
||||
("n2", ARTIFACT, "bellows"),
|
||||
],
|
||||
predications: &[(
|
||||
"n3",
|
||||
"compare",
|
||||
"may_be_compared",
|
||||
&[(EXPERIENCER, "n1"), (GOAL, "n2")],
|
||||
)],
|
||||
stance: &[("restraint", 800)],
|
||||
note: "A rhetorical question; the speech act is ask and the negated 'may not' is dropped.",
|
||||
},
|
||||
];
|
||||
|
||||
const ARTIFACT: &str = "referent_kind:5";
|
||||
|
||||
/// The packets of the slice, each minted for its corpus item.
|
||||
pub fn packets() -> Vec<Packet> {
|
||||
SPECS.iter().map(build).collect()
|
||||
}
|
||||
|
||||
fn build(spec: &Spec) -> Packet {
|
||||
let id = |s: &str| InvId::parse(s).expect("ids are well formed");
|
||||
let node = |s: &str| NodeId::parse(s).expect("nodes are well formed");
|
||||
Packet {
|
||||
id: PacketId::parse(&format!("packet:{}", hex8(spec.id))).expect("packet id"),
|
||||
schema_version: SCHEMA_VERSION,
|
||||
inventory_rev: 3,
|
||||
kind: PacketKind::CorpusExample,
|
||||
provenance: Provenance {
|
||||
source: ProvenanceSource::Authored,
|
||||
corpus_item: CorpusItemId::parse(spec.item).expect("item key"),
|
||||
encoder: EncoderId::parse(&format!("encoder:{}", hex8("claude-s"))).expect("encoder"),
|
||||
confidence: Grade::new(600).expect("grade"),
|
||||
},
|
||||
context: Context::default(),
|
||||
discourse: Discourse {
|
||||
speech_act: Some(id(spec.speech_act)),
|
||||
addressee: None,
|
||||
addressee_consent: None,
|
||||
features: vec![],
|
||||
},
|
||||
referents: spec
|
||||
.referents
|
||||
.iter()
|
||||
.map(|(n, kind, gloss)| Referent {
|
||||
id: node(n),
|
||||
kind: id(kind),
|
||||
concept: id(concept(gloss)),
|
||||
entity: None,
|
||||
features: vec![],
|
||||
})
|
||||
.collect(),
|
||||
predications: spec
|
||||
.predications
|
||||
.iter()
|
||||
.map(|(n, c, f, roles)| Predication {
|
||||
id: node(n),
|
||||
concept: id(concept(c)),
|
||||
frame: id(frame(f)),
|
||||
roles: roles
|
||||
.iter()
|
||||
.map(|(role, filler)| RoleFill {
|
||||
role: id(role),
|
||||
filler: node(filler),
|
||||
})
|
||||
.collect(),
|
||||
features: vec![],
|
||||
})
|
||||
.collect(),
|
||||
relations: vec![],
|
||||
constraints: Constraints::default(),
|
||||
extensions: vec![],
|
||||
trace: Trace {
|
||||
notes: vec![spec.note.to_string()],
|
||||
rejected_readings: vec![],
|
||||
},
|
||||
stance: Stance {
|
||||
features: spec
|
||||
.stance
|
||||
.iter()
|
||||
.map(|(g, v)| FeatureValue {
|
||||
feature: stance_feature(g),
|
||||
value: FeatValue::Grade(Grade::new(*v).expect("grade")),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -485,9 +485,20 @@ pub fn proof_revision() -> (InventoryRevision, Vec<InventoryEntry>) {
|
|||
for (family, glosses) in [
|
||||
(
|
||||
Family::Concept,
|
||||
["endanger", "prevent", "player", "control", "ship_mind"].as_slice(),
|
||||
[
|
||||
"endanger",
|
||||
"prevent",
|
||||
"player",
|
||||
"control",
|
||||
"ship_mind",
|
||||
"request",
|
||||
]
|
||||
.as_slice(),
|
||||
),
|
||||
(
|
||||
Family::Frame,
|
||||
["hazard_warning", "intervention", "request_intervention"].as_slice(),
|
||||
),
|
||||
(Family::Frame, ["hazard_warning", "intervention"].as_slice()),
|
||||
] {
|
||||
for (i, gloss) in glosses.iter().enumerate() {
|
||||
entries.push(InventoryEntry {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
//! (chrF, meaning cosine, leak diagnostics).
|
||||
|
||||
pub mod corpus;
|
||||
pub mod daodejing;
|
||||
pub mod entity;
|
||||
pub mod form;
|
||||
pub mod inventory;
|
||||
pub mod lower;
|
||||
pub mod packet;
|
||||
pub mod phones;
|
||||
pub mod schema;
|
||||
pub mod score;
|
||||
pub mod split;
|
||||
|
|
@ -22,7 +24,8 @@ pub use inventory::{
|
|||
Change, Family, InvId, Inventory, InventoryEntry, InventoryRevision, OptionLabel, OptionSet,
|
||||
Slot, ValueKind,
|
||||
};
|
||||
pub use lower::{EntityForms, LowerError, TARGET_LANGUAGE, lower, pronounce};
|
||||
pub use lower::{EntityForms, LowerError, PhoneLine, TARGET_LANGUAGE, lower, phones, pronounce};
|
||||
pub use phones::{Phone, PhoneInventory, SegmentError, Stress};
|
||||
pub use packet::{
|
||||
EncoderId, EntityId, Grade, NodeId, Packet, PacketId, PacketKind, SCHEMA_VERSION, Stance,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,15 +17,26 @@
|
|||
//! the target language, is refused: the concept word would say a different
|
||||
//! thing than the name the packet cites.
|
||||
//!
|
||||
//! A request clause ("You asked Nibu to stop.") says what the addressee did, so
|
||||
//! it is gated on the packet's own record of it: `Discourse.addressee_consent`
|
||||
//! (set only by authoring or game telemetry, V15) must name that predication,
|
||||
//! and its agent must be the addressee. Without it the lowerer refuses
|
||||
//! ([`LowerError::ConsentMissing`], [`LowerError::AskerNotAddressee`]); it does
|
||||
//! not read wording or guess who asked. A consent naming any other predication
|
||||
//! is an unrendered slot.
|
||||
//!
|
||||
//! The table is a temporary owner. Each row speaks for an inventory entry and
|
||||
//! names the gloss it speaks for, so a row cannot outlive a change of what its id
|
||||
//! means. `language-ontology` moves the rows into ontology documents.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::entity::Entity;
|
||||
use crate::inventory::{Family, InvId, Inventory};
|
||||
use crate::packet::{EntityId, NodeId, Predication};
|
||||
use crate::phones::{Phone, PhoneInventory, SegmentError};
|
||||
use crate::store::EntityStore;
|
||||
use crate::validate::LoweringInput;
|
||||
|
||||
|
|
@ -54,6 +65,33 @@ const CONCEPTS: &[Row] = &[
|
|||
("concept:3", "player", "player"),
|
||||
("concept:4", "control", "control"),
|
||||
("concept:5", "ship_mind", "ship mind"),
|
||||
// The Dao De Jing test bed (`daodejing.rs`). Nouns, then the verbs a
|
||||
// `{verb}` frame speaks.
|
||||
("concept:7", "sage", "sage"),
|
||||
("concept:8", "mind", "minds"),
|
||||
("concept:9", "belly", "bellies"),
|
||||
("concept:10", "will", "wills"),
|
||||
("concept:11", "bone", "bones"),
|
||||
("concept:12", "empty", "empties"),
|
||||
("concept:13", "fill", "fills"),
|
||||
("concept:14", "weaken", "weakens"),
|
||||
("concept:15", "strengthen", "strengthens"),
|
||||
("concept:16", "heaven", "heaven"),
|
||||
("concept:17", "earth", "earth"),
|
||||
("concept:18", "is_long_enduring", "is long-enduring"),
|
||||
("concept:19", "continues_long", "continues long"),
|
||||
("concept:20", "highest_excellence", "highest excellence"),
|
||||
("concept:21", "water", "water"),
|
||||
("concept:22", "true_words", "words that are strictly true"),
|
||||
("concept:23", "seem_paradoxical", "seem paradoxical"),
|
||||
(
|
||||
"concept:24",
|
||||
"space_between_heaven_and_earth",
|
||||
"space between heaven and earth",
|
||||
),
|
||||
("concept:25", "bellows", "bellows"),
|
||||
("concept:26", "compare", "compare"),
|
||||
("concept:27", "resemble", "resemble"),
|
||||
];
|
||||
|
||||
/// Frames: the clause a predication of that frame says. `{gloss}` is the filler
|
||||
|
|
@ -65,11 +103,36 @@ const FRAMES: &[Row] = &[
|
|||
"{agent} may endanger {patient} by touching {instrument}",
|
||||
),
|
||||
("frame:2", "intervention", "{agent} will stop {patient}"),
|
||||
(
|
||||
"frame:3",
|
||||
"request_intervention",
|
||||
"{agent} asked {patient} to stop",
|
||||
),
|
||||
// The Dao De Jing test bed. Frame 4 was written from a Train chapter, 5 and
|
||||
// 6 from Validation chapters, 7 and 8 from Test chapters.
|
||||
("frame:4", "act", "{agent} {verb} {patient}"),
|
||||
("frame:5", "intransitive", "{experiencer} {verb}"),
|
||||
("frame:6", "likeness", "{experiencer} is like {goal}"),
|
||||
(
|
||||
"frame:7",
|
||||
"seems_paradoxical",
|
||||
"{experiencer} seem paradoxical",
|
||||
),
|
||||
(
|
||||
"frame:8",
|
||||
"may_be_compared",
|
||||
"{experiencer} may be compared to {goal}",
|
||||
),
|
||||
];
|
||||
|
||||
/// The frame of "{agent} asked {patient} to stop": the one frame that reads
|
||||
/// the discourse's addressee consent.
|
||||
const REQUEST_FRAME: &str = "frame:3";
|
||||
|
||||
/// Speech acts: the opener of the first sentence.
|
||||
const SPEECH_ACTS: &[Row] = &[
|
||||
("speech_act:1", "assert", ""),
|
||||
("speech_act:2", "ask", ""),
|
||||
("speech_act:5", "warn", "Warning: "),
|
||||
];
|
||||
|
||||
|
|
@ -95,6 +158,19 @@ pub enum LowerError {
|
|||
NoPronunciation { entity: EntityId },
|
||||
/// IPA was requested for a packet that cites no entity.
|
||||
NoEntityReferent,
|
||||
/// Phones were requested for an entity whose ruled IPA holds a symbol the
|
||||
/// language's phone inventory does not list (or the language has no
|
||||
/// inventory). The lowerer does not guess a phone.
|
||||
UnknownPhone { entity: EntityId, at: usize, text: String },
|
||||
/// Phones were requested for an entity whose ruled IPA has a stress mark
|
||||
/// with no vowel after it.
|
||||
DanglingStress { entity: EntityId, at: usize },
|
||||
/// A request predication the packet does not carry the addressee's consent
|
||||
/// to: no consent, or a consent that names another predication. The
|
||||
/// lowerer does not guess who asked.
|
||||
ConsentMissing { predication: NodeId },
|
||||
/// A request predication whose agent is not the discourse addressee.
|
||||
AskerNotAddressee { predication: NodeId },
|
||||
/// The entity port failed to read.
|
||||
EntityRead { entity: EntityId, why: String },
|
||||
}
|
||||
|
|
@ -139,9 +215,7 @@ pub fn lower(
|
|||
{
|
||||
return unrendered("features");
|
||||
}
|
||||
if input.discourse().addressee_consent.is_some() {
|
||||
return unrendered("addressee consent");
|
||||
}
|
||||
request_gate(input, inv)?;
|
||||
if !input.relations().is_empty() {
|
||||
return unrendered("relations");
|
||||
}
|
||||
|
|
@ -188,6 +262,59 @@ pub fn lower(
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
/// The one place consent is read. A request clause ([`REQUEST_FRAME`]) says what
|
||||
/// the addressee did, so it needs the packet's own record of it: the discourse
|
||||
/// `addressee_consent`, which validation lets only authoring or game telemetry
|
||||
/// set (V15), must name that predication, and its agent must be the addressee.
|
||||
/// Nothing here reads wording. A consent naming any other predication is a slot
|
||||
/// the lowerer does not render.
|
||||
fn request_gate(input: &LoweringInput, inv: &Inventory) -> Result<(), LowerError> {
|
||||
let consent = input.discourse().addressee_consent.as_ref();
|
||||
let requests = input
|
||||
.predications()
|
||||
.iter()
|
||||
.filter(|p| p.frame.to_string() == REQUEST_FRAME);
|
||||
for p in requests {
|
||||
if consent != Some(&p.id) {
|
||||
return Err(LowerError::ConsentMissing {
|
||||
predication: p.id.clone(),
|
||||
});
|
||||
}
|
||||
let agent = inv
|
||||
.entries()
|
||||
.find(|e| {
|
||||
e.family() == Family::Role && e.gloss == "agent" && e.live_at(input.inventory_rev())
|
||||
})
|
||||
.ok_or_else(|| LowerError::UnknownRole {
|
||||
frame: p.frame,
|
||||
role: "agent".to_string(),
|
||||
})?;
|
||||
let asker = p
|
||||
.roles
|
||||
.iter()
|
||||
.find(|r| r.role == agent.id)
|
||||
.map(|r| &r.filler);
|
||||
if asker.is_none() || asker != input.discourse().addressee.as_ref() {
|
||||
return Err(LowerError::AskerNotAddressee {
|
||||
predication: p.id.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
match consent {
|
||||
Some(_)
|
||||
if !input
|
||||
.predications()
|
||||
.iter()
|
||||
.any(|p| p.frame.to_string() == REQUEST_FRAME) =>
|
||||
{
|
||||
Err(LowerError::Unrendered {
|
||||
what: "addressee consent",
|
||||
})
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The ruled IPA of every entity a packet cites, one per entity referent in
|
||||
/// the packet's referent order, as stored: bare, stress-marked, nothing
|
||||
/// derived. A rejected pronunciation is never read. An entity the port does not
|
||||
|
|
@ -197,13 +324,54 @@ pub fn pronounce(
|
|||
input: &LoweringInput,
|
||||
entities: &dyn EntityForms,
|
||||
) -> Result<Vec<String>, LowerError> {
|
||||
Ok(ruled(input, entities)?.into_iter().map(|(_, ipa)| ipa).collect())
|
||||
}
|
||||
|
||||
/// One entity's ruled pronunciation as the phones line a sound consumer reads:
|
||||
/// the language tag, the stored IPA it came from, and the segmented phones.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct PhoneLine {
|
||||
pub language: &'static str,
|
||||
pub source: String,
|
||||
pub phones: Vec<Phone>,
|
||||
}
|
||||
|
||||
/// The phones line of every entity a packet cites, in [`pronounce`] order, each
|
||||
/// segmented by [`TARGET_LANGUAGE`]'s phone inventory. A symbol the inventory
|
||||
/// does not list refuses the whole call.
|
||||
pub fn phones(
|
||||
input: &LoweringInput,
|
||||
entities: &dyn EntityForms,
|
||||
) -> Result<Vec<PhoneLine>, LowerError> {
|
||||
let inventory = PhoneInventory::for_language(TARGET_LANGUAGE);
|
||||
ruled(input, entities)?
|
||||
.into_iter()
|
||||
.map(|(entity, source)| {
|
||||
let unknown = |at, text| LowerError::UnknownPhone { entity: entity.clone(), at, text };
|
||||
let inventory = inventory.ok_or_else(|| unknown(0, source.clone()))?;
|
||||
let phones = inventory.segment(&source).map_err(|e| match e {
|
||||
SegmentError::UnknownPhone { at, text } => unknown(at, text),
|
||||
SegmentError::DanglingStress { at } => {
|
||||
LowerError::DanglingStress { entity: entity.clone(), at }
|
||||
}
|
||||
})?;
|
||||
Ok(PhoneLine { language: TARGET_LANGUAGE, source, phones })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `(entity, ruled IPA)` for each entity referent, in referent order.
|
||||
fn ruled(
|
||||
input: &LoweringInput,
|
||||
entities: &dyn EntityForms,
|
||||
) -> Result<Vec<(EntityId, String)>, LowerError> {
|
||||
let mut out = Vec::new();
|
||||
for id in input.referents().iter().filter_map(|r| r.entity.as_ref()) {
|
||||
let entity = entity(entities, id)?;
|
||||
let p = entity
|
||||
.pronunciation_in(TARGET_LANGUAGE)
|
||||
.ok_or_else(|| LowerError::NoPronunciation { entity: id.clone() })?;
|
||||
out.push(p.ipa.clone());
|
||||
out.push((id.clone(), p.ipa.clone()));
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(LowerError::NoEntityReferent);
|
||||
|
|
@ -228,6 +396,10 @@ fn clause(
|
|||
let close = rest[open..].find('}').map_or(rest.len(), |c| open + c);
|
||||
let name = &rest[open + 1..close];
|
||||
rest = &rest[(close + 1).min(rest.len())..];
|
||||
if name == "verb" {
|
||||
out.push_str(row(CONCEPTS, p.concept, inv, rev)?);
|
||||
continue;
|
||||
}
|
||||
let role = inv
|
||||
.entries()
|
||||
.find(|e| e.family() == Family::Role && e.gloss == name && e.live_at(rev))
|
||||
|
|
|
|||
|
|
@ -54,8 +54,13 @@ enum Command {
|
|||
key: String,
|
||||
/// Print the ruled IPA of each entity the packet cites, one per line,
|
||||
/// instead of the text; refused for an entity with no ruled pronunciation.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "phones")]
|
||||
ipa: bool,
|
||||
/// Print the phones line of each entity the packet cites, one JSON
|
||||
/// object per line ({language, source, phones: [{ipa, stress}]}),
|
||||
/// instead of the text; refused for a phone the inventory lacks.
|
||||
#[arg(long)]
|
||||
phones: bool,
|
||||
},
|
||||
/// Score output text (stdin, or --output) against corpus items, one embed call.
|
||||
Score(ScoreArgs),
|
||||
|
|
@ -72,6 +77,8 @@ enum Command {
|
|||
enum PacketCommand {
|
||||
/// Admit the nibu-proof-001 packet through `PacketStore::admit`.
|
||||
Proof { store: PathBuf },
|
||||
/// Admit the Dao De Jing slice's packets through `PacketStore::admit`.
|
||||
Dao { store: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -106,6 +113,8 @@ enum InventoryCommand {
|
|||
Seed,
|
||||
/// Admit revision 2, the concepts and frames the nibu-proof-001 packet names.
|
||||
Proof,
|
||||
/// Admit revision 3 of the Dao De Jing test bed stores (after seed and proof).
|
||||
Dao,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
|
|
@ -282,6 +291,16 @@ struct AddArgs {
|
|||
author: Option<String>,
|
||||
#[arg(long)]
|
||||
edition_url: Option<String>,
|
||||
/// The edition's licence, for `public-domain`.
|
||||
#[arg(long)]
|
||||
licence: Option<String>,
|
||||
/// Where the public-domain status is asserted, for `public-domain`.
|
||||
#[arg(long)]
|
||||
jurisdiction: Option<String>,
|
||||
#[arg(long)]
|
||||
translator: Option<String>,
|
||||
#[arg(long)]
|
||||
edition_year: Option<u16>,
|
||||
#[arg(long = "model-family", value_enum)]
|
||||
family: Option<ModelFamilyArg>,
|
||||
/// The generator string the fixture or run wrote, verbatim.
|
||||
|
|
@ -341,7 +360,15 @@ impl AddArgs {
|
|||
SourceKind::CastAuthored => &["persona", "fixture", "author"],
|
||||
SourceKind::OperatorWritten => &[],
|
||||
SourceKind::OperatorApproved => &["note"],
|
||||
SourceKind::PublicDomain => &["work", "author", "edition-url"],
|
||||
SourceKind::PublicDomain => &[
|
||||
"work",
|
||||
"author",
|
||||
"edition-url",
|
||||
"licence",
|
||||
"jurisdiction",
|
||||
"translator",
|
||||
"edition-year",
|
||||
],
|
||||
SourceKind::ModelWritten => &["model-family", "generator", "prompt-digest"],
|
||||
};
|
||||
let given = [
|
||||
|
|
@ -351,6 +378,10 @@ impl AddArgs {
|
|||
("note", self.note.is_some()),
|
||||
("work", self.work.is_some()),
|
||||
("edition-url", self.edition_url.is_some()),
|
||||
("licence", self.licence.is_some()),
|
||||
("jurisdiction", self.jurisdiction.is_some()),
|
||||
("translator", self.translator.is_some()),
|
||||
("edition-year", self.edition_year.is_some()),
|
||||
("model-family", self.family.is_some()),
|
||||
("generator", self.generator.is_some()),
|
||||
("prompt-digest", self.prompt_digest.is_some()),
|
||||
|
|
@ -377,6 +408,12 @@ impl AddArgs {
|
|||
work: get(&self.work, "work")?,
|
||||
author: get(&self.author, "author")?,
|
||||
edition_url: get(&self.edition_url, "edition-url")?,
|
||||
licence: get(&self.licence, "licence")?,
|
||||
jurisdiction: get(&self.jurisdiction, "jurisdiction")?,
|
||||
translator: get(&self.translator, "translator")?,
|
||||
edition_year: self
|
||||
.edition_year
|
||||
.context("this source needs --edition-year")?,
|
||||
},
|
||||
SourceKind::ModelWritten => Source::ModelWritten {
|
||||
family: self
|
||||
|
|
@ -544,6 +581,27 @@ fn main() -> Result<()> {
|
|||
.map_err(refused)?;
|
||||
println!("admitted rev:2 with {count} entries");
|
||||
}
|
||||
Command::Inventory(InventoryCommand::Dao) => {
|
||||
let (rev, entries) = weksa::daodejing::revision();
|
||||
let count = entries.len();
|
||||
InventoryStore::open(&cli.inventory)?
|
||||
.admit_revision(rev, entries)
|
||||
.map_err(refused)?;
|
||||
println!("admitted rev:3 with {count} entries");
|
||||
}
|
||||
Command::Packet(PacketCommand::Dao { store }) => {
|
||||
let mut packets = PacketStore::open(&store)?;
|
||||
let inventory = InventoryStore::open(&cli.inventory)?;
|
||||
let corpus = CorpusStore::open(&cli.corpus)?;
|
||||
let entities = EntityStore::open(&cli.entities)?;
|
||||
for packet in weksa::daodejing::packets() {
|
||||
let key = packet.id.to_string();
|
||||
packets
|
||||
.admit(packet, &inventory, &corpus, &entities)
|
||||
.map_err(refused)?;
|
||||
println!("admitted {key}");
|
||||
}
|
||||
}
|
||||
Command::Packet(PacketCommand::Proof { store }) => {
|
||||
let packet = proof_packet();
|
||||
let key = packet.id.to_string();
|
||||
|
|
@ -557,7 +615,7 @@ fn main() -> Result<()> {
|
|||
.map_err(refused)?;
|
||||
println!("admitted {key}");
|
||||
}
|
||||
Command::Lower { store, key, ipa } => {
|
||||
Command::Lower { store, key, ipa, phones } => {
|
||||
let inventory_store = InventoryStore::open(&cli.inventory)?;
|
||||
let inventory = inventory_store.inventory()?;
|
||||
let mut packets = PacketStore::open(&store)?;
|
||||
|
|
@ -577,6 +635,10 @@ fn main() -> Result<()> {
|
|||
for line in pronounce(&view, &entities)? {
|
||||
println!("{line}");
|
||||
}
|
||||
} else if phones {
|
||||
for line in weksa::phones(&view, &entities)? {
|
||||
println!("{}", serde_json::to_string(&line)?);
|
||||
}
|
||||
} else {
|
||||
println!("{}", lower(&view, &inventory, &entities)?);
|
||||
}
|
||||
|
|
|
|||
152
crates/weksa/src/phones.rs
Normal file
152
crates/weksa/src/phones.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! The phone inventory and the segmenter: a ruled IPA string to an ordered list of
|
||||
//! phones with stress, for the consumer that turns phones into sound. The
|
||||
//! inventory is data (`data/phonology/<language>.phones.tsv`); this module reads
|
||||
//! it and owns one rule, that every symbol of the string is a phone the inventory
|
||||
//! lists. A symbol it does not list is refused: no nearest phone, no spelling
|
||||
//! rule, no guess. Tense/lax is a column of the inventory, so /i/ and /ɪ/ are two
|
||||
//! phones here, not one phone and a flag on an entity.
|
||||
//!
|
||||
//! Stress marks are not phones. `ˈ` (primary) and `ˌ` (secondary) put their
|
||||
//! stress on the next vowel phone, the syllable's nucleus; every other phone is
|
||||
//! `none`. A mark with no vowel after it is refused. Durations, pitch and
|
||||
//! acoustics are the consumer's, never read or written here.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
const EN_US: &str = include_str!("../../../data/phonology/en-US.phones.tsv");
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Stress {
|
||||
None,
|
||||
Primary,
|
||||
Secondary,
|
||||
}
|
||||
|
||||
/// One phone of a segmented string: its IPA symbol as the inventory spells it
|
||||
/// and its stress.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct Phone {
|
||||
pub ipa: String,
|
||||
pub stress: Stress,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Tenseness {
|
||||
Tense,
|
||||
Lax,
|
||||
Reduced,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Class {
|
||||
Consonant,
|
||||
Vowel(Tenseness),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SegmentError {
|
||||
/// The symbol (or the text from it on) matches no inventory phone.
|
||||
UnknownPhone { at: usize, text: String },
|
||||
/// A stress mark (`at` is its byte offset) that finds no vowel before the
|
||||
/// string ends or another mark comes.
|
||||
DanglingStress { at: usize },
|
||||
}
|
||||
|
||||
pub struct PhoneInventory {
|
||||
phones: BTreeMap<String, Class>,
|
||||
longest: usize,
|
||||
}
|
||||
|
||||
impl PhoneInventory {
|
||||
/// The inventory for a language tag, if one is committed.
|
||||
pub fn for_language(language: &str) -> Option<&'static PhoneInventory> {
|
||||
static EN_US_INV: OnceLock<PhoneInventory> = OnceLock::new();
|
||||
match language {
|
||||
"en-US" => Some(EN_US_INV.get_or_init(|| {
|
||||
PhoneInventory::parse(EN_US).expect("the committed en-US phone inventory parses")
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the tab-separated inventory. Malformed lines are errors, as are
|
||||
/// repeated symbols.
|
||||
pub fn parse(text: &str) -> Result<Self, String> {
|
||||
let mut phones = BTreeMap::new();
|
||||
for (n, line) in text.lines().enumerate() {
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let cols: Vec<&str> = line.split('\t').collect();
|
||||
let [ipa, class, tenseness, gloss] = cols[..] else {
|
||||
return Err(format!("line {}: want 4 tab-separated columns", n + 1));
|
||||
};
|
||||
let class = match (class, tenseness) {
|
||||
("consonant", "-") => Class::Consonant,
|
||||
("vowel", "tense") => Class::Vowel(Tenseness::Tense),
|
||||
("vowel", "lax") => Class::Vowel(Tenseness::Lax),
|
||||
("vowel", "reduced") => Class::Vowel(Tenseness::Reduced),
|
||||
_ => return Err(format!("line {}: bad class/tenseness {class}/{tenseness}", n + 1)),
|
||||
};
|
||||
if ipa.is_empty() || gloss.is_empty() || ipa.chars().any(|c| matches!(c, 'ˈ' | 'ˌ')) {
|
||||
return Err(format!("line {}: empty or stress-marked entry", n + 1));
|
||||
}
|
||||
if phones.insert(ipa.to_string(), class).is_some() {
|
||||
return Err(format!("line {}: {ipa} repeats", n + 1));
|
||||
}
|
||||
}
|
||||
let longest = phones.keys().map(|k| k.chars().count()).max().unwrap_or(0);
|
||||
Ok(Self { phones, longest })
|
||||
}
|
||||
|
||||
pub fn class(&self, ipa: &str) -> Option<&Class> {
|
||||
self.phones.get(ipa)
|
||||
}
|
||||
|
||||
/// Segment `ipa` into phones: longest inventory match at each position.
|
||||
pub fn segment(&self, ipa: &str) -> Result<Vec<Phone>, SegmentError> {
|
||||
let chars: Vec<(usize, char)> = ipa.char_indices().collect();
|
||||
let mut out: Vec<Phone> = Vec::new();
|
||||
let mut pending: Option<(Stress, usize)> = None;
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
let at = chars[i].0;
|
||||
let mark = match chars[i].1 {
|
||||
'ˈ' => Some(Stress::Primary),
|
||||
'ˌ' => Some(Stress::Secondary),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(m) = mark {
|
||||
if let Some((_, was)) = pending {
|
||||
return Err(SegmentError::DanglingStress { at: was });
|
||||
}
|
||||
pending = Some((m, at));
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let hit = (1..=self.longest.min(chars.len() - i)).rev().find_map(|len| {
|
||||
let end = chars.get(i + len).map_or(ipa.len(), |c| c.0);
|
||||
let s = &ipa[at..end];
|
||||
self.phones.get(s).map(|c| (len, s, c))
|
||||
});
|
||||
let Some((len, s, class)) = hit else {
|
||||
return Err(SegmentError::UnknownPhone { at, text: ipa[at..].to_string() });
|
||||
};
|
||||
let stress = if matches!(class, Class::Vowel(_)) {
|
||||
pending.take().map_or(Stress::None, |(m, _)| m)
|
||||
} else {
|
||||
Stress::None
|
||||
};
|
||||
out.push(Phone { ipa: s.to_string(), stress });
|
||||
i += len;
|
||||
}
|
||||
if let Some((_, at)) = pending {
|
||||
return Err(SegmentError::DanglingStress { at });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
|
@ -573,6 +573,18 @@ fn empty_attestation(source: &Source) -> Option<&'static str> {
|
|||
Source::PublicDomain { edition_url, .. } if empty(edition_url) => {
|
||||
Some("a public-domain source names its edition url")
|
||||
}
|
||||
Source::PublicDomain { licence, .. } if empty(licence) => {
|
||||
Some("a public-domain source names its licence")
|
||||
}
|
||||
Source::PublicDomain { jurisdiction, .. } if empty(jurisdiction) => {
|
||||
Some("a public-domain source names its jurisdiction")
|
||||
}
|
||||
Source::PublicDomain { translator, .. } if empty(translator) => {
|
||||
Some("a public-domain source names its translator")
|
||||
}
|
||||
Source::PublicDomain {
|
||||
edition_year: 0, ..
|
||||
} => Some("a public-domain source names its edition year"),
|
||||
Source::ModelWritten { generator, .. } if empty_some(generator) => {
|
||||
Some("a model-written source names a non-empty generator")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,6 +210,14 @@ fn model_written_and_public_domain_sources_name_their_provenance() {
|
|||
"a",
|
||||
"--edition-url",
|
||||
"https://example.org/w",
|
||||
"--licence",
|
||||
"Public domain",
|
||||
"--jurisdiction",
|
||||
"US",
|
||||
"--translator",
|
||||
"T. Translator",
|
||||
"--edition-year",
|
||||
"1891",
|
||||
]);
|
||||
assert!(pd.status.success(), "{}", out(&pd));
|
||||
let bad_turn = run(&[
|
||||
|
|
@ -566,6 +574,14 @@ fn source_flags_must_match_the_source() {
|
|||
"a",
|
||||
"--edition-url",
|
||||
"u",
|
||||
"--licence",
|
||||
"l",
|
||||
"--jurisdiction",
|
||||
"j",
|
||||
"--translator",
|
||||
"t",
|
||||
"--edition-year",
|
||||
"1891",
|
||||
"--note",
|
||||
"n",
|
||||
],
|
||||
|
|
@ -618,6 +634,78 @@ fn source_flags_must_match_the_source() {
|
|||
&["--work", "w", "--edition-url", "u"],
|
||||
"--author",
|
||||
),
|
||||
(
|
||||
"public-domain",
|
||||
&[
|
||||
"--work",
|
||||
"w",
|
||||
"--author",
|
||||
"a",
|
||||
"--edition-url",
|
||||
"u",
|
||||
"--jurisdiction",
|
||||
"j",
|
||||
"--translator",
|
||||
"t",
|
||||
"--edition-year",
|
||||
"1891",
|
||||
],
|
||||
"--licence",
|
||||
),
|
||||
(
|
||||
"public-domain",
|
||||
&[
|
||||
"--work",
|
||||
"w",
|
||||
"--author",
|
||||
"a",
|
||||
"--edition-url",
|
||||
"u",
|
||||
"--licence",
|
||||
"l",
|
||||
"--translator",
|
||||
"t",
|
||||
"--edition-year",
|
||||
"1891",
|
||||
],
|
||||
"--jurisdiction",
|
||||
),
|
||||
(
|
||||
"public-domain",
|
||||
&[
|
||||
"--work",
|
||||
"w",
|
||||
"--author",
|
||||
"a",
|
||||
"--edition-url",
|
||||
"u",
|
||||
"--licence",
|
||||
"l",
|
||||
"--jurisdiction",
|
||||
"j",
|
||||
"--edition-year",
|
||||
"1891",
|
||||
],
|
||||
"--translator",
|
||||
),
|
||||
(
|
||||
"public-domain",
|
||||
&[
|
||||
"--work",
|
||||
"w",
|
||||
"--author",
|
||||
"a",
|
||||
"--edition-url",
|
||||
"u",
|
||||
"--licence",
|
||||
"l",
|
||||
"--jurisdiction",
|
||||
"j",
|
||||
"--translator",
|
||||
"t",
|
||||
],
|
||||
"--edition-year",
|
||||
),
|
||||
// Empty provenance strings.
|
||||
(
|
||||
"model-written",
|
||||
|
|
@ -876,6 +964,18 @@ fn proof_packet_lowers_and_the_lowering_scores_against_items() {
|
|||
let ipa = weksa(d.path(), &args);
|
||||
assert!(ipa.status.success(), "{}", out(&ipa));
|
||||
assert_eq!(String::from_utf8(ipa.stdout).unwrap(), "ˈnibu\n");
|
||||
// The phones line is the committed fixture, byte for byte, and is a different
|
||||
// output from --ipa, so the two flags cannot be combined.
|
||||
let phones = weksa(d.path(), &["lower", "./p.cc", "packet:6e6962752d303031", "--phones"]);
|
||||
assert!(phones.status.success(), "{}", out(&phones));
|
||||
let fixture = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../data/phonology/fixtures/nibu-en-US.phones.json"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(String::from_utf8(phones.stdout).unwrap(), fixture);
|
||||
let both = weksa(d.path(), &["lower", "./p.cc", "packet:6e6962752d303031", "--phones", "--ipa"]);
|
||||
assert!(!both.status.success(), "{}", out(&both));
|
||||
let again = weksa(d.path(), &["lower", "./p.cc", "packet:6e6962752d303031"]);
|
||||
assert_eq!(String::from_utf8(again.stdout).unwrap(), text);
|
||||
|
||||
|
|
|
|||
71
crates/weksa/tests/daodejing.rs
Normal file
71
crates/weksa/tests/daodejing.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
//! The Dao De Jing test bed (`data/daodejing`): its Legge items carry full
|
||||
//! public-domain provenance, and its packets are admitted and lower to the
|
||||
//! English the first golf-loop run scored (`docs/targets/dao-de-jing-test-bed.md`).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use weksa::daodejing::packets;
|
||||
use weksa::{CorpusStore, EntityStore, InventoryStore, PacketStore, Source, lower};
|
||||
|
||||
fn root() -> std::path::PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data/daodejing")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_legge_item_names_licence_jurisdiction_translator_and_edition_year() {
|
||||
let items = CorpusStore::open(&root().join("corpus.cc"))
|
||||
.unwrap()
|
||||
.all()
|
||||
.unwrap();
|
||||
assert_eq!(items.len(), 5);
|
||||
for item in items {
|
||||
let Source::PublicDomain {
|
||||
licence,
|
||||
jurisdiction,
|
||||
translator,
|
||||
edition_year,
|
||||
..
|
||||
} = item.source
|
||||
else {
|
||||
panic!("{} is not a public-domain item", item.id);
|
||||
};
|
||||
assert!(
|
||||
!licence.is_empty() && !jurisdiction.is_empty(),
|
||||
"{}",
|
||||
item.id
|
||||
);
|
||||
assert_eq!((translator.as_str(), edition_year), ("James Legge", 1891));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_slice_packets_are_admitted_and_lower_to_the_scored_english() {
|
||||
let inventory = InventoryStore::open(&root().join("inventory.cc")).unwrap();
|
||||
let corpus = CorpusStore::open(&root().join("corpus.cc")).unwrap();
|
||||
let entities = EntityStore::open(&root().join("entity.cc")).unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut store = PacketStore::open(&dir.path().join("p.cc")).unwrap();
|
||||
let inv = inventory.inventory().unwrap();
|
||||
let lowered: Vec<String> = packets()
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let view = store.admit(p, &inventory, &corpus, &entities).unwrap();
|
||||
lower(&view, &inv, &entities).unwrap()
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
lowered,
|
||||
[
|
||||
"The sage empties the minds. The sage fills the bellies. The sage weakens the wills. The sage strengthens the bones.",
|
||||
"The heaven is long-enduring. The earth continues long.",
|
||||
"The highest excellence is like the water.",
|
||||
"The words that are strictly true seem paradoxical.",
|
||||
"The space between heaven and earth may be compared to the bellows.",
|
||||
]
|
||||
);
|
||||
// The committed packet store holds exactly what the module builds.
|
||||
let stored = PacketStore::open(&root().join("packets.cc")).unwrap();
|
||||
for p in packets() {
|
||||
assert_eq!(stored.get(&p.id.to_string()).unwrap(), Some(p));
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ use weksa::inventory::proof_revision;
|
|||
use weksa::packet::{EntityId, FeatValue, FeatureValue, Packet, proof_packet};
|
||||
use weksa::{
|
||||
CorpusStore, EntityForms, EntityStore, Inventory, InventoryStore, LowerError, LoweringInput,
|
||||
PacketStore, Pronunciation, lower, pronounce, validate,
|
||||
PacketStore, PhoneInventory, PhoneLine, Pronunciation, Stress, lower, pronounce, validate,
|
||||
};
|
||||
|
||||
const PROOF: &str = "Warning: you may endanger yourself by touching the control. \
|
||||
|
|
@ -436,3 +436,269 @@ fn ipa_needs_an_entity_the_port_holds_and_a_packet_that_cites_one() {
|
|||
Err(LowerError::NoEntityReferent)
|
||||
);
|
||||
}
|
||||
|
||||
// ---- the phones line: the phone inventory and the segmenter ----
|
||||
|
||||
fn phones_of(ipa: &str) -> Result<Vec<PhoneLine>, LowerError> {
|
||||
let mut n = nibu(&[("en-US", &["Nibu"])]);
|
||||
n.forms[0].pronunciation = Some(ruled(ipa, &[]));
|
||||
let mut e = Entities::default();
|
||||
e.held.push(n);
|
||||
let (inv, _dir) = committed();
|
||||
weksa::phones(&view(&proof_packet(), &inv), &e)
|
||||
}
|
||||
|
||||
fn seg(ipa: &str) -> Vec<(String, Stress)> {
|
||||
let inv = PhoneInventory::for_language("en-US").unwrap();
|
||||
inv.segment(ipa)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|p| (p.ipa, p.stress))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pairs(v: &[(&str, Stress)]) -> Vec<(String, Stress)> {
|
||||
v.iter().map(|(i, s)| ((*i).to_string(), *s)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nibus_committed_ruling_lowers_to_the_committed_phones_fixture() {
|
||||
let (inv, _dir) = committed();
|
||||
let lines = weksa::phones(&view(&proof_packet(), &inv), &committed_entities()).unwrap();
|
||||
let fixture = std::fs::read_to_string(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../data/phonology/fixtures/nibu-en-US.phones.json"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(serde_json::to_string(&lines[0]).unwrap(), fixture.trim_end());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tense_i_is_a_phone_and_the_rejected_lax_form_is_a_different_one() {
|
||||
use weksa::phones::{Class, Tenseness};
|
||||
let inv = PhoneInventory::for_language("en-US").unwrap();
|
||||
assert_eq!(inv.class("i"), Some(&Class::Vowel(Tenseness::Tense)));
|
||||
assert_eq!(inv.class("ɪ"), Some(&Class::Vowel(Tenseness::Lax)));
|
||||
assert_eq!(inv.class("u"), Some(&Class::Vowel(Tenseness::Tense)));
|
||||
let ruled_form = seg("ˈnibu");
|
||||
let lax = seg("ˈnɪbu");
|
||||
assert_ne!(ruled_form, lax);
|
||||
assert_eq!(lax[1], ("ɪ".to_string(), Stress::Primary));
|
||||
// The committed seed rejects the lax form, and it is not what the seed emits.
|
||||
let seed = committed_entities();
|
||||
let nibu = seed.get(&EntityId::parse("entity:nibu").unwrap()).unwrap().unwrap();
|
||||
let p = nibu.pronunciation_in("en-US").unwrap();
|
||||
assert_eq!(p.rejected, ["ˈnɪbu"]);
|
||||
assert_eq!(seg(&p.ipa), ruled_form);
|
||||
assert!(p.rejected.iter().all(|r| seg(r) != seg(&p.ipa)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stress_lands_on_the_vowel_after_the_mark_and_nowhere_else() {
|
||||
use Stress::{None as N, Primary as P, Secondary as S};
|
||||
assert_eq!(seg("ˈnibu"), pairs(&[("n", N), ("i", P), ("b", N), ("u", N)]));
|
||||
assert_eq!(seg("nˈibu"), seg("ˈnibu"));
|
||||
assert_eq!(seg("bˈunˌi"), pairs(&[("b", N), ("u", P), ("n", N), ("i", S)]));
|
||||
assert_eq!(seg("nibu"), pairs(&[("n", N), ("i", N), ("b", N), ("u", N)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_character_phones_are_one_phone_by_longest_match() {
|
||||
use Stress::{None as N, Primary as P};
|
||||
assert_eq!(seg("ˈtʃeɪ"), pairs(&[("tʃ", N), ("eɪ", P)]));
|
||||
assert_eq!(seg("dʒaʊ"), pairs(&[("dʒ", N), ("aʊ", N)]));
|
||||
// /t/ then /ʃ/ is not what a lone "t" and "ʃ" mean when the affricate is listed,
|
||||
// but a symbol sequence the inventory cannot join still splits.
|
||||
assert_eq!(seg("ɔɪt"), pairs(&[("ɔɪ", N), ("t", N)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_phone_the_inventory_lacks_is_refused_with_where_and_never_guessed() {
|
||||
let id = EntityId::parse("entity:nibu").unwrap();
|
||||
for (ipa, at, text) in [
|
||||
("ˈnixu", "ˈni".len(), "xu"),
|
||||
("ˈniːbu", "ˈni".len(), "ːbu"),
|
||||
("ˈnibu.", "ˈnibu".len(), "."),
|
||||
("ˈni bu", "ˈni".len(), " bu"),
|
||||
] {
|
||||
assert_eq!(
|
||||
phones_of(ipa),
|
||||
Err(LowerError::UnknownPhone { entity: id.clone(), at, text: text.into() }),
|
||||
"{ipa}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stress_mark_with_no_vowel_to_land_on_is_refused() {
|
||||
let id = EntityId::parse("entity:nibu").unwrap();
|
||||
for (ipa, at) in [("nibuˈ", "nibu".len()), ("ˈˈnibu", 0), ("ˈnˈibu", 0), ("ˈnb", 0)] {
|
||||
assert_eq!(
|
||||
phones_of(ipa),
|
||||
Err(LowerError::DanglingStress { entity: id.clone(), at }),
|
||||
"{ipa}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phones_need_a_ruling_and_an_entity_exactly_as_ipa_does() {
|
||||
assert_eq!(
|
||||
weksa::phones(&view(&proof_packet(), &committed().0), &Entities::default()),
|
||||
Err(LowerError::UnknownEntity { entity: EntityId::parse("entity:nibu").unwrap() })
|
||||
);
|
||||
let mut e = Entities::default();
|
||||
e.held.push(nibu(&[("en-US", &["Nibu"])]));
|
||||
assert_eq!(
|
||||
weksa::phones(&view(&proof_packet(), &committed().0), &e),
|
||||
Err(LowerError::NoPronunciation { entity: EntityId::parse("entity:nibu").unwrap() })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_inventory_parser_refuses_what_it_cannot_read() {
|
||||
for bad in [
|
||||
"i\tvowel\ttense",
|
||||
"i\tvowel\t-\tx",
|
||||
"n\tconsonant\ttense\tx",
|
||||
"i\tvowel\tmid\tx",
|
||||
"ˈi\tvowel\ttense\tx",
|
||||
"\tvowel\ttense\tx",
|
||||
"i\tvowel\ttense\t",
|
||||
"i\tvowel\ttense\ta\ni\tvowel\tlax\tb",
|
||||
] {
|
||||
assert!(PhoneInventory::parse(bad).is_err(), "{bad:?}");
|
||||
}
|
||||
assert!(PhoneInventory::parse("# c\n\ni\tvowel\ttense\ta\n").is_ok());
|
||||
assert!(PhoneInventory::for_language("sa-Latn").is_none());
|
||||
}
|
||||
|
||||
/// `nibu-proof-001` re-cut as the request: the addressee asked Nibu to stop,
|
||||
/// one frame:3 predication (n4), consent naming it, and no other claim.
|
||||
fn asked() -> Packet {
|
||||
let mut p = proof_packet();
|
||||
p.referents.remove(1);
|
||||
let mut ask = p.predications[1].clone();
|
||||
ask.id = nd("n4");
|
||||
ask.concept = iv("concept:6");
|
||||
ask.frame = iv("frame:3");
|
||||
ask.roles = vec![
|
||||
weksa::packet::RoleFill {
|
||||
role: iv("role:1"),
|
||||
filler: nd("n1"),
|
||||
},
|
||||
weksa::packet::RoleFill {
|
||||
role: iv("role:2"),
|
||||
filler: nd("n3"),
|
||||
},
|
||||
];
|
||||
p.predications = vec![ask];
|
||||
p.discourse.speech_act = Some(iv("speech_act:1"));
|
||||
p.discourse.addressee_consent = Some(nd("n4"));
|
||||
p.constraints.preserve = vec![nd("n1")];
|
||||
p
|
||||
}
|
||||
|
||||
/// The request plus a frame:2 predication (n5) the consent could wrongly name.
|
||||
fn asked_and_stopped() -> Packet {
|
||||
let mut p = asked();
|
||||
let mut stop = proof_packet().predications.remove(1);
|
||||
stop.roles[1].filler = nd("n4");
|
||||
p.predications.push(stop);
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_consented_request_lowers_to_what_the_addressee_asked() {
|
||||
let (inv, _dir) = committed();
|
||||
assert_eq!(lowered(&asked(), &inv).unwrap(), "You asked Nibu to stop.");
|
||||
let mut p = asked();
|
||||
p.provenance.source = weksa::packet::ProvenanceSource::GameTelemetry;
|
||||
assert_eq!(lowered(&p, &inv).unwrap(), "You asked Nibu to stop.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_without_its_consent_is_refused_not_invented() {
|
||||
let (inv, _dir) = committed();
|
||||
let mut p = asked();
|
||||
p.discourse.addressee_consent = None;
|
||||
assert_eq!(
|
||||
lowered(&p, &inv),
|
||||
Err(LowerError::ConsentMissing {
|
||||
predication: nd("n4")
|
||||
})
|
||||
);
|
||||
// Consent that names a different predication is no consent to the request.
|
||||
let mut p = asked_and_stopped();
|
||||
p.discourse.addressee_consent = Some(nd("n5"));
|
||||
assert_eq!(
|
||||
lowered(&p, &inv),
|
||||
Err(LowerError::ConsentMissing {
|
||||
predication: nd("n4")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consent_to_one_of_two_requests_leaves_the_other_unconsented() {
|
||||
let (inv, _dir) = committed();
|
||||
let mut p = asked();
|
||||
let mut second = p.predications[0].clone();
|
||||
second.id = nd("n5");
|
||||
p.predications.push(second);
|
||||
assert_eq!(
|
||||
lowered(&p, &inv),
|
||||
Err(LowerError::ConsentMissing {
|
||||
predication: nd("n5")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_by_someone_other_than_the_addressee_is_refused() {
|
||||
let (inv, _dir) = committed();
|
||||
let mut p = asked();
|
||||
p.predications[0].roles[0].filler = nd("n3");
|
||||
p.predications[0].roles[1].filler = nd("n1");
|
||||
assert_eq!(
|
||||
lowered(&p, &inv),
|
||||
Err(LowerError::AskerNotAddressee {
|
||||
predication: nd("n4")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consent_on_a_claim_that_is_no_request_stays_unrendered() {
|
||||
let (inv, _dir) = committed();
|
||||
let mut p = proof_packet();
|
||||
p.discourse.addressee_consent = Some(nd("n5"));
|
||||
assert_eq!(
|
||||
lowered(&p, &inv),
|
||||
Err(LowerError::Unrendered {
|
||||
what: "addressee consent"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_decomposition_or_import_cannot_reach_the_lowerer_with_consent() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let inventory = InventoryStore::open(&root.join("data/interlingua/inventory.cc")).unwrap();
|
||||
let corpus = CorpusStore::open(&root.join("data/corpus/corpus.cc")).unwrap();
|
||||
let entities = EntityStore::open(&root.join("data/entity/entity.cc")).unwrap();
|
||||
for source in [
|
||||
weksa::packet::ProvenanceSource::ModelDecomposition,
|
||||
weksa::packet::ProvenanceSource::ImportedCorpus,
|
||||
] {
|
||||
let mut p = asked();
|
||||
p.provenance.source = source;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let refused = PacketStore::open(&dir.path().join("p.cc"))
|
||||
.unwrap()
|
||||
.admit(p, &inventory, &corpus, &entities)
|
||||
.unwrap_err();
|
||||
assert!(format!("{refused:?}").contains("V15"), "{source:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -522,7 +522,7 @@ fn an_attestation_holds_something_readable() {
|
|||
let mut store = CorpusStore::open(&d.path().join("c.cc")).unwrap();
|
||||
let s = |x: &str| x.to_string();
|
||||
type Build = fn(String) -> Source;
|
||||
let builders: [(Build, &'static str); 9] = [
|
||||
let builders: [(Build, &'static str); 12] = [
|
||||
(
|
||||
|v| Source::CastAuthored {
|
||||
persona: v,
|
||||
|
|
@ -556,6 +556,10 @@ fn an_attestation_holds_something_readable() {
|
|||
work: v,
|
||||
author: "a".into(),
|
||||
edition_url: "u".into(),
|
||||
licence: "l".into(),
|
||||
jurisdiction: "j".into(),
|
||||
translator: "t".into(),
|
||||
edition_year: 1891,
|
||||
},
|
||||
"a public-domain source names its work",
|
||||
),
|
||||
|
|
@ -564,6 +568,10 @@ fn an_attestation_holds_something_readable() {
|
|||
work: "w".into(),
|
||||
author: v,
|
||||
edition_url: "u".into(),
|
||||
licence: "l".into(),
|
||||
jurisdiction: "j".into(),
|
||||
translator: "t".into(),
|
||||
edition_year: 1891,
|
||||
},
|
||||
"a public-domain source names its author",
|
||||
),
|
||||
|
|
@ -572,9 +580,49 @@ fn an_attestation_holds_something_readable() {
|
|||
work: "w".into(),
|
||||
author: "a".into(),
|
||||
edition_url: v,
|
||||
licence: "l".into(),
|
||||
jurisdiction: "j".into(),
|
||||
translator: "t".into(),
|
||||
edition_year: 1891,
|
||||
},
|
||||
"a public-domain source names its edition url",
|
||||
),
|
||||
(
|
||||
|v| Source::PublicDomain {
|
||||
work: "w".into(),
|
||||
author: "a".into(),
|
||||
edition_url: "u".into(),
|
||||
licence: v,
|
||||
jurisdiction: "j".into(),
|
||||
translator: "t".into(),
|
||||
edition_year: 1891,
|
||||
},
|
||||
"a public-domain source names its licence",
|
||||
),
|
||||
(
|
||||
|v| Source::PublicDomain {
|
||||
work: "w".into(),
|
||||
author: "a".into(),
|
||||
edition_url: "u".into(),
|
||||
licence: "l".into(),
|
||||
jurisdiction: v,
|
||||
translator: "t".into(),
|
||||
edition_year: 1891,
|
||||
},
|
||||
"a public-domain source names its jurisdiction",
|
||||
),
|
||||
(
|
||||
|v| Source::PublicDomain {
|
||||
work: "w".into(),
|
||||
author: "a".into(),
|
||||
edition_url: "u".into(),
|
||||
licence: "l".into(),
|
||||
jurisdiction: "j".into(),
|
||||
translator: v,
|
||||
edition_year: 1891,
|
||||
},
|
||||
"a public-domain source names its translator",
|
||||
),
|
||||
(
|
||||
|v| Source::ModelWritten {
|
||||
family: ModelFamily::Unrecorded,
|
||||
|
|
@ -679,6 +727,10 @@ fn an_item_source_attests_with_non_empty_strings() {
|
|||
work: s(work),
|
||||
author: s(author),
|
||||
edition_url: s(url),
|
||||
licence: s("l"),
|
||||
jurisdiction: s("j"),
|
||||
translator: s("t"),
|
||||
edition_year: 1891,
|
||||
};
|
||||
let cases: [(Source, &'static str); 9] = [
|
||||
(
|
||||
|
|
@ -1443,3 +1495,26 @@ fn seed_reseeded() {
|
|||
replay.add(i, &entities).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_public_domain_source_without_an_edition_year_is_refused() {
|
||||
let d = dir();
|
||||
let mut store = CorpusStore::open(&d.path().join("c.cc")).unwrap();
|
||||
let key = "item:pd:1";
|
||||
let source = |edition_year| Source::PublicDomain {
|
||||
work: "w".into(),
|
||||
author: "a".into(),
|
||||
edition_url: "u".into(),
|
||||
licence: "l".into(),
|
||||
jurisdiction: "j".into(),
|
||||
translator: "t".into(),
|
||||
edition_year,
|
||||
};
|
||||
assert_eq!(
|
||||
refusal(store.add(item_in(key, "line", "scene-1", source(0)), &ents(&d))),
|
||||
malformed(key, "a public-domain source names its edition year")
|
||||
);
|
||||
store
|
||||
.add(item_in(key, "line", "scene-1", source(1891)), &ents(&d))
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
|||
1
data/daodejing/corpus.cc
Normal file
1
data/daodejing/corpus.cc
Normal file
|
|
@ -0,0 +1 @@
|
|||
“²cultcache.store.v1‘—±weksa.corpus_item±weksa.corpus_item´weksa.corpus_item.v1±weksa.corpus_itemÙV{"schemaName":"weksa.corpus_item","schemaVersion":"weksa.corpus_item.v1","members":[]}‘±weksa.corpus_item<EFBFBD>•”°item:daodejing:1±weksa.corpus_item´2026-11-07T02:09:00ZÅ4›°item:daodejing:1Ù’Therefore the sage, in the exercise of his government, empties their minds, fills their bellies, weakens their wills, and strengthens their bones.¥en-US¬entity:laozi³daodejing-legge-ch3<EFBFBD><EFBFBD>¬PublicDomain—ÙBThe Tao Teh King, or the Tao and its Characteristics (Dao De Jing)¥LaoziÙ$https://www.gutenberg.org/ebooks/216Ù<36>Public domain (author d. c. 5th century BCE; translator d. 1897); Project Gutenberg eBook 216 text used without its trademark or licence matterÙ7United States and life+70 jurisdictions (Legge d. 1897)«James LeggeÍcª2026-11-07ÀÂ<C380>”°item:daodejing:2±weksa.corpus_item´2026-11-07T02:09:00ZÅÓ›°item:daodejing:2Ù1Heaven is long-enduring and earth continues long.¥en-US¬entity:laozi³daodejing-legge-ch7<68><37>¬PublicDomain—ÙBThe Tao Teh King, or the Tao and its Characteristics (Dao De Jing)¥LaoziÙ$https://www.gutenberg.org/ebooks/216Ù<36>Public domain (author d. c. 5th century BCE; translator d. 1897); Project Gutenberg eBook 216 text used without its trademark or licence matterÙ7United States and life+70 jurisdictions (Legge d. 1897)«James LeggeÍcª2026-11-07ÀÂ<C380>”°item:daodejing:3±weksa.corpus_item´2026-11-07T02:09:00ZÅÑ›°item:daodejing:3Ù/The highest excellence is like (that of) water.¥en-US¬entity:laozi³daodejing-legge-ch8<68><38>¬PublicDomain—ÙBThe Tao Teh King, or the Tao and its Characteristics (Dao De Jing)¥LaoziÙ$https://www.gutenberg.org/ebooks/216Ù<36>Public domain (author d. c. 5th century BCE; translator d. 1897); Project Gutenberg eBook 216 text used without its trademark or licence matterÙ7United States and life+70 jurisdictions (Legge d. 1897)«James LeggeÍcª2026-11-07ÀÂ<C380>”°item:daodejing:4±weksa.corpus_item´2026-11-07T02:09:00ZÅ×›°item:daodejing:4Ù4Words that are strictly true seem to be paradoxical.¥en-US¬entity:laozi´daodejing-legge-ch78<37><38>¬PublicDomain—ÙBThe Tao Teh King, or the Tao and its Characteristics (Dao De Jing)¥LaoziÙ$https://www.gutenberg.org/ebooks/216Ù<36>Public domain (author d. c. 5th century BCE; translator d. 1897); Project Gutenberg eBook 216 text used without its trademark or licence matterÙ7United States and life+70 jurisdictions (Legge d. 1897)«James LeggeÍcª2026-11-07ÀÂ<C380>”°item:daodejing:5±weksa.corpus_item´2026-11-07T02:09:00ZÅæ›°item:daodejing:5ÙDMay not the space between heaven and earth be compared to a bellows?¥en-US¬entity:laozi³daodejing-legge-ch5<68><35>¬PublicDomain—ÙBThe Tao Teh King, or the Tao and its Characteristics (Dao De Jing)¥LaoziÙ$https://www.gutenberg.org/ebooks/216Ù<36>Public domain (author d. c. 5th century BCE; translator d. 1897); Project Gutenberg eBook 216 text used without its trademark or licence matterÙ7United States and life+70 jurisdictions (Legge d. 1897)«James LeggeÍcª2026-11-07ÀÂ<C380>
|
||||
1
data/daodejing/entity.cc
Normal file
1
data/daodejing/entity.cc
Normal file
|
|
@ -0,0 +1 @@
|
|||
“²cultcache.store.v1‘—¬weksa.entity¬weksa.entity¯weksa.entity.v1¬weksa.entityÙL{"schemaName":"weksa.entity","schemaVersion":"weksa.entity.v1","members":[]}‘¬weksa.entity<EFBFBD>‘”¬entity:laozi¬weksa.entity´2026-11-07T02:09:00ZÄb“¬entity:laozi‘’¥en-US‘¥Laozi<EFBFBD>¬PublicDomain‘Ù4The Tao Teh King, or the Tao and its Characteristics
|
||||
BIN
data/daodejing/inventory.cc
Normal file
BIN
data/daodejing/inventory.cc
Normal file
Binary file not shown.
1
data/daodejing/packets.cc
Normal file
1
data/daodejing/packets.cc
Normal file
|
|
@ -0,0 +1 @@
|
|||
“²cultcache.store.v1‘—¸weksa.interlingua_packet¸weksa.interlingua_packet»weksa.interlingua_packet.v1¸weksa.interlingua_packetÙd{"schemaName":"weksa.interlingua_packet","schemaVersion":"weksa.interlingua_packet.v1","members":[]}‘¸weksa.interlingua_packet<EFBFBD>•”·packet:64616f2d30333032¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅSž·packet:64616f2d30333032CorpusExample”¨Authored°item:daodejing:1¸encoder:636c617564652d73ÍX”ÀÀ<EFBFBD><EFBFBD>”¬speech_act:1ÀÀ<EFBFBD>••¢n1¯referent_kind:1©concept:7À<EFBFBD>•¢n2°referent_kind:11©concept:8À<EFBFBD>•¢n3¯referent_kind:3©concept:9À<EFBFBD>•¢n4°referent_kind:11ªconcept:10À<EFBFBD>•¢n5¯referent_kind:3ªconcept:11À<EFBFBD>”•¢n6ªconcept:12§frame:4’’¦role:1¢n1’¦role:2¢n2<EFBFBD>•¢n7ªconcept:13§frame:4’’¦role:1¢n1’¦role:2¢n3<EFBFBD>•¢n8ªconcept:14§frame:4’’¦role:1¢n1’¦role:2¢n4<EFBFBD>•¢n9ªconcept:15§frame:4’’¦role:1¢n1’¦role:2¢n5<EFBFBD><EFBFBD>“<EFBFBD>ÂÀ<EFBFBD>’‘ÙXFour acts of one agent; 'in the exercise of his government' and 'their' are not carried.<EFBFBD>‘‘’ªfeature:18<EFBFBD>¥GradeÍ ”·packet:64616f2d30353032¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅ^ž·packet:64616f2d30353032CorpusExample”¨Authored°item:daodejing:5¸encoder:636c617564652d73ÍX”ÀÀ<EFBFBD><EFBFBD>”¬speech_act:2ÀÀ<EFBFBD>’•¢n1¯referent_kind:4ªconcept:24À<EFBFBD>•¢n2¯referent_kind:5ªconcept:25À<EFBFBD>‘•¢n3ªconcept:26§frame:8’’¦role:3¢n1’¦role:6¢n2<EFBFBD><EFBFBD>“<EFBFBD>ÂÀ<EFBFBD>’‘ÙRA rhetorical question; the speech act is ask and the negated 'may not' is dropped.<EFBFBD>‘‘’ªfeature:18<EFBFBD>¥GradeÍ ”·packet:64616f2d30373031¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅyž·packet:64616f2d30373031CorpusExample”¨Authored°item:daodejing:2¸encoder:636c617564652d73ÍX”ÀÀ<EFBFBD><EFBFBD>”¬speech_act:1ÀÀ<EFBFBD>’•¢n1¯referent_kind:4ªconcept:16À<EFBFBD>•¢n2¯referent_kind:4ªconcept:17À<EFBFBD>’•¢n3ªconcept:18§frame:5‘’¦role:3¢n1<EFBFBD>•¢n4ªconcept:19§frame:5‘’¦role:3¢n2<EFBFBD><EFBFBD>“<EFBFBD>ÂÀ<EFBFBD>’‘ÙTHeaven and earth as places; whether they are entities is a ruling the ontology owes.<EFBFBD>‘‘’ªfeature:18<EFBFBD>¥GradeÍ ”·packet:64616f2d30383031¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅwž·packet:64616f2d30383031CorpusExample”¨Authored°item:daodejing:3¸encoder:636c617564652d73ÍX”ÀÀ<EFBFBD><EFBFBD>”¬speech_act:1ÀÀ<EFBFBD>’•¢n1°referent_kind:11ªconcept:20À<EFBFBD>•¢n2°referent_kind:11ªconcept:21À<EFBFBD>‘•¢n3ªconcept:27§frame:6’’¦role:3¢n1’¦role:6¢n2<EFBFBD><EFBFBD>“<EFBFBD>ÂÀ<EFBFBD>’‘ÙSThe metaphor is a plain likeness predication; the tenor and vehicle are not marked.<EFBFBD>‘’’ªfeature:18<EFBFBD>¥GradeÍ ’ªfeature:16<EFBFBD>¥GradeÍ,”·packet:64616f2d37383034¸weksa.interlingua_packet´2026-11-09T10:16:16ZÅNž·packet:64616f2d37383034CorpusExample”¨Authored°item:daodejing:4¸encoder:636c617564652d73ÍX”ÀÀ<EFBFBD><EFBFBD>”¬speech_act:1ÀÀ<EFBFBD>‘•¢n1°referent_kind:11ªconcept:22À<EFBFBD>‘•¢n2ªconcept:23§frame:7‘’¦role:3¢n1<EFBFBD><EFBFBD>“<EFBFBD>ÂÀ<EFBFBD>’‘ÙmThe whole paradox is in the referent's concept and the frame; the packet holds no more than the surface does.<EFBFBD>‘‘’ªfeature:18<EFBFBD>¥GradeÍ
|
||||
Binary file not shown.
50
data/phonology/en-US.phones.tsv
Normal file
50
data/phonology/en-US.phones.tsv
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Weksa phone inventory, en-US (General American, broad). One phone per line, tab separated:
|
||||
# ipa <TAB> class <TAB> tenseness <TAB> gloss
|
||||
# class: consonant | vowel. tenseness: tense | lax | reduced for a vowel, `-` for a consonant. A multi-character
|
||||
# phone (affricate, diphthong) is one entry and one phone. The segmenter reads only this file: an IPA symbol with no
|
||||
# line here is refused, never guessed. Tense/lax is the textbook classification; it is what separates /i/ (nee) from
|
||||
# /ɪ/ (nih).
|
||||
# Provenance of the i/ɪ rows: /i/ is tense and /ɪ/ is lax. Nibu's own ruling of 2026-11-07 (source
|
||||
# weksa-body:follow_up:chat-s3981-3983, #minds: tense vowel, two syllables, stress on the first) chose ˈnibu and rejected
|
||||
# ˈnɪbu; the entity entry records it, this file is what makes the two forms two different phones.
|
||||
# Stress is not a phone: ˈ and ˌ are read by the segmenter and put on the vowel that follows.
|
||||
p consonant - voiceless bilabial stop
|
||||
b consonant - voiced bilabial stop
|
||||
t consonant - voiceless alveolar stop
|
||||
d consonant - voiced alveolar stop
|
||||
k consonant - voiceless velar stop
|
||||
g consonant - voiced velar stop
|
||||
f consonant - voiceless labiodental fricative
|
||||
v consonant - voiced labiodental fricative
|
||||
θ consonant - voiceless dental fricative
|
||||
ð consonant - voiced dental fricative
|
||||
s consonant - voiceless alveolar fricative
|
||||
z consonant - voiced alveolar fricative
|
||||
ʃ consonant - voiceless postalveolar fricative
|
||||
ʒ consonant - voiced postalveolar fricative
|
||||
h consonant - voiceless glottal fricative
|
||||
tʃ consonant - voiceless postalveolar affricate
|
||||
dʒ consonant - voiced postalveolar affricate
|
||||
m consonant - bilabial nasal
|
||||
n consonant - alveolar nasal
|
||||
ŋ consonant - velar nasal
|
||||
l consonant - alveolar lateral approximant
|
||||
ɹ consonant - alveolar approximant
|
||||
j consonant - palatal approximant
|
||||
w consonant - labial-velar approximant
|
||||
i vowel tense close front unrounded (fleece, nee)
|
||||
ɪ vowel lax near-close front unrounded (kit, nih)
|
||||
ɛ vowel lax open-mid front unrounded (dress)
|
||||
æ vowel lax near-open front unrounded (trap)
|
||||
ɑ vowel tense open back unrounded (palm)
|
||||
ɔ vowel tense open-mid back rounded (thought)
|
||||
ʊ vowel lax near-close back rounded (foot)
|
||||
u vowel tense close back rounded (goose, boo)
|
||||
ʌ vowel lax open-mid back unrounded (strut)
|
||||
ə vowel reduced schwa
|
||||
ɚ vowel reduced r-coloured schwa
|
||||
eɪ vowel tense face diphthong
|
||||
oʊ vowel tense goat diphthong
|
||||
aɪ vowel tense price diphthong
|
||||
aʊ vowel tense mouth diphthong
|
||||
ɔɪ vowel tense choice diphthong
|
||||
|
Can't render this file because it has a wrong number of fields in line 11.
|
1
data/phonology/fixtures/nibu-en-US.phones.json
Normal file
1
data/phonology/fixtures/nibu-en-US.phones.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"language":"en-US","source":"ˈnibu","phones":[{"ipa":"n","stress":"none"},{"ipa":"i","stress":"primary"},{"ipa":"b","stress":"none"},{"ipa":"u","stress":"none"}]}
|
||||
|
|
@ -195,9 +195,10 @@ 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. `addressee_consent` is no feature: it names the
|
||||
predication the addressee asked for or agreed to, a fact about what the
|
||||
addressee did. The English lowerer renders no consent and refuses a packet that
|
||||
carries one; a row that speaks for the request ("you asked her to") reads it
|
||||
and refuses without it.
|
||||
addressee did. The English lowerer's request row (`frame:3`, "{agent} asked
|
||||
{patient} to stop") reads it: the consent must name that predication and its
|
||||
agent must be the addressee, else `ConsentMissing` or `AskerNotAddressee`. A
|
||||
consent naming any other predication is refused as unrendered.
|
||||
|
||||
### `stance`
|
||||
|
||||
|
|
|
|||
116
docs/targets/dao-de-jing-test-bed.md
Normal file
116
docs/targets/dao-de-jing-test-bed.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Dao De Jing test bed
|
||||
|
||||
A lowering test bed on James Legge's public-domain translation of the Dao De
|
||||
Jing, from Meta's analogy: the Dao has many varied human translations, so it is
|
||||
a ready-made field of precedents for what one meaning can be lowered to. Meta
|
||||
proposed it in #weksa:gamecult.org on 2026-11-06 (Matrix event
|
||||
`$2_P0mbtbnnsP4I61UhVMlatuDSTjFXMOuGa4SSbhGmc`: public-domain translations only
|
||||
as the scoring corpus, copyrighted ones cited without copying) and noted the
|
||||
ontology consequence (event `$E4VHwS-zFEk9Fa-daFX8MkfoySn0iX0m1grltEJ9A8Y`:
|
||||
model an imperial native term as a term with several context-tagged readings
|
||||
plus a separately recorded chosen gloss per lowering).
|
||||
|
||||
## What is here
|
||||
|
||||
- `data/daodejing/`: its own stores (inventory revisions 1 to 3, one entity, five
|
||||
corpus items, five packets), apart from the production stores in `data/`.
|
||||
Revision 3 is `weksa::daodejing::revision()`; the packets are
|
||||
`weksa::daodejing::packets()`. The scene revision (`scene_revision()`, also
|
||||
numbered 3) is not applied in these stores and will need renumbering when it
|
||||
is applied anywhere both meet.
|
||||
- Rebuild: `weksa --corpus C --inventory I --entities E` with `inventory seed`,
|
||||
`inventory proof`, `inventory dao`, `entity put entity:laozi ...`,
|
||||
`corpus add ... --source public-domain` per item, then `packet dao <store>`.
|
||||
- Source: Project Gutenberg eBook 216, "The Tao Teh King, or the Tao and its
|
||||
Characteristics", Laozi, tr. James Legge (Sacred Books of the East 39, 1891).
|
||||
Gutenberg's licence (1.E.2) lets a work derived from texts not protected by US
|
||||
copyright be used with no fee provided every Project Gutenberg reference is
|
||||
removed from the work. The items hold only Legge's words, one numbered section
|
||||
or sentence each; no Gutenberg header, licence text or trademark is stored,
|
||||
and the eBook file itself is not committed. `edition_url` names the page the
|
||||
text was retrieved from, as a citation of provenance and not as part of any
|
||||
item's text. The legal-root question of whether a citation counts as a
|
||||
"reference" is not ruled; if Gutenberg says it does, the field moves to the
|
||||
Sacred Books of the East print edition.
|
||||
- `Source::PublicDomain` now carries `licence`, `jurisdiction`, `translator` and
|
||||
`edition_year`, and the store refuses an item missing any of them (an empty
|
||||
string, or year 0).
|
||||
|
||||
## The slice
|
||||
|
||||
Corpus groups are chapters, so the derived split spreads the slice:
|
||||
|
||||
| item | Legge | split |
|
||||
|---|---|---|
|
||||
| `item:daodejing:1` | ch. 3 s. 2 | Train |
|
||||
| `item:daodejing:2` | ch. 7 s. 1, first sentence | Validation |
|
||||
| `item:daodejing:3` | ch. 8 s. 1, first sentence | Validation |
|
||||
| `item:daodejing:4` | ch. 78 s. 4 | Test |
|
||||
| `item:daodejing:5` | ch. 5 s. 2, first sentence | Test |
|
||||
|
||||
Each packet states neutral facts (referents, predications, a speech act) and a
|
||||
stance (restraint 800 on all; affection 300 on the water likeness). The stance is
|
||||
my reading of the sage's voice, not data.
|
||||
|
||||
## First closeness number
|
||||
|
||||
Scored with `weksa score` (chrF, and meaning as cosine under `qwen3-embedding:0.6b`
|
||||
through the local Ollama), one lowered output against its own item.
|
||||
|
||||
| item | split | chrF | meaning | lowered |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Train | 44.18 | 0.831 | The sage empties the minds. The sage fills the bellies. The sage weakens the wills. The sage strengthens the bones. |
|
||||
| 2 | Validation | 82.12 | 0.976 | The heaven is long-enduring. The earth continues long. |
|
||||
| 3 | Validation | 74.99 | 0.974 | The highest excellence is like the water. |
|
||||
| 4 | Test | 82.50 | 0.949 | The words that are strictly true seem paradoxical. |
|
||||
| 5 | Test | 75.99 | 0.894 | The space between heaven and earth may be compared to the bellows. |
|
||||
|
||||
Mean over the five: chrF 71.96, meaning 0.925. Mismatched baseline (each output
|
||||
against the other four references, 20 pairs): chrF 14.1, meaning 0.458. So the
|
||||
scorer separates the right reference from a wrong one by a wide margin; that is
|
||||
all it shows here.
|
||||
|
||||
**Do not read 72 as a measure of the interlingua.** The five outputs are the
|
||||
ceiling of an author who saw the reference:
|
||||
|
||||
- One author wrote the packets, the inventory entries and the lowerer rows with
|
||||
Legge's sentence open. Frame 3 (`act`) was written from the Train item. Frames
|
||||
4 and 5 came from Validation items and frames 6 and 7 from Test items, each
|
||||
written while reading that item. The Train item, the only one lowered by a
|
||||
frame minted from Train, scored lowest (chrF 44.18). The scores rise with
|
||||
how bespoke the frame is, the reverse of generalisation. The Validation and
|
||||
Test numbers measure the lowerer table, not what the interlingua can carry.
|
||||
- n = 5, one translation as reference, one embedding model, no variance.
|
||||
|
||||
## What the scorer cannot see
|
||||
|
||||
- **Stance.** The stance features are in the packets and the lowerer does not
|
||||
render them, so the number is blind to the packet's voice entirely. It would
|
||||
not change if every stance were deleted.
|
||||
- **Dropped meaning.** Item 5 is a negated rhetorical question ("May not ... be
|
||||
compared"); the lowering is a plain statement with no question mark and no
|
||||
"not". Item 1 loses "Therefore", "in the exercise of his government" and "their".
|
||||
Item 4 loses "to be". chrF scores the overlap and cannot say that the speech
|
||||
act changed. The embedding cosine of 0.894 for item 5 is not much lower than
|
||||
for the statement-for-statement items.
|
||||
- **Table defects it forgives.** "The heaven" and "the water" are wrong English
|
||||
(the lowerer prefixes "the" to every referent and has no proper-noun,
|
||||
mass-noun or entity rendering). chrF takes a small penalty only.
|
||||
- **Register and meaning of the Chinese.** The packets were read off Legge's
|
||||
English, not the Chinese, so this is a round trip English to interlingua to
|
||||
English. It measures how well the packet kept Legge's wording, not whether Legge
|
||||
or the packet is right about the Dao. The term "Tao" never appears in the slice.
|
||||
The multi-reading term modelling Meta described is untested.
|
||||
- **Leak.** `weksa score` scores the whole output against the whole reference
|
||||
with no mask, as the leak ruling says. Nothing here enforces that the
|
||||
author did not use the reference; only the labels above say so.
|
||||
|
||||
## What the run showed the interlingua needs
|
||||
|
||||
- Negation and question force: `polarity` is refused by the lowerer ("features")
|
||||
and `ask` has no punctuation row, so two of Laozi's habitual moves, the negated
|
||||
claim and the rhetorical question, cannot be lowered at all.
|
||||
- A frame per sentence shape is the lowerer's cost model today: each new shape
|
||||
is one more table row and one more inventory frame. The Dao's aphorisms are
|
||||
mostly copular and paradoxical, which suggests the next frames to mint
|
||||
(attribution, contrast, negated identity) before more chapters are added.
|
||||
|
|
@ -342,7 +342,7 @@ measurable baseline from a packet: `lower` gives text, `score` gives numbers.
|
|||
| Whether a packet exists | `validate` / `PacketStore::admit` (unchanged) | packet, inventory, corpus, entities | admitted packet or typed refusal | the lowerer |
|
||||
| Wording of a packet in English | `crates/weksa/src/lower.rs`: `lower(&LoweringInput, &Inventory) -> Result<String, LowerError>` | the validated lowering view and the inventory at its revision | text, or a typed refusal naming the missing template | the corpus item, the scorer, any model |
|
||||
| English template table (concept words, frame templates, speech-act openers) | the same module, as rows `(inventory id, gloss it speaks for, surface)` | none; it is data in code | rows the lowerer looks up | the inventory (it owns the ids and glosses, not the English) |
|
||||
| The inventory's concepts and frames | an inventory revision; here `inventory::proof_revision()` (rev 2), hand-seeded as `seed()` is | none | the 5 concepts and 2 frames `nibu-proof-001` names | the lowerer |
|
||||
| The inventory's concepts and frames | an inventory revision; here `inventory::proof_revision()` (rev 2), hand-seeded as `seed()` is | none | the 6 concepts and 3 frames `nibu-proof-001` and the consent-gated request packet name | the lowerer |
|
||||
| What a score counts | `Scorer::score` (unchanged) | output text, reference text | `ItemScore` | `weksa score`, which only reads text and calls it |
|
||||
| The proof packet | `packet::proof_packet()`, typed ids of rev 2, admitted by `weksa packet proof <store>` through `PacketStore::admit`. Not JSON: `Packet`'s serde is CultCache's positional tuple, and JSON in is the xenos boundary the cut 7 encoder owns | none | admitted packet | the CLI decides nothing about validity |
|
||||
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"description": "A public-domain work, with everything a reader needs to check the claim:\nthe edition's licence and the jurisdiction the public-domain status is\nasserted in, who translated it (the translation is the text's copyright,\nnot the original's) and when the edition appeared. A work missing any\nis refused at admission.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"PublicDomain": {
|
||||
|
|
@ -181,6 +182,21 @@
|
|||
"edition_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"edition_year": {
|
||||
"type": "integer",
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0
|
||||
},
|
||||
"jurisdiction": {
|
||||
"type": "string"
|
||||
},
|
||||
"licence": {
|
||||
"type": "string"
|
||||
},
|
||||
"translator": {
|
||||
"type": "string"
|
||||
},
|
||||
"work": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
@ -188,7 +204,11 @@
|
|||
"required": [
|
||||
"work",
|
||||
"author",
|
||||
"edition_url"
|
||||
"edition_url",
|
||||
"licence",
|
||||
"jurisdiction",
|
||||
"translator",
|
||||
"edition_year"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue