diff --git a/.gitignore b/.gitignore index e00572b..1428a57 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,3 @@ build/ .venv/ venv/ target/ -tools/show-working/out/ diff --git a/AGENTS.md b/AGENTS.md index 55d488a..a82bc28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,4 +60,4 @@ language-project truth. `CARGO_HOME=/var/lib/mind-weksa-self/cargo-home cargo test --workspace`. - `weksa packet proof ` admits the nibu proof packet under the key `packet:6e6962752d303031` (hex of `nibu-001`), not `nibu-proof-001`. Use that key - with `weksa lower `. + with `weksa pronounce `. diff --git a/README.md b/README.md index 2b5d0d3..8becda9 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ measured. - `settings/`: setting-specific proving grounds for language projects. **Live versus planned.** Live: docs, fixtures, examples, ontology data and the -packet crate `crates/weksa`, including `weksa lower` (the template lowerer for -proof fixtures) and `weksa score`, and the golf loop's round and edit-accept +packet crate `crates/weksa`, including `weksa pronounce` (the ruled IPA and phones +of the entities a packet cites), `weksa score`, and the golf loop's round and edit-accept cycle (`weksa loop open|round|cycle`), which encodes and lowers through the model (a prompted Claude subagent, per staged-distill). Planned: Score and lowering documents and the other panel languages. No diff --git a/crates/weksa/examples/show_working.rs b/crates/weksa/examples/show_working.rs deleted file mode 100644 index 6a31b26..0000000 --- a/crates/weksa/examples/show_working.rs +++ /dev/null @@ -1,398 +0,0 @@ -//! Build-time generator for the "Show her working" page. Runs the real -//! admission door, lowerer and IPA reader over the committed proof packet and -//! variants of it, and prints one JSON document on stdout: the typed slots, the -//! inventory entries the packet chose, and the outcome, which is lowered text or -//! a named refusal. Scoring is not here: `tools/show-working/build.py` calls the -//! `weksa score` CLI on the text this emits. -//! -//! Run from the repository root: `cargo run --release --example show_working`. -//! Every variant is a mutation of `proof_packet()`, spelled in its `source`. - -use std::path::Path; - -use serde_json::{Value, json}; -use weksa::packet::{ - EntityId, FeatValue, FeatureValue, Grade, NodeId, ProvenanceSource, RoleFill, proof_packet, -}; -use weksa::{ - CorpusStore, EntityStore, InvId, Inventory, InventoryStore, LowerError, Packet, PacketStore, - StoreError, lower, pronounce, -}; - -struct Fixture { - id: &'static str, - title: &'static str, - note: &'static str, - source: &'static str, - packet: Packet, -} - -fn id(s: &str) -> InvId { - s.parse().expect("inventory id") -} - -fn node(s: &str) -> NodeId { - NodeId::parse(s).expect("node id") -} - -fn grade(feature: &str, v: u16) -> FeatureValue { - FeatureValue { - feature: id(feature), - value: FeatValue::Grade(Grade::new(v).expect("grade")), - } -} - -fn fixtures() -> Vec { - let proof = proof_packet(); - let mut out = vec![Fixture { - id: "proof", - title: "Nibu warns the player off the control", - note: "The committed proof packet, unchanged.", - source: "proof_packet() in crates/weksa/src/packet.rs", - packet: proof.clone(), - }]; - let mut add = |id, title, note, source, f: &dyn Fn(&mut Packet)| { - let mut packet = proof.clone(); - f(&mut packet); - out.push(Fixture { id, title, note, source, packet }); - }; - add( - "no-addressee", - "Same meaning, nobody addressed", - "With the addressee unset the player is a third party: no \"you\", no reflexive.", - "proof_packet() with discourse.addressee = None", - &|p| p.discourse.addressee = None, - ); - add( - "stance-held-back", - "Anger held back, restraint high", - "Stance is the speaker's felt affect. The lowerer has no row that reads it, so the text is the proof's text.", - "proof_packet() with stance anger=300, restraint=700", - &|p| p.stance.features = vec![grade("feature:9", 300), grade("feature:18", 700)], - ); - add( - "hedged", - "Hedged and uncertain warning", - "Hedge and certainty are meaning the lowerer cannot yet say. It refuses rather than drop them.", - "proof_packet() with discourse hedge=600, certainty=400", - &|p| p.discourse.features = vec![grade("feature:6", 600), grade("feature:7", 400)], - ); - add( - "negated", - "Negated intervention (polarity: negative)", - "Dropping the negation would say the opposite, so the lowerer refuses. This is the shape of a negated Dao line awaiting polarity.", - "proof_packet() with predication n5 polarity=negative", - &|p| { - p.predications[1].features = vec![FeatureValue { - feature: id("feature:1"), - value: FeatValue::Option("negative".parse().expect("label")), - }] - }, - ); - add( - "consent-authored", - "Addressee consent, authored source", - "Someone who saw the request says the addressee asked for it. No lowering speaks for that request yet.", - "proof_packet() with discourse.addressee_consent = n4 (source Authored)", - &|p| p.discourse.addressee_consent = Some(node("n4")), - ); - add( - "consent-imported", - "Addressee consent, imported-corpus source", - "A reader of wording cannot have seen a request, so the packet is refused at admission (V15), before any lowering.", - "proof_packet() with addressee_consent = n4 and provenance.source = ImportedCorpus", - &|p| { - p.discourse.addressee_consent = Some(node("n4")); - p.provenance.source = ProvenanceSource::ImportedCorpus; - }, - ); - add( - "relation", - "A causal relation between the predications", - "Relations are not rendered; the lowerer refuses instead of asserting a different meaning.", - "proof_packet() with relation:8 (causes) n4 -> n5", - &|p| { - p.relations = vec![weksa::packet::Relation { - relation: id("relation:8"), - from: node("n4"), - to: node("n5"), - features: vec![], - }] - }, - ); - add( - "dangling-filler", - "A role filled by a node that does not exist", - "Refused at admission (V4): the packet is not a packet.", - "proof_packet() with predication n5 agent filler n9", - &|p| { - p.predications[1].roles[0] = RoleFill { role: id("role:1"), filler: node("n9") } - }, - ); - add( - "unknown-entity", - "A referent naming an entity nobody holds", - "The entity store is the only owner of names; a packet cannot invent one.", - "proof_packet() with referent n3 entity = entity:nobody", - &|p| p.referents[2].entity = Some(EntityId::parse("entity:nobody").expect("entity")), - ); - add( - "common-noun-agent", - "The ship mind as a common noun", - "No entity: the referent lowers as \"the ship mind\", and there is no name to pronounce.", - "proof_packet() with referent n3 entity = None", - &|p| p.referents[2].entity = None, - ); - out -} - -fn gloss(inv: &Inventory, rev: u32, i: &InvId) -> Value { - match inv.resolve(i, rev) { - Some(e) => json!({ "id": i.to_string(), "gloss": e.gloss }), - None => json!({ "id": i.to_string(), "gloss": null }), - } -} - -fn feature_json(inv: &Inventory, rev: u32, slot: &str, at: &str, f: &FeatureValue) -> Value { - let mut g = gloss(inv, rev, &f.feature); - g["slot"] = json!(slot); - g["at"] = json!(at); - g["value"] = serde_json::to_value(&f.value).expect("value"); - g -} - -/// Every feature the packet carries, with where it sits. -fn features(p: &Packet, inv: &Inventory) -> Vec { - let rev = p.inventory_rev; - let mut v = Vec::new(); - for f in &p.discourse.features { - v.push(feature_json(inv, rev, "discourse", "discourse", f)); - } - for f in &p.stance.features { - v.push(feature_json(inv, rev, "stance", "stance", f)); - } - for r in &p.referents { - for f in &r.features { - v.push(feature_json(inv, rev, "referent", r.id.as_str(), f)); - } - } - for pr in &p.predications { - for f in &pr.features { - v.push(feature_json(inv, rev, "predication", pr.id.as_str(), f)); - } - } - v -} - -/// Ids the packet chose, once each, with the inventory's gloss at its revision. -fn chosen(p: &Packet, inv: &Inventory) -> Vec { - let rev = p.inventory_rev; - let mut ids: Vec = Vec::new(); - let mut push = |i: &InvId| { - if !ids.contains(i) { - ids.push(*i) - } - }; - p.context.setting.iter().for_each(&mut push); - p.context.medium.iter().for_each(&mut push); - p.discourse.speech_act.iter().for_each(&mut push); - for r in &p.referents { - push(&r.kind); - push(&r.concept); - } - for pr in &p.predications { - push(&pr.concept); - push(&pr.frame); - pr.roles.iter().for_each(|f| push(&f.role)); - } - p.relations.iter().for_each(|r| push(&r.relation)); - features(p, inv) - .iter() - .filter_map(|f| f["id"].as_str().and_then(|s| s.parse().ok())) - .for_each(|i: InvId| push(&i)); - ids.iter().map(|i| gloss(inv, rev, i)).collect() -} - -fn slots(p: &Packet) -> Value { - let j = |v: &dyn erased::Ser| v.to_value(); - json!({ - "meaning": { - "referents": p.referents.iter().map(|r| j(r)).collect::>(), - "predications": p.predications.iter().map(|r| j(r)).collect::>(), - "relations": p.relations.iter().map(|r| j(r)).collect::>(), - }, - "stance": j(&p.stance), - "discourse": j(&p.discourse), - "context": j(&p.context), - "extensions": p.extensions.iter().map(|r| j(r)).collect::>(), - "constraints": j(&p.constraints), - "provenance": { "source": j(&p.provenance.source), "corpus_item": p.provenance.corpus_item.to_string(), "confidence": p.provenance.confidence.get() }, - "kind": j(&p.kind), - "inventory_rev": p.inventory_rev, - }) -} - -mod erased { - pub trait Ser { - fn to_value(&self) -> serde_json::Value; - } - impl Ser for T { - fn to_value(&self) -> serde_json::Value { - serde_json::to_value(self).expect("serialize") - } - } -} - -fn lower_error(e: &LowerError) -> Value { - let (variant, reason) = match e { - LowerError::Unrendered { what } => ("Unrendered", format!("the packet carries {what}, which the lowerer does not render")), - LowerError::NoRow { id } => ("NoRow", format!("no table row for {id}")), - LowerError::GlossMismatch { id, row } => ("GlossMismatch", format!("{id} is not the {row} the row speaks for")), - LowerError::UnknownRole { frame, role } => ("UnknownRole", format!("{frame} names role {role}, which the inventory lacks")), - LowerError::MissingRole { predication, role } => ("MissingRole", format!("{predication} fills no {role} role")), - LowerError::UnknownEntity { entity } => ("UnknownEntity", format!("the entity store does not hold {entity}")), - LowerError::NoForm { entity } => ("NoForm", format!("{entity} has no en-US form")), - LowerError::NoPronunciation { entity } => ("NoPronunciation", format!("{entity} has no ruled en-US pronunciation")), - LowerError::NoEntityReferent => ("NoEntityReferent", "the packet cites no entity".to_string()), - LowerError::UnknownPhone { entity, at, .. } => ("UnknownPhone", format!("the ruled pronunciation of {entity} holds a symbol at {at} with no listed phone")), - LowerError::DanglingStress { entity, at } => ("DanglingStress", format!("the ruled pronunciation of {entity} has a stress mark at {at} with no vowel after it")), - LowerError::ConsentMissing { predication } => ("ConsentMissing", format!("{predication} is a request the packet does not carry the addressee's consent to")), - LowerError::AskerNotAddressee { predication } => ("AskerNotAddressee", format!("the agent of {predication} is not the addressee")), - LowerError::EntityRead { entity, .. } => ("EntityRead", format!("the entity store failed reading {entity}")), - LowerError::SceneUnread => ("SceneUnread", "the view's scene was never read".to_string()), - LowerError::SpeakerTokenTaken => ("SpeakerTokenTaken", "a referent cites the entity id the speaker token uses".to_string()), - }; - json!({ "variant": variant, "reason": reason }) -} - -struct World { - inventory: InventoryStore, - corpus: CorpusStore, - entities: EntityStore, - inv: Inventory, -} - -/// Admit `packet` through a fresh packet store and lower it. `Ok` carries the -/// text and the IPA outcome; `Err` is the refusal, as JSON. -fn run(w: &World, dir: &Path, packet: &Packet) -> Result<(String, Value), Value> { - let mut store = PacketStore::open(&dir.join("p.cc")).expect("packet store"); - let view = match store.admit(packet.clone(), &w.inventory, &w.corpus, &w.entities) { - Ok(v) => v, - Err(StoreError::Invalid(rs)) => { - return Err(json!({ - "kind": "admission_refused", - "refusals": rs.iter().map(|r| json!({ "rule": r.rule(), "detail": r.to_string() })).collect::>(), - })); - } - Err(e) => { - return Err(json!({ "kind": "admission_refused", "refusals": [{ "rule": "store", "detail": e.to_string() }] })); - } - }; - let text = lower(&view, &w.inv, &w.entities).map_err(|e| { - json!({ "kind": "lower_error", "error": lower_error(&e) }) - })?; - let ipa = match pronounce(&view, &w.entities) { - Ok(v) => json!({ "ok": v }), - Err(e) => json!({ "error": lower_error(&e) }), - }; - Ok((text, ipa)) -} - -fn main() { - let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let inventory = InventoryStore::open(&root.join("data/interlingua/inventory.cc")).expect("inventory"); - let inv = inventory.inventory().expect("inventory view"); - let w = World { - corpus: CorpusStore::open(&root.join("data/corpus/corpus.cc")).expect("corpus"), - entities: EntityStore::open(&root.join("data/entity/entity.cc")).expect("entities"), - inventory, - inv, - }; - let dir = std::env::temp_dir().join(format!("show-working-{}", std::process::id())); - std::fs::create_dir_all(&dir).expect("scratch dir"); - let mut out = Vec::new(); - for fx in fixtures() { - let p = &fx.packet; - let first = run(&w, &dir, p); - let outcome = match first { - Err(e) if e["kind"] == "admission_refused" => e, - first => { - // Ablation: run again with each feature removed, against the - // baseline outcome. It is how the page says whether the lowerer - // read a feature, instead of the page asserting it. - let base = first.as_ref().ok().map(|(t, _)| t.clone()); - let n_disc = p.discourse.features.len(); - let n_stance = p.stance.features.len(); - let total = features(p, &w.inv).len(); - let mut effects = Vec::new(); - for k in 0..total { - let mut q = p.clone(); - if k < n_disc { - q.discourse.features.remove(k); - } else if k < n_disc + n_stance { - q.stance.features.remove(k - n_disc); - } else { - let mut at = k - n_disc - n_stance; - 'o: { - for r in &mut q.referents { - if at < r.features.len() { r.features.remove(at); break 'o; } - at -= r.features.len(); - } - for pr in &mut q.predications { - if at < pr.features.len() { pr.features.remove(at); break 'o; } - at -= pr.features.len(); - } - } - } - let _ = std::fs::remove_file(dir.join("p.cc")); - effects.push(match (&base, run(&w, &dir, &q)) { - (Some(t), Ok((u, _))) if *t == u => "Without it the text is identical: the lowerer did not read it.", - (Some(_), Ok(_)) => "Without it the text changes: the lowerer read it.", - (Some(_), Err(_)) => "Without it the packet is refused.", - (None, Ok(_)) => "Without it the packet lowers: this feature is what the lowerer refuses.", - (None, Err(_)) => "Still refused without this feature alone.", - }); - } - match first { - Ok((text, ipa)) => json!({ "kind": "lowered", "text": text, "ipa": ipa, "feature_effects": effects }), - Err(e) => { let mut e = e; e["feature_effects"] = json!(effects); e } - } - } - }; - let _ = std::fs::remove_file(dir.join("p.cc")); - let mut slots = slots(p); - slots["features"] = json!(features(p, &w.inv)); - out.push(json!({ - "id": fx.id, - "title": fx.title, - "note": fx.note, - "source": fx.source, - "packet_id": p.id.to_string(), - "corpus_item": p.provenance.corpus_item.to_string(), - "slots": slots, - "chosen": chosen(p, &w.inv), - "outcome": outcome, - })); - } - let _ = std::fs::remove_dir_all(&dir); - println!("{}", serde_json::to_string_pretty(&json!({ "fixtures": out })).expect("json")); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn refusal_carries_the_closed_reason_never_a_debug_of_the_error() { - // UnknownPhone holds the ruled IPA text; no field of the output may. - let e = LowerError::UnknownPhone { - entity: EntityId::parse("entity:nibu").expect("entity id"), - at: 3, - text: "CANARY-IPA".into(), - }; - let v = lower_error(&e); - assert!(!v.to_string().contains("CANARY")); - let keys: Vec<_> = v.as_object().expect("object").keys().cloned().collect(); - assert_eq!(keys, ["reason", "variant"]); - } -} diff --git a/crates/weksa/src/lib.rs b/crates/weksa/src/lib.rs index 2510fae..77026ca 100644 --- a/crates/weksa/src/lib.rs +++ b/crates/weksa/src/lib.rs @@ -31,7 +31,7 @@ pub use inventory::{ Change, Family, InvId, Inventory, InventoryEntry, InventoryRevision, OptionLabel, OptionSet, Slot, ValueKind, }; -pub use lower::{EntityForms, LowerError, PhoneLine, TARGET_LANGUAGE, lower, phones, pronounce}; +pub use lower::{EntityForms, LowerError, PhoneLine, TARGET_LANGUAGE, phones, pronounce}; pub use model::{AnswerFiles, ModelPort}; pub use ontology::{Condition, Layer, Ontology, OntologyEntry, Test, select}; pub use packet::{ diff --git a/crates/weksa/src/lower.rs b/crates/weksa/src/lower.rs index 12fbb61..411172d 100644 --- a/crates/weksa/src/lower.rs +++ b/crates/weksa/src/lower.rs @@ -1,52 +1,28 @@ -//! The English template lowerer: the validated lowering view of a packet to -//! text, by a fixed table keyed by inventory ids. It is the deterministic -//! lowerer of `weksa lower`, the proof fixtures and their tests; the golf loop -//! lowers through the `ModelPort` (`run.rs`, `Prompt::lower`), and follow-up -//! `table-retirement` decides this table's future. It renders predications, referents and the -//! speech act, and nothing else: no style, no model, no corpus text. It takes a -//! [`LoweringInput`], so it cannot see the trace or the corpus item. A packet -//! carrying a feature, relation, extension, role fill or referent it does not -//! render is refused, because rendering part of a meaning would assert a different -//! one (`polarity: negative` dropped says the opposite). Context, kind and -//! constraints steer delivery and are not rendered; the concept of a predication -//! must resolve but the frame owns the verb. +//! The entity port and the ruled pronunciation of a packet's entities. //! -//! A referent that cites an entity renders as that entity's own form, read -//! through the [`EntityForms`] port: the first (citation) form stored for -//! [`TARGET_LANGUAGE`], verbatim. The choice is a table lookup on the language -//! tag and the form's position, not a reading of the form. Forms after the -//! first are inflected or particle-bound shapes; no English frame asks for one, -//! so none is selected. An entity the port does not hold, or one with no form in -//! 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. +//! Wording of a packet in a target language is the `ModelPort`'s answer to +//! `Prompt::lower` (`context.rs`); nothing here produces lowered text. This +//! module holds what the entity store owns and the lowering path reads: the +//! narrow [`EntityForms`] port, the ruled IPA of every entity a packet cites +//! ([`pronounce`]), and its segmentation into phones ([`phones`]). A referent +//! that cites an entity is read through the port; an entity the port does not +//! hold, one with no form or no ruled pronunciation in [`TARGET_LANGUAGE`], is +//! refused, because spelling is not a pronunciation and there is no +//! grapheme-to-phoneme guess to offer in its place. use std::fmt; use serde::Serialize; use crate::entity::Entity; -use crate::inventory::{Family, InvId, Inventory}; -use crate::packet::{EntityId, NodeId, Predication}; +use crate::packet::EntityId; use crate::phones::{Phone, PhoneInventory, SegmentError}; use crate::store::EntityStore; use crate::validate::LoweringInput; -/// The language tag of the template table and the phones: what `weksa lower` -/// says in. The golf loop lowers each item in its own language and does not -/// read this. Spelled as an entity's `LanguageForms::language` and a corpus -/// item's language are. +/// The language tag of [`pronounce`] and [`phones`] only. The golf loop lowers +/// each item in its own language and does not read this. Spelled as an +/// entity's `LanguageForms::language` and a corpus item's language are. pub const TARGET_LANGUAGE: &str = "en-US"; /// The lowerer's whole view of the entity store: one entity by id. Narrow so a @@ -62,98 +38,8 @@ impl EntityForms for EntityStore { } } -/// `(inventory id, the gloss the row speaks for, surface)`. -type Row = (&'static str, &'static str, &'static str); - -/// Referent concepts, as the noun phrase after "the". -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 -/// of the role whose inventory gloss it is. -const FRAMES: &[Row] = &[ - ( - "frame:1", - "hazard_warning", - "{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: "), -]; - #[derive(Clone, Debug, PartialEq, Eq)] pub enum LowerError { - /// The packet carries a slot the lowerer does not render. - Unrendered { what: &'static str }, - /// No table row for an id the packet uses. - NoRow { id: InvId }, - /// The row's gloss is not the gloss the inventory gives its id at the - /// packet's revision, or the id does not resolve there. - GlossMismatch { id: InvId, row: &'static str }, - /// A frame template names a role the inventory has no live entry for. - UnknownRole { frame: InvId, role: String }, - /// The predication fills no role its frame's template needs. - MissingRole { predication: NodeId, role: String }, /// A referent cites an entity the entity port does not hold. UnknownEntity { entity: EntityId }, /// The entity has no form in [`TARGET_LANGUAGE`]. @@ -165,7 +51,7 @@ pub enum LowerError { 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. + /// inventory). No phone is guessed. UnknownPhone { entity: EntityId, at: usize, @@ -174,12 +60,6 @@ pub enum LowerError { /// 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 }, /// A lowering prompt was asked of a view whose scene was never read. @@ -192,11 +72,6 @@ pub enum LowerError { impl fmt::Display for LowerError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { - LowerError::Unrendered { .. } => "packet carries a slot the lowerer does not render", - LowerError::NoRow { .. } => "no table row for an inventory id the packet uses", - LowerError::GlossMismatch { .. } => "table row does not match the inventory's gloss", - LowerError::UnknownRole { .. } => "frame template names a role the inventory lacks", - LowerError::MissingRole { .. } => "predication fills no role its frame needs", LowerError::UnknownEntity { .. } => "referent cites an entity the port does not hold", LowerError::NoForm { .. } => "entity has no form in the target language", LowerError::NoPronunciation { .. } => "entity has no ruled pronunciation", @@ -205,149 +80,17 @@ impl fmt::Display for LowerError { "ruled pronunciation holds a symbol with no listed phone" } LowerError::DanglingStress { .. } => "stress mark with no vowel after it", - LowerError::ConsentMissing { .. } => "request lacks the addressee's consent", - LowerError::AskerNotAddressee { .. } => "request agent is not the addressee", LowerError::EntityRead { .. } => "entity port failed to read", LowerError::SceneUnread => "the view's scene was never read", - LowerError::SpeakerTokenTaken => "a referent cites the entity id the speaker token uses", + LowerError::SpeakerTokenTaken => { + "a referent cites the entity id the speaker token uses" + } }) } } impl std::error::Error for LowerError {} -fn row( - table: &'static [Row], - id: InvId, - inv: &Inventory, - rev: u32, -) -> Result<&'static str, LowerError> { - let (_, gloss, surface) = table - .iter() - .find(|(key, _, _)| *key == id.to_string()) - .ok_or(LowerError::NoRow { id })?; - match inv.resolve(&id, rev) { - Some(e) if e.gloss == *gloss => Ok(surface), - _ => Err(LowerError::GlossMismatch { id, row: gloss }), - } -} - -pub fn lower( - input: &LoweringInput, - inv: &Inventory, - entities: &dyn EntityForms, -) -> Result { - let rev = input.inventory_rev(); - let unrendered = |what| Err(LowerError::Unrendered { what }); - if input.predications().is_empty() { - return unrendered("no predication"); - } - if !input.discourse().features.is_empty() - || input.referents().iter().any(|r| !r.features.is_empty()) - || input.predications().iter().any(|p| !p.features.is_empty()) - { - return unrendered("features"); - } - request_gate(input, inv)?; - if !input.relations().is_empty() { - return unrendered("relations"); - } - if !input.extensions().is_empty() { - return unrendered("extensions"); - } - let opener = match input.discourse().speech_act { - Some(act) => row(SPEECH_ACTS, act, inv, rev)?, - None => "", - }; - let mut sentences = Vec::new(); - for p in input.predications() { - sentences.push(clause(input, inv, entities, p)?); - } - let mentioned = |r: &NodeId| { - input - .predications() - .iter() - .any(|p| p.roles.iter().any(|f| &f.filler == r)) - }; - if input.referents().iter().any(|r| !mentioned(&r.id)) { - return unrendered("a referent no role mentions"); - } - let mut out = String::new(); - for (i, (clause, name_first)) in sentences.iter().enumerate() { - if i > 0 { - out.push(' '); - } - if i == 0 { - out.push_str(opener); - } - let mut chars = clause.chars(); - if let Some(first) = chars.next() { - // A name opens its clause as written; only template words are cased. - if (i == 0 && !opener.is_empty()) || *name_first { - out.push(first); - } else { - out.extend(first.to_uppercase()); - } - out.push_str(chars.as_str()); - } - out.push('.'); - } - 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 @@ -424,91 +167,6 @@ fn ruled( Ok(out) } -fn clause( - input: &LoweringInput, - inv: &Inventory, - entities: &dyn EntityForms, - p: &Predication, -) -> Result<(String, bool), LowerError> { - let rev = input.inventory_rev(); - let template = row(FRAMES, p.frame, inv, rev)?; - let mut said: Vec<&NodeId> = Vec::new(); - let mut out = String::new(); - let mut name_first = false; - let mut rest = template; - while let Some(open) = rest.find('{') { - out.push_str(&rest[..open]); - 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)) - .ok_or_else(|| LowerError::UnknownRole { - frame: p.frame, - role: name.to_string(), - })?; - let fill = - p.roles - .iter() - .find(|r| r.role == role.id) - .ok_or_else(|| LowerError::MissingRole { - predication: p.id.clone(), - role: name.to_string(), - })?; - let (phrase, is_name) = filler(input, inv, entities, &fill.filler, &said)?; - name_first |= is_name && out.is_empty(); - out.push_str(&phrase); - said.push(&fill.filler); - } - out.push_str(rest); - if p.roles.len() != said.len() { - return Err(LowerError::Unrendered { - what: "a role the frame does not name", - }); - } - Ok((out, name_first)) -} - -/// A role filler as a phrase: the addressee is "you", a predication is "that", -/// another referent is its entity's citation form when it cites one, else "the" -/// and its concept word. The addressee said twice in -/// one predication is reflexive; no other referent is, because the packet has no -/// animacy to choose "themself" or "itself" by. -fn filler( - input: &LoweringInput, - inv: &Inventory, - entities: &dyn EntityForms, - node: &NodeId, - said: &[&NodeId], -) -> Result<(String, bool), LowerError> { - let again = said.contains(&node); - if let Some(r) = input.referents().iter().find(|r| &r.id == node) { - if input.discourse().addressee.as_ref() == Some(node) { - return Ok((if again { "yourself" } else { "you" }.to_string(), false)); - } - if let Some(id) = &r.entity { - return Ok((name(entities, id)?, true)); - } - let word = row(CONCEPTS, r.concept, inv, input.inventory_rev())?; - return Ok((format!("the {word}"), false)); - } - Ok(("that".to_string(), false)) -} - -/// The entity's citation form in [`TARGET_LANGUAGE`]. -fn name(entities: &dyn EntityForms, id: &EntityId) -> Result { - entity(entities, id)? - .forms_in(TARGET_LANGUAGE) - .and_then(<[String]>::first) - .cloned() - .ok_or_else(|| LowerError::NoForm { entity: id.clone() }) -} - pub(crate) fn entity(entities: &dyn EntityForms, id: &EntityId) -> Result { entities .entity(id) diff --git a/crates/weksa/src/main.rs b/crates/weksa/src/main.rs index 21693d5..a0974b6 100644 --- a/crates/weksa/src/main.rs +++ b/crates/weksa/src/main.rs @@ -20,7 +20,7 @@ use weksa::run::{ use weksa::score::ScoreInput; use weksa::{ Annotator, CorpusStore, EntityId, EntityStore, Family, InventoryStore, OllamaEmbedder, OntologyStore, OptionLabel, - PacketStore, PairRelation, Party, Scene, Slot, SpeakerStateStore, ValueKind, Scorer, Split, SplitScheme, StoreError, StoreRefusal, lower, pronounce, validate, + PacketStore, PairRelation, Party, Scene, Slot, SpeakerStateStore, ValueKind, Scorer, Split, SplitScheme, StoreError, StoreRefusal, pronounce, validate, }; #[derive(Parser)] @@ -64,17 +64,14 @@ enum Command { Scene(SceneCommand), #[command(subcommand)] Packet(PacketCommand), - /// Lower a stored packet to English text. - Lower { + /// Print the ruled IPA of each entity a stored packet cites, one per line; + /// refused for an entity with no ruled pronunciation. + Pronounce { store: PathBuf, 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, 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. + /// Print the phones line of each entity instead, one JSON object per + /// line ({language, source, phones: [{ipa, stress}]}); refused for a + /// phone the inventory lacks. #[arg(long)] phones: bool, }, @@ -1120,14 +1117,8 @@ fn main() -> Result<()> { .map_err(refused)?; println!("admitted {key}"); } - Command::Lower { - store, - key, - ipa, - phones, - } => { + Command::Pronounce { store, key, phones } => { let inventory_store = InventoryStore::open(&cli.inventory)?; - let inventory = inventory_store.inventory()?; let mut packets = PacketStore::open(&store)?; let packet = packets.get(&key)?.context("no such packet")?; // Admission is the only source of a LoweringInput; re-admitting the @@ -1141,16 +1132,14 @@ fn main() -> Result<()> { ) .map_err(refused)?; let entities = EntityStore::open(&cli.entities)?; - if ipa { - for line in pronounce(&view, &entities)? { - println!("{line}"); - } - } else if phones { + if phones { for line in weksa::phones(&view, &entities)? { println!("{}", serde_json::to_string(&line)?); } } else { - println!("{}", lower(&view, &inventory, &entities)?); + for line in pronounce(&view, &entities)? { + println!("{line}"); + } } } Command::Score(args) => { diff --git a/crates/weksa/src/run.rs b/crates/weksa/src/run.rs index 7e2609d..fb2b94a 100644 --- a/crates/weksa/src/run.rs +++ b/crates/weksa/src/run.rs @@ -19,8 +19,7 @@ //! take caller stores (`validate`, `packet proof|dao`, `lower`) are unchanged. //! //! A round encodes, admits, then lowers each admitted view through the -//! `ModelPort` (`Prompt::lower`), in two answer phases; the CLI's `lower` still -//! uses the template table. The prompt source pin covers `context.rs`, +//! `ModelPort` (`Prompt::lower`), in two answer phases; no other path produces lowered text. The prompt source pin covers `context.rs`, //! `split.rs` and `lower.rs`. //! //! `RunStores::stance_ablation`, like `recall`, is a diagnostic in its own @@ -3973,7 +3972,7 @@ mod tests { ); } if prompt.text().starts_with(crate::context::LOWER_INSTRUCTIONS) { - // The table's English for the Dao items, the lowerer's answer. + // The fake lowerer's answer for the Dao items. let text = match prompt.item_keys()[0].as_str() { "item:daodejing:1" => "The sage empties the minds. The sage fills the bellies. The sage weakens the wills. The sage strengthens the bones.", "item:daodejing:2" => "The heaven is long-enduring. The earth continues long.", diff --git a/crates/weksa/tests/cli.rs b/crates/weksa/tests/cli.rs index bfcd2d8..ef21abc 100644 --- a/crates/weksa/tests/cli.rs +++ b/crates/weksa/tests/cli.rs @@ -904,7 +904,7 @@ fn fake_embedder() -> String { } #[test] -fn proof_packet_lowers_and_the_lowering_scores_against_items() { +fn proof_packet_pronounces_and_the_item_scores_against_its_own_text() { let d = tempfile::tempdir().unwrap(); cast_entities(d.path(), &["nibu"]); for args in [&["inventory", "seed"][..], &["inventory", "proof"]] { @@ -939,21 +939,11 @@ fn proof_packet_lowers_and_the_lowering_scores_against_items() { "{}", out(&admitted) ); - let lowered = weksa(d.path(), &["lower", "./p.cc", "packet:6e6962752d303031"]); - assert!(lowered.status.success(), "{}", out(&lowered)); - let text = String::from_utf8(lowered.stdout).unwrap(); - assert!( - text.starts_with("Warning: you may endanger yourself"), - "{text}" - ); - // The referent cites entity:nibu: its stored en-US form (NIBU here) reaches the text as stored. - assert!(text.contains("NIBU will stop"), "{text}"); - - let missing = weksa(d.path(), &["lower", "./p.cc", "packet:0000000000000000"]); + let missing = weksa(d.path(), &["pronounce", "./p.cc", "packet:0000000000000000"]); assert!(!missing.status.success() && out(&missing).contains("no such packet")); // IPA is the ruled pronunciation or nothing: spelling is not one. - let args = ["lower", "./p.cc", "packet:6e6962752d303031", "--ipa"]; + let args = ["pronounce", "./p.cc", "packet:6e6962752d303031"]; let unruled = weksa(d.path(), &args); assert!( !unruled.status.success() && out(&unruled).contains("entity has no ruled pronunciation"), @@ -993,11 +983,10 @@ 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. + // The phones line is the committed fixture, byte for byte. let phones = weksa( d.path(), - &["lower", "./p.cc", "packet:6e6962752d303031", "--phones"], + &["pronounce", "./p.cc", "packet:6e6962752d303031", "--phones"], ); assert!(phones.status.success(), "{}", out(&phones)); let fixture = std::fs::read_to_string( @@ -1006,21 +995,13 @@ fn proof_packet_lowers_and_the_lowering_scores_against_items() { ) .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); + // The old lowering subcommand is gone: clap's usage error, exit 2. + let gone = weksa(d.path(), &["lower", "./p.cc", "packet:6e6962752d303031"]); + assert_eq!(gone.status.code(), Some(2), "{}", out(&gone)); - std::fs::write(d.path().join("out.txt"), &text).unwrap(); + // The item's own text, as the scorer's output. + let text = "Warning: you may endanger yourself by touching the control. NIBU will stop that."; + std::fs::write(d.path().join("out.txt"), text).unwrap(); let url = fake_embedder(); let scored = weksa( d.path(), @@ -1180,7 +1161,7 @@ fn cli_errors_never_echo_identifiers() { ]), "--model-family", ); - // Keys on withdraw, split, validate, lower and score. + // Keys on withdraw, split, validate, pronounce and score. refuses_without_echo(p, &["corpus", "withdraw", "item:canary:7"], "no such item"); refuses_without_echo(p, &["split", "item:canary:7"], "no such item"); std::fs::write(p.join("out.txt"), "text").unwrap(); @@ -1198,7 +1179,7 @@ fn cli_errors_never_echo_identifiers() { &["validate", "p.cc", "packet:canary:7"], "no such packet", ); - refuses_without_echo(p, &["lower", "p.cc", "packet:canary:7"], "no such packet"); + refuses_without_echo(p, &["pronounce", "p.cc", "packet:canary:7"], "no such packet"); // S6 names no held key; S5 names no group. let held = weksa( p, diff --git a/crates/weksa/tests/daodejing.rs b/crates/weksa/tests/daodejing.rs index 7e5f9ae..160c3c6 100644 --- a/crates/weksa/tests/daodejing.rs +++ b/crates/weksa/tests/daodejing.rs @@ -1,11 +1,11 @@ //! 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`). +//! public-domain provenance, and its packets are admitted +//! (`docs/targets/dao-de-jing-test-bed.md`). use std::path::Path; use weksa::daodejing::packets; -use weksa::{CorpusStore, EntityStore, InventoryStore, PacketStore, Source, lower}; +use weksa::{CorpusStore, EntityStore, InventoryStore, PacketStore, Source}; fn root() -> std::path::PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data/daodejing") @@ -39,30 +39,15 @@ fn every_legge_item_names_licence_jurisdiction_translator_and_edition_year() { } #[test] -fn the_slice_packets_are_admitted_and_lower_to_the_scored_english() { +fn the_slice_packets_are_admitted_and_the_committed_store_holds_them() { 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 = 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.", - ] - ); + for p in packets() { + store.admit(p, &inventory, &corpus, &entities).unwrap(); + } // The committed packet store holds exactly what the module builds. let stored = PacketStore::open(&root().join("packets.cc")).unwrap(); for p in packets() { diff --git a/crates/weksa/tests/display.rs b/crates/weksa/tests/display.rs index 20b5b6f..e82b5fb 100644 --- a/crates/weksa/tests/display.rs +++ b/crates/weksa/tests/display.rs @@ -33,20 +33,6 @@ fn assert_clean_and_distinct(all: Vec #[test] fn lower_error_display_names_the_variant_and_no_identifier() { assert_clean_and_distinct(vec![ - LowerError::Unrendered { what: SENTINEL }, - LowerError::NoRow { id: inv() }, - LowerError::GlossMismatch { - id: inv(), - row: SENTINEL, - }, - LowerError::UnknownRole { - frame: inv(), - role: SENTINEL.into(), - }, - LowerError::MissingRole { - predication: node(), - role: SENTINEL.into(), - }, LowerError::UnknownEntity { entity: entity() }, LowerError::NoForm { entity: entity() }, LowerError::NoPronunciation { entity: entity() }, @@ -60,12 +46,6 @@ fn lower_error_display_names_the_variant_and_no_identifier() { entity: entity(), at: 4242, }, - LowerError::ConsentMissing { - predication: node(), - }, - LowerError::AskerNotAddressee { - predication: node(), - }, LowerError::EntityRead { entity: entity(), why: SENTINEL.into(), diff --git a/crates/weksa/tests/golf_loop.rs b/crates/weksa/tests/golf_loop.rs index 0d75b3b..f521ba3 100644 --- a/crates/weksa/tests/golf_loop.rs +++ b/crates/weksa/tests/golf_loop.rs @@ -2742,15 +2742,6 @@ fn source_tripwires() { .collect(); let count = |needle: &str| code.iter().filter(|l| l.contains(needle)).count(); assert!(!run.contains("pub fn new")); - // The round does not call the template table: the model lowers. - assert_eq!( - code.iter() - .filter(|l| l.contains("lower(&") && !l.contains("Prompt::lower(")) - .count(), - 0, - "run.rs calls the table" - ); - assert_eq!(count("crate::lower::lower"), 0, "run.rs imports the table"); assert_eq!( count("Prompt::lower("), 2, diff --git a/crates/weksa/tests/lower.rs b/crates/weksa/tests/lower.rs index 3ec94cf..0cf5347 100644 --- a/crates/weksa/tests/lower.rs +++ b/crates/weksa/tests/lower.rs @@ -1,22 +1,16 @@ -//! The English lowerer against the committed inventory (revision 2) and the -//! `nibu-proof-001` packet, and the rules that keep a table row from speaking -//! for a meaning it does not carry. +//! The entity port, `pronounce` and `phones` against the committed stores and +//! the `nibu-proof-001` packet. mod common; use std::path::Path; -use common::{iv, nd}; use weksa::entity::{Entity, EntityOrigin, LanguageForms}; -use weksa::inventory::proof_revision; -use weksa::packet::{EntityId, FeatValue, FeatureValue, Packet, proof_packet}; +use weksa::packet::{EntityId, Packet, proof_packet}; use weksa::{ CorpusStore, EntityForms, EntityStore, Inventory, InventoryStore, LowerError, LoweringInput, - PacketStore, PhoneInventory, PhoneLine, Pronunciation, Stress, lower, pronounce, validate, + PacketStore, PhoneInventory, PhoneLine, Pronunciation, Stress, pronounce, }; -const PROOF: &str = "Warning: you may endanger yourself by touching the control. \ - Nibu will stop that."; - fn committed() -> (Inventory, tempfile::TempDir) { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); let inv = InventoryStore::open(&root.join("data/interlingua/inventory.cc")).unwrap(); @@ -48,181 +42,6 @@ fn committed_entities() -> EntityStore { EntityStore::open(&root.join("data/entity/entity.cc")).unwrap() } -fn lowered(packet: &Packet, inv: &Inventory) -> Result { - lower(&view(packet, inv), inv, &committed_entities()) -} - -#[test] -fn the_proof_packet_is_admitted_by_the_store_and_lowers_to_its_sentences() { - let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let (inv, dir) = committed(); - 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(); - let mut packets = PacketStore::open(&dir.path().join("p.cc")).unwrap(); - packets - .admit(proof_packet(), &inventory, &corpus, &entities) - .unwrap(); - let stored = packets.get("packet:6e6962752d303031").unwrap().unwrap(); - assert_eq!(lowered(&stored, &inv).unwrap(), PROOF); -} - -#[test] -fn only_the_addressee_is_you_and_is_reflexive() { - let (inv, _dir) = committed(); - let mut p = proof_packet(); - p.discourse.addressee = None; - assert_eq!( - lowered(&p, &inv).unwrap(), - "Warning: the player may endanger the player by touching the control. \ - Nibu will stop that." - ); -} - -#[test] -fn no_speech_act_means_no_opener_and_a_capital() { - let (inv, _dir) = committed(); - let mut p = proof_packet(); - p.discourse.speech_act = None; - assert!(lowered(&p, &inv).unwrap().starts_with("You may endanger")); - p.discourse.speech_act = Some(iv("speech_act:1")); - assert!(lowered(&p, &inv).unwrap().starts_with("You may endanger")); -} - -#[test] -fn a_packet_carrying_what_the_lowerer_does_not_render_is_refused() { - let dir = tempfile::tempdir().unwrap(); - let mut store = InventoryStore::open(&dir.path().join("i.cc")).unwrap(); - let (rev1, entries) = weksa::inventory::seed(); - store.admit_revision(rev1, entries).unwrap(); - let (rev2, entries) = proof_revision(); - store.admit_revision(rev2, entries).unwrap(); - let mut ext = common::entry("extension:1", 3); - ext.value_kind = Some(weksa::ValueKind::Grade); - common::admit(&mut store, 3, vec![ext]); - let inv = store.inventory().unwrap(); - let discourse = |p: &mut Packet| { - p.discourse.features.push(FeatureValue { - feature: iv("feature:6"), - value: FeatValue::Grade(common::grade(300)), - }) - }; - let predication = |p: &mut Packet| { - p.predications[0].features.push(FeatureValue { - feature: iv("feature:1"), - value: FeatValue::Option(common::lab("negative")), - }) - }; - let relation = |p: &mut Packet| { - p.relations.push(weksa::packet::Relation { - relation: iv("relation:8"), - from: nd("n4"), - to: nd("n5"), - features: vec![], - }) - }; - let extension = |p: &mut Packet| { - p.inventory_rev = 3; - p.extensions.push(weksa::packet::Extension { - key: iv("extension:1"), - value: FeatValue::Grade(common::grade(3)), - }) - }; - let referent_feature = |p: &mut Packet| { - p.referents[0].features.push(FeatureValue { - feature: iv("feature:6"), - value: FeatValue::Grade(common::grade(1)), - }) - }; - let no_predication = |p: &mut Packet| p.predications.clear(); - let extra_role = |p: &mut Packet| { - let filler = p.predications[0].roles[0].filler.clone(); - p.predications[0].roles.push(weksa::packet::RoleFill { - role: iv("role:3"), - filler, - }) - }; - let consent = |p: &mut Packet| p.discourse.addressee_consent = Some(nd("n5")); - let lone_referent = |p: &mut Packet| { - let mut r = p.referents[0].clone(); - r.id = nd("n99"); - p.referents.push(r) - }; - for (what, edit) in [ - ("features", &discourse as &dyn Fn(&mut Packet)), - ("features", &predication), - ("addressee consent", &consent), - ("relations", &relation), - ("extensions", &extension), - ("no predication", &no_predication), - ("a role the frame does not name", &extra_role), - ("a referent no role mentions", &lone_referent), - ] { - let mut p = proof_packet(); - edit(&mut p); - assert_eq!( - lower(&admitted(&p, &store), &inv, &committed_entities()), - Err(LowerError::Unrendered { what }), - "{what}" - ); - } - // The seed lets no feature sit on a referent, so that arm is refused by validate. - let mut p = proof_packet(); - referent_feature(&mut p); - assert!(validate(&p, &inv).is_err()); -} - -#[test] -fn an_unknown_row_and_an_unfilled_role_are_refused_by_name() { - let (inv, _dir) = committed(); - let mut p = proof_packet(); - p.discourse.speech_act = Some(iv("speech_act:3")); - assert_eq!( - lowered(&p, &inv), - Err(LowerError::NoRow { - id: iv("speech_act:3") - }) - ); - let mut p = proof_packet(); - p.predications[0].roles.pop(); - assert_eq!( - lowered(&p, &inv), - Err(LowerError::MissingRole { - predication: nd("n4"), - role: "patient".into() - }) - ); -} - -#[test] -fn a_row_cannot_speak_for_an_id_whose_gloss_differs() { - // Revision 2 with concept:4 glossed "hatch" in place of "control": the row - // for concept:4 says "control" and must not render it. - let dir = tempfile::tempdir().unwrap(); - let mut store = InventoryStore::open(&dir.path().join("i.cc")).unwrap(); - let (rev1, entries) = weksa::inventory::seed(); - store.admit_revision(rev1, entries).unwrap(); - let (rev2, mut entries) = proof_revision(); - for e in &mut entries { - if e.id == iv("concept:4") { - e.gloss = "hatch".into(); - } - } - store.admit_revision(rev2, entries).unwrap(); - let inv = store.inventory().unwrap(); - assert_eq!( - lower( - &admitted(&proof_packet(), &store), - &inv, - &committed_entities() - ), - Err(LowerError::GlossMismatch { - id: iv("concept:4"), - row: "control" - }) - ); -} - /// An entity port a test controls: ids to entities, and an optional read fault. #[derive(Default)] struct Entities { @@ -257,109 +76,6 @@ fn nibu(forms: &[(&str, &[&str])]) -> Entity { } } -fn through(entities: &Entities) -> Result { - let (inv, _dir) = committed(); - lower(&view(&proof_packet(), &inv), &inv, entities) -} - -#[test] -fn a_name_survives_lowering_verbatim_where_the_concept_word_was() { - // The entity's own form, not "the ship mind", and not re-cased or inflected: - // the form is lowercase and stays so mid-sentence. - let mut e = Entities::default(); - let n = nibu(&[("en-US", &["nibu-of-the-deep"])]); - e.held.push(n); - // It opens the second sentence, where a template word would be capitalised. - assert_eq!( - through(&e).unwrap(), - "Warning: you may endanger yourself by touching the control. \ - nibu-of-the-deep will stop that." - ); -} - -#[test] -fn only_the_target_languages_citation_form_is_chosen() { - // Forms after the first are inflected shapes no English frame asks for; a - // form under another language tag is never read, whatever order it is in. - let mut e = Entities::default(); - let n = nibu(&[ - ("sa-Latn", &["Nibuwa"]), - ("en-US", &["Nibu", "Nibu's", "Nibus"]), - ]); - e.held.push(n); - assert_eq!(through(&e).unwrap(), PROOF); -} - -#[test] -fn an_entity_the_port_does_not_hold_is_refused_not_replaced_by_its_concept_word() { - let id = EntityId::parse("entity:nibu").unwrap(); - assert_eq!( - through(&Entities::default()), - Err(LowerError::UnknownEntity { entity: id }) - ); -} - -#[test] -fn an_entity_with_no_target_language_form_is_refused() { - let mut e = Entities::default(); - let n = nibu(&[("sa-Latn", &["Nibuwa"])]); - let id = n.id.clone(); - e.held.push(n); - assert_eq!(through(&e), Err(LowerError::NoForm { entity: id })); -} - -#[test] -fn a_failing_entity_port_is_refused_by_name() { - let e = Entities { - fault: true, - ..Entities::default() - }; - assert_eq!( - through(&e), - Err(LowerError::EntityRead { - entity: EntityId::parse("entity:nibu").unwrap(), - why: "store unreadable".into() - }) - ); -} - -#[test] -fn an_addressee_entity_still_lowers_to_you_and_is_never_looked_up() { - // Naming the addressee would break "you"/"yourself": the port is not asked, - // so a port that faults on every read changes nothing. - let (inv, _dir) = committed(); - let mut p = proof_packet(); - p.referents[0].entity = Some(EntityId::parse("entity:nibu").unwrap()); - p.referents[2].entity = None; - let faulting = Entities { - fault: true, - ..Entities::default() - }; - assert_eq!( - lower(&view(&p, &inv), &inv, &faulting).unwrap(), - "Warning: you may endanger yourself by touching the control. \ - The ship mind will stop that." - ); -} - -#[test] -fn a_purpose_the_frame_does_not_say_is_refused_not_dropped() { - // The intervention row says "will stop"; a packet that also fills the - // beneficiary role would assert a purpose the text does not carry. - let (inv, _dir) = committed(); - let mut p = proof_packet(); - p.predications[1].roles.push(weksa::packet::RoleFill { - role: iv("role:8"), - filler: nd("n1"), - }); - assert_eq!( - lowered(&p, &inv), - Err(LowerError::Unrendered { - what: "a role the frame does not name" - }) - ); -} - fn ruled(ipa: &str, rejected: &[&str]) -> Pronunciation { Pronunciation { ipa: ipa.into(), @@ -375,6 +91,21 @@ fn say(entities: &dyn EntityForms) -> Result, LowerError> { pronounce(&view(&proof_packet(), &inv), entities) } +#[test] +fn a_failing_entity_port_is_refused_by_name() { + let e = Entities { + fault: true, + ..Entities::default() + }; + assert_eq!( + say(&e), + Err(LowerError::EntityRead { + entity: EntityId::parse("entity:nibu").unwrap(), + why: "store unreadable".into() + }) + ); +} + #[test] fn nibu_round_trips_from_the_committed_seed_to_its_ruled_ipa() { let seed = committed_entities(); @@ -573,160 +304,3 @@ fn the_inventory_parser_refuses_what_it_cannot_read() { 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 the_asker_is_the_role_the_inventory_glosses_agent_not_the_first_role() { - // role:1 and role:2 swap glosses: "agent" is now role:2, so the asker of - // `asked()` (role:1 filled by the addressee) is the one role:2 names, Nibu. - // A lookup that took the first role, or the first live entry, would read the - // addressee and let the request through. - let dir = tempfile::tempdir().unwrap(); - let mut store = InventoryStore::open(&dir.path().join("i.cc")).unwrap(); - let (rev1, mut entries) = weksa::inventory::seed(); - for e in &mut entries { - if e.id == iv("role:1") { - e.gloss = "patient".into(); - } else if e.id == iv("role:2") { - e.gloss = "agent".into(); - } - } - store.admit_revision(rev1, entries).unwrap(); - let (rev2, entries) = proof_revision(); - store.admit_revision(rev2, entries).unwrap(); - let inv = store.inventory().unwrap(); - assert_eq!( - lower(&admitted(&asked(), &store), &inv, &committed_entities()), - Err(LowerError::AskerNotAddressee { - predication: nd("n4") - }) - ); -} - -#[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:?}"); - } -} diff --git a/crates/weksa/tests/scene.rs b/crates/weksa/tests/scene.rs index 5974883..e4841e6 100644 --- a/crates/weksa/tests/scene.rs +++ b/crates/weksa/tests/scene.rs @@ -11,7 +11,7 @@ use weksa::packet::{FeatValue, FeatureValue, Packet, proof_packet}; use weksa::speaker::{Annotator, PairRelation, Party, Scene}; use weksa::{ CorpusStore, EntityStore, Inventory, InventoryStore, LoweringInput, NameSpan, PacketStore, - Refusal, SceneRead, SpeakerStateStore, StoreError, lower, + Refusal, SceneRead, SpeakerStateStore, StoreError, }; struct W { @@ -158,7 +158,7 @@ fn selected(rev: u32, features: Vec) -> SceneRead { } #[test] -fn a_scene_relation_reaches_the_lowering_view_and_changes_no_english() { +fn a_scene_relation_reaches_the_lowering_view() { let mut w = world(); put(&mut w, standard("g", 1)); let plain = view_of(&w, 3, Some("n3")); @@ -170,10 +170,6 @@ fn a_scene_relation_reaches_the_lowering_view_and_changes_no_english() { .unwrap(); assert_eq!(with.scene(), status().as_slice()); assert_eq!(with.scene_read(), &selected(1, status())); - assert_eq!( - lower(&with, &inv(&w), &w.entities).unwrap(), - lower(&plain, &inv(&w), &w.entities).unwrap() - ); } #[test] diff --git a/docs/english-pronunciation-lowering.md b/docs/english-pronunciation-lowering.md index 0e2f7cc..acd5e37 100644 --- a/docs/english-pronunciation-lowering.md +++ b/docs/english-pronunciation-lowering.md @@ -85,8 +85,8 @@ thing. An entity's citation form in a language may carry a ruled pronunciation (`LanguageForms.pronunciation`): bare IPA with stress marks, who ruled it, the date, the source, and an optional list of rejected variants kept so nobody -re-proposes them. `weksa lower --ipa` prints the stored IPA of each -entity the packet cites, one per line, and refuses with `NoPronunciation` for an +re-proposes them. `weksa pronounce ` prints the stored IPA of each +entity the packet cites, one per line (`--phones` prints the phones line), and refuses with `NoPronunciation` for an entity with none; rejected variants are never read. This is not the plan below: it carries no timing, contour, length or invented prosody, and Weksa has no English G2P, so an unruled name has no IPA. diff --git a/docs/interlingua-standard.md b/docs/interlingua-standard.md index f8e7937..cfed562 100644 --- a/docs/interlingua-standard.md +++ b/docs/interlingua-standard.md @@ -196,10 +196,15 @@ 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'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. +addressee did. `V14` and `V15` check the field: it may be set only by authoring or game +telemetry, and it must name a predication of the packet. Open gap: `frame:3` +(`request_intervention`) is in the committed inventories (`data/interlingua`, +`data/original`, `data/daodejing`) and seeded at `inventory.rs:518`. No +committed packet uses it, but the encoder sees it, and `V14` and `V15` check only +the consent field, so a model-encoded `frame:3` predication with a null +`addressee_consent` reaches `Prompt::lower` and nothing refuses it. The retired +gate covered only the deleted `lower()`. Follow-up +`table-retirement-consent-gap` owns the gap. ### `stance` diff --git a/docs/targets/dao-de-jing-test-bed.md b/docs/targets/dao-de-jing-test-bed.md index 2b71dab..0af4831 100644 --- a/docs/targets/dao-de-jing-test-bed.md +++ b/docs/targets/dao-de-jing-test-bed.md @@ -81,6 +81,8 @@ ceiling of an author who saw the reference: 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. + That table is deleted: the Dao numbers below were measured on it, and the + loop now lowers through the model (`Prompt::lower`). - n = 5, one translation as reference, one embedding model, no variance. ## What the scorer cannot see diff --git a/docs/targets/interlingua-lowering-map.md b/docs/targets/interlingua-lowering-map.md index 1600f6b..2a54223 100644 --- a/docs/targets/interlingua-lowering-map.md +++ b/docs/targets/interlingua-lowering-map.md @@ -606,57 +606,25 @@ measurable with this corpus either. Training pairs are Train only, at a frozen revision; the export is 12b. -## Lowerer first cut (`golf-loop-lowerer-cut`) +## Lowering authorities (`golf-loop-lowerer-cut`, `table-retirement`) -Authority map for the smallest lowerer, written before its code (Imagination -pass by the Weksa Self run, 2026-11-01). The cut exists to give the golf loop a -measurable baseline from a packet: `lower` gives text, `score` gives numbers. +Authority map of who decides what about a packet's wording. The first cut gave +the loop a baseline from an English template table; cut `table-retirement` +deleted that table, `lower()` and the CLI lowering command, so no deterministic path +produces lowered text and the model's answer to `Prompt::lower` is the only +wording owner. `lower.rs` keeps the entity port, `pronounce` and `phones` +(`weksa pronounce`). | Decision | Owner | Inputs | Outputs | Not the owner | |---|---|---|---|---| | Whether a packet exists | `validate` / `PacketStore::admit` (unchanged) | packet, inventory, corpus, entities | admitted packet or typed refusal | the lowerer | | Wording of a scored item in English (golf loop, `golf-loop-model-lowerer`) | the ModelPort's answer to `Prompt::lower` (`context.rs`), read as `LoweringDraft` (`encode.rs`) and held in `run::score_round`'s locals | the admitted view, the inventory at its revision, en-US entity forms | `{"text"}`, scored once, never stored | the corpus item, the scorer, the editor | -| Wording of a packet in English (`weksa lower`, proof fixtures) | `crates/weksa/src/lower.rs`: `lower(&LoweringInput, &Inventory) -> Result` | the validated lowering view and the inventory at its revision | text, or a typed refusal naming the missing template | the corpus item, the scorer, the golf loop | -| 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 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 ` 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 | Decisions this map makes, and why: -- **`lower` takes `&LoweringInput`, not `&Packet`.** A `Packet` carries `trace` - and `provenance.corpus_item`; the model page says the lowerer sees the packet - without its trace and never the corpus item (`lowering-traced`, - `leak-owned-by-flow-and-scorer-3`). `validate` already yields the view, so the - CLI runs `validate` and hands it over. Cut 4 narrows the door to - `PacketStore::admit`; this cut does not wait on it. -- **The frame owns the verb phrase; a predication's concept must resolve but is - not rendered.** Referent concepts supply nouns. A frame template names its - roles by inventory gloss (`{agent}`), resolved against the inventory at the - packet's revision, so a retired or re-glossed role cannot be silently - mis-keyed. A row whose gloss differs from the inventory's entry for its id is - a `LowerError`, so a table row cannot speak for a different meaning than its - id now carries. -- **Rendering rules, all deterministic and all from packet fields:** the - discourse addressee renders "you" ("yourself" when it already filled a role - in the same predication), other referents render "the ", a - predication filling a role renders "that", the speech-act row supplies an - optional opener, sentences are capitalised and end in a full stop. - Entity-bearing referents render by concept word: the lowerer does not read - the entity store in this cut (follow-up: entity forms in the lowerer). -- **No style, no register, no features.** Hedge, certainty, modality and - polarity are not rendered, so the baseline scores what meaning alone buys. - A lowerer that ignores `polarity: negative` would assert the opposite, so - `lower` refuses any packet that carries a feature, relation or extension it - does not render rather than render it wrong (`packet-owns-meaning`); features arrive as templates - learn them. -- **Deleted by later cuts, not this one:** the table in `lower.rs` and - `proof_revision()` are temporary owners. Cut 5 (`language-ontology`) moves - the table to `weksa.ontology_entry` documents selected by the target - profile; cut 7 replaces `proof_revision()` with the loop's minting round. - Nothing is deleted today because no earlier lowerer exists; the YAML - `examples/english-lowering/` hand-lowerings are fixtures, not owners, and - stay. - **`weksa score` is a thin reader of `Scorer::score`**: `weksa score ... [--output FILE]` embeds the output text once against every named item. It stores nothing (Score documents arrive with cut 7) and gates nothing. diff --git a/scripts/publish-show-working.sh b/scripts/publish-show-working.sh deleted file mode 100755 index 3af3194..0000000 --- a/scripts/publish-show-working.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# Build the "Show her working" page from the real lowerer and scorer, then publish it as a Mind preview. -# Run from the repository root, in a Mind's Self run (it writes only /srv/minds-web//). -# -# scripts/publish-show-working.sh [--no-publish] -# -# Needs cargo, python3 and an Ollama embedder at 127.0.0.1:11434 (qwen3-embedding:0.6b): the scorer is not stubbed. -# publish-mind-preview.sh lives in the gamecult-ops checkout: set GAMECULT_OPS to it (default ../gamecult-ops). -set -euo pipefail -[ -f Cargo.toml ] && [ -d tools/show-working ] || { echo "run from the Weksa repository root" >&2; exit 2; } -out=${SHOW_WORKING_OUT:-tools/show-working/out} -rm -rf "$out" -python3 -I tools/show-working/build.py "$out" -[ "${1:-}" = --no-publish ] && { echo "built $out"; exit 0; } -ops=${GAMECULT_OPS:-../gamecult-ops} -exec "$ops/scripts/publish-mind-preview.sh" "$out" weksa-show-working diff --git a/tools/show-working/README.md b/tools/show-working/README.md deleted file mode 100644 index 5e2a320..0000000 --- a/tools/show-working/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Show her working - -A static page that shows the lowering chain for committed fixture packets: typed slots, inventory entries chosen, lowered English, scorer closeness to the corpus target, with hedge, certainty and restraint annotated. Refusals (admission rules, `LowerError`, addressee consent, negation awaiting polarity) are results, not omissions. - -- `crates/weksa/examples/show_working.rs`: runs the real admission door, `lower` and `pronounce` over `proof_packet()` and its variants and prints JSON. Each feature is removed in turn and the packet re-run, so "the lowerer read this" is measured. -- `build.py`: runs that, scores each lowered text with the `weksa score` CLI (needs Ollama with `qwen3-embedding:0.6b`), writes `out/data.json` and one dependency-free `out/index.html`. Fails if anything cannot run; there is no fallback text. -- `scripts/publish-show-working.sh`: build, then publish through gamecult-ops `publish-mind-preview.sh`. - -Adding a fixture: add an entry in `fixtures()` in the example. A packet from another store needs a loader there; none exists yet. diff --git a/tools/show-working/build.py b/tools/show-working/build.py deleted file mode 100755 index ede5458..0000000 --- a/tools/show-working/build.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python3 -"""Build the "Show her working" page from the real lowerer and scorer. - -usage: build.py [out_dir] (run from the repository root) - -1. `cargo run --example show_working` admits each fixture packet through the - packet store and lowers it with the English lowerer (crates/weksa/examples). -2. `weksa score` scores each lowered text against its corpus item (and the - other committed Nibu reference) through the Ollama embedder. -3. out_dir/data.json holds the joined result; out_dir/index.html is one file - with no scripts to fetch and no network calls. - -Nothing on the page is typed by hand except the layout and the reading notes. -If cargo or the scorer cannot run, the build fails: there is no fallback text. -Standard library only. -""" -import json -import pathlib -import re -import subprocess -import sys -import time -from html import escape as e - -root = pathlib.Path.cwd() -out_dir = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "tools/show-working/out") -weksa_bin = None - - -def run(cmd, **kw): - r = subprocess.run(cmd, cwd=root, capture_output=True, text=True, **kw) - if r.returncode != 0: - sys.exit(f"{' '.join(map(str, cmd))} failed:\n{r.stderr}") - return r.stdout - - -def cargo_target(): - meta = json.loads(run(["cargo", "metadata", "--format-version", "1", "--no-deps"])) - return pathlib.Path(meta["target_directory"]) - - -# -- 1. the real lowerer ------------------------------------------------------ -run(["cargo", "build", "--release", "--bin", "weksa", "--example", "show_working"]) -target = cargo_target() / "release" -weksa_bin = target / "weksa" -data = json.loads(run([str(target / "examples" / "show_working")])) - -# -- 2. the real scorer ------------------------------------------------------- -REFS_EXTRA = ["item:nibu:2"] -# The page publishes only the corpus items its fixtures cite (plus the extra -# reference each lowered fixture is scored against). A Validation or Test item -# is never published, even when hand-added to REFS_EXTRA or cited by a fixture: -# the dump holds every split, and the guard below reads each item's split. -cited = {fx["corpus_item"] for fx in data["fixtures"]} | set(REFS_EXTRA) -corpus = {} -for m in re.finditer(r'^(item:\S+) \[(.*?)\]\n "(.*)"$', run([str(weksa_bin), "corpus", "dump"]), re.M): - if m.group(1) in cited: - corpus[m.group(1)] = {"text": m.group(3), "meta": m.group(2)} -if set(corpus) != cited: - sys.exit(f"corpus dump lacks cited items {sorted(cited - set(corpus))}; unexpected {sorted(set(corpus) - cited)}") -# The dump's meta ends with the split (`... group=g Validation`, `... excluded`). -# Only Train and split-less items publish; any other name, including a split -# added later, is held out until it is allowed here. -held_out = {k: v["meta"].split()[-1] for k, v in corpus.items() if v["meta"].split()[-1] not in ("Train", "excluded")} -if held_out: - sys.exit(f"refusing to publish held-out items (nothing written): {held_out}") -model = "qwen3-embedding:0.6b" - - -def score(text, items): - out = run([str(weksa_bin), "score", *items], input=text) - got = {} - for m in re.finditer(r"^(item:\S+) chrf=([\d.]+) meaning=(-?[\d.]+)", out, re.M): - got[m.group(1)] = {"chrf": float(m.group(2)), "meaning": float(m.group(3))} - if set(got) != set(items): - sys.exit(f"scorer answered {sorted(got)} for {items}:\n{out}") - return got - - -for fx in data["fixtures"]: - o = fx["outcome"] - if o["kind"] == "lowered": - refs = [fx["corpus_item"]] + [r for r in REFS_EXTRA if r != fx["corpus_item"]] - o["scores"] = [{"item": k, "target": k == fx["corpus_item"], **v} for k, v in score(o["text"], refs).items()] - -data["corpus"] = corpus -data["build"] = { - "commit": run(["git", "rev-parse", "HEAD"]).strip(), - "embedder": model, - "built_unix": int(time.time()), -} -out_dir.mkdir(parents=True, exist_ok=True) -(out_dir / "data.json").write_text(json.dumps(data, indent=1, ensure_ascii=False), encoding="utf-8") - -# -- 3. the page -------------------------------------------------------------- -CSS = """ -:root{--ink:#1c1b19;--mute:#5d5a54;--line:#cfc9bd;--bg:#f6f3ec;--card:#fff;--ok:#0b6b4b;--warn:#9a4a00;--bad:#9b1c2c;--bar:#365f9c} -*{box-sizing:border-box}body{font:16px/1.55 system-ui,sans-serif;max-width:62rem;margin:2rem auto;padding:0 1rem;color:var(--ink);background:var(--bg)} -h1{margin:0 0 .2rem}h2{margin:0 0 .4rem}h3{margin:1.2rem 0 .3rem;font-size:.85rem;text-transform:uppercase;letter-spacing:.06em;color:var(--mute)} -code,.mono{font:13px/1.4 ui-monospace,Menlo,monospace}code{background:#ece8dd;padding:0 .25rem;border-radius:3px} -.lede{max-width:46rem}.meta{color:var(--mute);font-size:.88rem} -nav.pick{display:flex;flex-wrap:wrap;gap:.4rem;margin:1.2rem 0} -nav.pick a{border:1px solid var(--line);background:var(--card);padding:.25rem .6rem;border-radius:999px;text-decoration:none;color:var(--ink);font-size:.9rem} -nav.pick a.refused{border-color:var(--bad)}nav.pick a.lowered{border-color:var(--ok)} -article{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:1rem 1.3rem;margin:1.4rem 0} -.chain{display:grid;grid-template-columns:repeat(4,1fr);gap:.6rem;margin:.6rem 0 1rem} -.chain div{border:1px solid var(--line);border-radius:6px;padding:.3rem .6rem;font-size:.82rem;color:var(--mute)} -.chain b{display:block;color:var(--ink)} -table{border-collapse:collapse;width:100%;font-size:.9rem}th,td{text-align:left;padding:.25rem .5rem;border-bottom:1px solid #e6e1d5;vertical-align:top}th{color:var(--mute);font-weight:600} -.out{font-size:1.15rem;padding:.6rem .9rem;border-left:4px solid var(--ok);background:#eef6f1} -.refusal{padding:.6rem .9rem;border-left:4px solid var(--bad);background:#faeef0}.refusal b{color:var(--bad)} -.tag{display:inline-block;font-size:.75rem;padding:0 .4rem;border-radius:3px;border:1px solid var(--line);color:var(--mute)} -.tag.read{border-color:var(--ok);color:var(--ok)}.tag.unread{border-color:var(--warn);color:var(--warn)}.tag.refuse{border-color:var(--bad);color:var(--bad)} -.bar{height:.7rem;background:#e4dfd2;border-radius:3px;min-width:8rem;position:relative}.bar i{display:block;height:100%;background:var(--bar);border-radius:3px} -.ref{font-size:.88rem;color:var(--mute);margin:.2rem 0 .6rem}.ref q{color:var(--ink)} -details{margin:.5rem 0}summary{cursor:pointer;color:var(--mute);font-size:.88rem} -section.gaps li{margin:.3rem 0} -@media(max-width:700px){.chain{grid-template-columns:1fr 1fr}} -""" - -PICK_JS = """(function(){var a=[].slice.call(document.querySelectorAll('article[data-fx]'));function show(){var h=location.hash.slice(1);var any=a.some(function(x){return x.id===h});a.forEach(function(x){x.hidden=any&&x.id!==h})} -window.addEventListener('hashchange',show);show()})()""" - - -def g(chosen, i): - return next((c["gloss"] for c in chosen if c["id"] == i), "?") - - -def val(v): - (k, x), = v.items() - return f"{x}‰" if k == "Grade" else str(x) - - -def slots_html(fx): - s, ch = fx["slots"], fx["chosen"] - rows = [] - for r in s["meaning"]["referents"]: - ent = f" · entity {e(r['entity'])}" if r["entity"] else "" - rows.append(f"{e(r['id'])}referent{e(g(ch, r['kind']))} / {e(g(ch, r['concept']))}{ent}") - for p in s["meaning"]["predications"]: - roles = ", ".join(f"{e(g(ch, x['role']))}={e(x['filler'])}" for x in p["roles"]) - rows.append(f"{e(p['id'])}predication{e(g(ch, p['concept']))} in frame {e(g(ch, p['frame']))}: {roles}") - for r in s["meaning"]["relations"]: - rows.append(f"relation{e(g(ch, r['relation']))}: {e(r['from'])} → {e(r['to'])}") - meaning = "" + "".join(rows) + "
nodekindcontent
" - d = s["discourse"] - drows = [] - if d["speech_act"]: - drows.append(f"speech act{e(g(ch, d['speech_act']))}") - if d["addressee"]: - drows.append(f"addressee{e(d['addressee'])}") - if d["addressee_consent"]: - drows.append(f"addressee consentpredication {e(d['addressee_consent'])} (what the addressee asked for)") - drows.append(f"source{e(s['provenance']['source'])}, kind {e(s['kind'])}") - return (f"

