Retire the English template table, lower(), request_gate and show_working; weksa lower becomes weksa pronounce #93
21 changed files with 94 additions and 1607 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -22,4 +22,3 @@ build/
|
|||
.venv/
|
||||
venv/
|
||||
target/
|
||||
tools/show-working/out/
|
||||
|
|
|
|||
|
|
@ -60,4 +60,4 @@ language-project truth.
|
|||
`CARGO_HOME=/var/lib/mind-weksa-self/cargo-home cargo test --workspace`.
|
||||
- `weksa packet proof <store>` admits the nibu proof packet under the key
|
||||
`packet:6e6962752d303031` (hex of `nibu-001`), not `nibu-proof-001`. Use that key
|
||||
with `weksa lower <store> <key>`.
|
||||
with `weksa pronounce <store> <key>`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Fixture> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
let rev = p.inventory_rev;
|
||||
let mut ids: Vec<InvId> = 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::<Vec<_>>(),
|
||||
"predications": p.predications.iter().map(|r| j(r)).collect::<Vec<_>>(),
|
||||
"relations": p.relations.iter().map(|r| j(r)).collect::<Vec<_>>(),
|
||||
},
|
||||
"stance": j(&p.stance),
|
||||
"discourse": j(&p.discourse),
|
||||
"context": j(&p.context),
|
||||
"extensions": p.extensions.iter().map(|r| j(r)).collect::<Vec<_>>(),
|
||||
"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<T: serde::Serialize> 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::<Vec<_>>(),
|
||||
}));
|
||||
}
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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<String, LowerError> {
|
||||
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<String, LowerError> {
|
||||
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<Entity, LowerError> {
|
||||
entities
|
||||
.entity(id)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<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.",
|
||||
]
|
||||
);
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -33,20 +33,6 @@ fn assert_clean_and_distinct<T: std::fmt::Display + std::fmt::Debug>(all: Vec<T>
|
|||
#[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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<String, LowerError> {
|
||||
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<String, LowerError> {
|
||||
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<Vec<String>, 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:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FeatureValue>) -> 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]
|
||||
|
|
|
|||
|
|
@ -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 <store> <key> --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 <store> <key>` 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.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<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, 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 <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 |
|
||||
|
||||
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 <concept word>", 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 <item>...
|
||||
[--output FILE]` embeds the output text once against every named item. It
|
||||
stores nothing (Score documents arrive with cut 7) and gates nothing.
|
||||
|
|
|
|||
|
|
@ -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/<mind>/).
|
||||
#
|
||||
# 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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 <code>{e(r['entity'])}</code>" if r["entity"] else ""
|
||||
rows.append(f"<tr><td><code>{e(r['id'])}</code></td><td>referent</td><td>{e(g(ch, r['kind']))} / {e(g(ch, r['concept']))}{ent}</td></tr>")
|
||||
for p in s["meaning"]["predications"]:
|
||||
roles = ", ".join(f"{e(g(ch, x['role']))}=<code>{e(x['filler'])}</code>" for x in p["roles"])
|
||||
rows.append(f"<tr><td><code>{e(p['id'])}</code></td><td>predication</td><td>{e(g(ch, p['concept']))} in frame {e(g(ch, p['frame']))}: {roles}</td></tr>")
|
||||
for r in s["meaning"]["relations"]:
|
||||
rows.append(f"<tr><td></td><td>relation</td><td>{e(g(ch, r['relation']))}: <code>{e(r['from'])}</code> → <code>{e(r['to'])}</code></td></tr>")
|
||||
meaning = "<table><tr><th>node</th><th>kind</th><th>content</th></tr>" + "".join(rows) + "</table>"
|
||||
d = s["discourse"]
|
||||
drows = []
|
||||
if d["speech_act"]:
|
||||
drows.append(f"<tr><td>speech act</td><td>{e(g(ch, d['speech_act']))}</td></tr>")
|
||||
if d["addressee"]:
|
||||
drows.append(f"<tr><td>addressee</td><td><code>{e(d['addressee'])}</code></td></tr>")
|
||||
if d["addressee_consent"]:
|
||||
drows.append(f"<tr><td>addressee consent</td><td>predication <code>{e(d['addressee_consent'])}</code> (what the addressee asked for)</td></tr>")
|
||||
drows.append(f"<tr><td>source</td><td>{e(s['provenance']['source'])}, kind {e(s['kind'])}</td></tr>")
|
||||
return (f"<h3>Slot: meaning</h3>{meaning}<h3>Slot: discourse</h3><table>{''.join(drows)}</table>"
|
||||
+ 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"<tr><td>{e(f['gloss'])} <span class='meta'>{e(f['id'])}</span></td><td>{e(f['slot'])} @ {e(f['at'])}</td><td>{e(val(f['value']))}</td><td><span class='tag {cls}'>{e(note)}</span></td></tr>")
|
||||
feats = ("<table><tr><th>feature</th><th>slot</th><th>value</th><th>measured effect on the lowerer</th></tr>" + "".join(rows) + "</table>") if rows else "<p class='meta'>No features: no hedge, certainty, polarity, irony or stance set.</p>"
|
||||
return ("<h3>Slot: stance, hedge, certainty, restraint (features)</h3>" + feats
|
||||
+ "<p class='meta'>Register: the packet has no register slot. Register and address belong to a target-language profile, not to the packet; this lowerer reads none.</p>")
|
||||
|
||||
|
||||
def outcome_html(fx):
|
||||
o = fx["outcome"]
|
||||
if o["kind"] == "lowered":
|
||||
ipa = o["ipa"]
|
||||
ipa_s = ("IPA of cited entities: <span class='mono'>" + e(" ".join(ipa["ok"])) + "</span>") if "ok" in ipa else f"IPA refused: {e(ipa['error']['reason'])} <code>{e(ipa['error']['variant'])}</code>"
|
||||
sc = []
|
||||
for s in o["scores"]:
|
||||
ref = corpus[s["item"]]["text"]
|
||||
sc.append(f"<div class='ref'>{'Target' if s['target'] else 'Other reference'} <code>{e(s['item'])}</code>: <q>{e(ref)}</q></div>"
|
||||
f"<table><tr><td width=110>chrF {s['chrf']:.1f}</td><td><div class='bar'><i style='width:{min(s['chrf'],100):.0f}%'></i></div></td>"
|
||||
f"<td width=150>meaning cosine {s['meaning']:.3f}</td><td><div class='bar'><i style='width:{max(s['meaning'],0)*100:.0f}%'></i></div></td></tr></table>")
|
||||
return (f"<h3>Lowered English</h3><div class='out'>{e(o['text'])}</div><p class='meta'>{ipa_s}</p>"
|
||||
f"<h3>Closeness to the corpus ({e(data['build']['embedder'])})</h3>{''.join(sc)}")
|
||||
if o["kind"] == "lower_error":
|
||||
er = o["error"]
|
||||
return f"<h3>Refused by the lowerer</h3><div class='refusal'><b>LowerError::{e(er['variant'])}</b>: {e(er['reason'])}</div><p class='meta'>No text, so no score: nothing was invented to score.</p>"
|
||||
items = "".join(f"<li><b>{e(r['rule'])}</b> <span class='mono'>{e(r['detail'])}</span></li>" for r in o["refusals"])
|
||||
return f"<h3>Refused at admission</h3><div class='refusal'><ul>{items}</ul></div><p class='meta'>The packet never became a lowering input, so the lowerer never saw it.</p>"
|
||||
|
||||
|
||||
def card(fx):
|
||||
o = fx["outcome"]
|
||||
ch = "".join(f"<tr><td><code>{e(c['id'])}</code></td><td>{e(c['gloss'] or '(unresolved)')}</td></tr>" 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"<div><b>{e(a)}</b>{e(b)}</div>" for a, b in steps)
|
||||
return (f"<article id='{e(fx['id'])}' data-fx><h2>{e(fx['title'])}</h2><p>{e(fx['note'])}</p>"
|
||||
f"<p class='meta'>Packet <code>{e(fx['packet_id'])}</code> for <code>{e(fx['corpus_item'])}</code>. Made from: {e(fx['source'])}.</p>"
|
||||
f"<div class='chain'>{chain}</div>{slots_html(fx)}"
|
||||
f"<details><summary>Inventory entries chosen ({len(fx['chosen'])})</summary><table><tr><th>id</th><th>gloss at this revision</th></tr>{ch}</table></details>"
|
||||
f"{outcome_html(fx)}</article>")
|
||||
|
||||
|
||||
nav = "".join(f"<a class='{'lowered' if f['outcome']['kind']=='lowered' else 'refused'}' href='#{e(f['id'])}'>{e(f['title'])}</a>" for f in data["fixtures"])
|
||||
b = data["build"]
|
||||
page = f"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Weksa: show her working</title><style>{CSS}</style></head><body>
|
||||
<h1>Show her working</h1>
|
||||
<p class="lede">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.</p>
|
||||
<p class="meta">Built from <code>{e(b['commit'][:10])}</code>, embedder <code>{e(b['embedder'])}</code>. English only. Fixtures are the committed <code>nibu-proof-001</code> 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: <a href="data.json">data.json</a>.</p>
|
||||
<nav class="pick">{nav}</nav>
|
||||
{''.join(card(f) for f in data['fixtures'])}
|
||||
<script>{PICK_JS}</script></body></html>"""
|
||||
(out_dir / "index.html").write_text(page, encoding="utf-8")
|
||||
print(f"wrote {out_dir}/index.html and data.json ({len(data['fixtures'])} fixtures)")
|
||||
Loading…
Reference in a new issue