Slot: meaning

{meaning}

Slot: discourse

{''.join(drows)}
" - + stance_register(fx)) - - -def stance_register(fx): - s = fx["slots"] - eff = fx["outcome"].get("feature_effects") or [] - rows = [] - for i, f in enumerate(s["features"]): - note = eff[i] if i < len(eff) else "" - cls = "read" if "read it" in note else "unread" if "did not read" in note else "refuse" if "refus" in note else "" - rows.append(f"{e(f['gloss'])} {e(f['id'])}{e(f['slot'])} @ {e(f['at'])}{e(val(f['value']))}{e(note)}") - feats = ("" + "".join(rows) + "
featureslotvaluemeasured effect on the lowerer
") if rows else "

No features: no hedge, certainty, polarity, irony or stance set.

" - return ("

Slot: stance, hedge, certainty, restraint (features)

" + feats - + "

Register: the packet has no register slot. Register and address belong to a target-language profile, not to the packet; this lowerer reads none.

") - - -def outcome_html(fx): - o = fx["outcome"] - if o["kind"] == "lowered": - ipa = o["ipa"] - ipa_s = ("IPA of cited entities: " + e(" ".join(ipa["ok"])) + "") if "ok" in ipa else f"IPA refused: {e(ipa['error']['reason'])} {e(ipa['error']['variant'])}" - sc = [] - for s in o["scores"]: - ref = corpus[s["item"]]["text"] - sc.append(f"
{'Target' if s['target'] else 'Other reference'} {e(s['item'])}: {e(ref)}
" - f"" - f"
chrF {s['chrf']:.1f}
meaning cosine {s['meaning']:.3f}
") - return (f"

Lowered English

{e(o['text'])}

{ipa_s}

" - f"

Closeness to the corpus ({e(data['build']['embedder'])})

{''.join(sc)}") - if o["kind"] == "lower_error": - er = o["error"] - return f"

Refused by the lowerer

LowerError::{e(er['variant'])}: {e(er['reason'])}

No text, so no score: nothing was invented to score.

" - items = "".join(f"
  • {e(r['rule'])} {e(r['detail'])}
  • " for r in o["refusals"]) - return f"

    Refused at admission

      {items}

    The packet never became a lowering input, so the lowerer never saw it.

    " - - -def card(fx): - o = fx["outcome"] - ch = "".join(f"{e(c['id'])}{e(c['gloss'] or '(unresolved)')}" for c in fx["chosen"]) - steps = [("1 Typed slots", "meaning, stance, discourse"), ("2 Inventory entries", f"{len(fx['chosen'])} ids, rev {fx['slots']['inventory_rev']}"), - ("3 Lowered English", {"lowered": "text", "lower_error": "refused", "admission_refused": "never reached"}[o["kind"]]), - ("4 Closeness", "scored" if o["kind"] == "lowered" else "no text, no score")] - chain = "".join(f"
    {e(a)}{e(b)}
    " for a, b in steps) - return (f"

    {e(fx['title'])}

    {e(fx['note'])}

    " - f"

    Packet {e(fx['packet_id'])} for {e(fx['corpus_item'])}. Made from: {e(fx['source'])}.

    " - f"
    {chain}
    {slots_html(fx)}" - f"
    Inventory entries chosen ({len(fx['chosen'])}){ch}
    idgloss at this revision
    " - f"{outcome_html(fx)}
    ") - - -nav = "".join(f"{e(f['title'])}" for f in data["fixtures"]) -b = data["build"] -page = f""" -Weksa: show her working -

    Show her working

    -

    A meaning packet goes in. The English lowerer picks inventory entries, writes a sentence, and a scorer measures how close that sentence is to a human-written target. Every number and sentence below was produced by running the real Rust lowerer and scorer when this page was built. The refusals are results too: when the lowerer cannot say a meaning without changing it, it says so instead of dropping the part it cannot say.

    -

    Built from {e(b['commit'][:10])}, embedder {e(b['embedder'])}. English only. Fixtures are the committed nibu-proof-001 packet and variants of it, each variant described in its own card. Scores are against the committed corpus: chrF (0-100) and embedding cosine. Raw data: data.json.

    - -{''.join(card(f) for f in data['fixtures'])} -""" -(out_dir / "index.html").write_text(page, encoding="utf-8") -print(f"wrote {out_dir}/index.html and data.json ({len(data['fixtures'])} fixtures)")