From 7babf8aadcff9c8b46d96ffd1090ff42fadcba1f Mon Sep 17 00:00:00 2001 From: Weksa Date: Thu, 12 Nov 2026 18:08:04 +0000 Subject: [PATCH] ja-inventory: rev 5 concepts and frames for the original bed (model-drafted, Validation-informed), weksa inventory admit, onto:ja-JP:15 (#54) --- README.md | 4 +- crates/weksa/src/encode.rs | 32 ++-- crates/weksa/src/main.rs | 25 ++- crates/weksa/src/run.rs | 4 +- crates/weksa/tests/cli.rs | 28 ++++ crates/weksa/tests/original_corpus.rs | 193 ++++++----------------- data/ontology/ontology.cc | Bin 12262 -> 12856 bytes data/original/inventory.cc | Bin 11369 -> 34658 bytes docs/targets/interlingua-lowering-map.md | 6 +- 9 files changed, 130 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index 34f83c0..0f04479 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ measured. published schemas in `schemas/cultnet/`. - `data/corpus/corpus.cc`, `data/entity/entity.cc` and `data/interlingua/inventory.cc`: the English corpus, its entities and the inventory, read with `weksa corpus dump`, - `weksa entity dump` and `weksa inventory dump`. + `weksa entity dump` and `weksa inventory dump`. `data/original/inventory.cc` + carries its own Validation-informed lexicon revision (rev 5) for lowering + measurements; scored runs use `data/interlingua`. - [Lowering target](./docs/targets/interlingua-lowering.md) and [its map](./docs/targets/interlingua-lowering-map.md): the design, scoring and cut sequence. diff --git a/crates/weksa/src/encode.rs b/crates/weksa/src/encode.rs index 5ae008a..d6b22db 100644 --- a/crates/weksa/src/encode.rs +++ b/crates/weksa/src/encode.rs @@ -223,19 +223,22 @@ impl EditDraft { } /// The revision after `base` this edit asks for, and the entries it adds. - /// `None` when the edit makes no change or more than [`MAX_EDIT_CHANGES`]. + /// `None` when the edit makes no change or more than `max_changes`. The + /// loop's cycle passes [`MAX_EDIT_CHANGES`]; the CLI's `inventory admit` + /// passes its own larger cap for a draft reviewed whole in its cut. /// Every id is the store's: an added entry takes the next number of its /// family after the highest in `inventory`, retired entries included, in /// draft order. Nothing else is judged here: the gloss, the entry's shape, /// whether a retire target is live and a repeated id are /// `InventoryStore::admit_revision`'s to refuse. - pub(crate) fn into_revision( + pub fn into_revision( self, inventory: &Inventory, base: u32, + max_changes: usize, ) -> Option<(InventoryRevision, Vec)> { let count = self.add.len() + self.retire.len(); - if count == 0 || count > MAX_EDIT_CHANGES { + if count == 0 || count > max_changes { return None; } let n = base.checked_add(1)?; @@ -583,7 +586,7 @@ mod tests { InvId::parse("frame:2").unwrap(), ], }; - let (rev, entries) = draft.clone().into_revision(&inv, 6).unwrap(); + let (rev, entries) = draft.clone().into_revision(&inv, 6, MAX_EDIT_CHANGES).unwrap(); let ids: Vec = entries.iter().map(|e| e.id.to_string()).collect(); // After the highest of the family, retired ones included, in draft order. assert_eq!(ids, ["concept:4", "role:1", "concept:5", "frame:3"]); @@ -626,20 +629,25 @@ mod tests { .collect(), retire: vec![], }; - assert!(adds(0).into_revision(&inv, 1).is_none()); - assert!(adds(1).into_revision(&inv, 1).is_some()); - assert_eq!(adds(8).into_revision(&inv, 1).unwrap().1.len(), 8); - assert!(adds(9).into_revision(&inv, 1).is_none()); + assert!(adds(0).into_revision(&inv, 1, MAX_EDIT_CHANGES).is_none()); + assert!(adds(1).into_revision(&inv, 1, MAX_EDIT_CHANGES).is_some()); + assert_eq!(adds(8).into_revision(&inv, 1, MAX_EDIT_CHANGES).unwrap().1.len(), 8); + assert!(adds(9).into_revision(&inv, 1, MAX_EDIT_CHANGES).is_none()); // Adds and retires count together. let mixed = |a: usize, r: usize| EditDraft { add: (0..a).map(|_| add(Family::Concept, "g")).collect(), retire: vec![InvId::parse("concept:1").unwrap(); r], }; - assert!(mixed(4, 4).into_revision(&inv, 1).is_some()); - assert!(mixed(4, 5).into_revision(&inv, 1).is_none()); - assert!(mixed(0, 9).into_revision(&inv, 1).is_none()); + assert!(mixed(4, 4).into_revision(&inv, 1, MAX_EDIT_CHANGES).is_some()); + assert!(mixed(4, 5).into_revision(&inv, 1, MAX_EDIT_CHANGES).is_none()); + assert!(mixed(0, 9).into_revision(&inv, 1, MAX_EDIT_CHANGES).is_none()); // A repeated retire is the door's to refuse, not this fn's. - assert!(mixed(0, 2).into_revision(&inv, 1).is_some()); + assert!(mixed(0, 2).into_revision(&inv, 1, MAX_EDIT_CHANGES).is_some()); + // The cap is the caller's. + let (_, entries) = adds(200).into_revision(&inv, 1, 256).unwrap(); + assert_eq!(entries.first().unwrap().id.to_string(), "concept:2"); + assert_eq!(entries.last().unwrap().id.to_string(), "concept:201"); + assert!(adds(257).into_revision(&inv, 1, 256).is_none()); } #[test] diff --git a/crates/weksa/src/main.rs b/crates/weksa/src/main.rs index c4d7be6..24e68b0 100644 --- a/crates/weksa/src/main.rs +++ b/crates/weksa/src/main.rs @@ -8,7 +8,7 @@ use clap::error::{ContextKind, ContextValue, ErrorKind}; use clap::{Args, Parser, Subcommand, ValueEnum}; use unicode_normalization::UnicodeNormalization; use weksa::corpus::{CorpusItem, ModelFamily, NameSpan, Source, Turn}; -use weksa::encode::OntologyDraft; +use weksa::encode::{EditDraft, OntologyDraft}; use weksa::entity::{Entity, EntityOrigin, LanguageForms, Pronunciation}; use weksa::inventory::{proof_revision, seed, speaker_scene_revision}; use weksa::packet::{FeatValue, FeatureValue, Grade}; @@ -200,8 +200,15 @@ enum InventoryCommand { /// Admit the speaker-state revision (Scene and Speaker features) as the /// inventory's latest plus one. SpeakerScene, + /// Admit a model's inventory draft (a JSON file, {add, retire} as the + /// loop's editor writes it) as the inventory's latest plus one. + Admit { answer: PathBuf }, } +/// The most changes `inventory admit` takes in one draft. A hand-admitted draft +/// is reviewed whole in its cut, so the cap is not the loop editor's. +const MAX_ADMIT_CHANGES: usize = 256; + #[derive(Subcommand)] enum SceneCommand { /// Append a scene revision for one corpus group: its one live speaker's @@ -785,6 +792,22 @@ fn main() -> Result<()> { store.admit_revision(rev, entries).map_err(refused)?; println!("admitted rev:{n} with {count} entries"); } + Command::Inventory(InventoryCommand::Admit { answer }) => { + let text = std::fs::read_to_string(&answer).context("the draft would not read")?; + let draft = EditDraft::parse(&text)?; + let mut store = InventoryStore::open(&cli.inventory)?; + let inventory = store.inventory()?; + let latest = inventory + .latest() + .map(|r| r.n) + .context("the inventory holds no revision")?; + let (rev, entries) = draft + .into_revision(&inventory, latest, MAX_ADMIT_CHANGES) + .context("the draft makes no change or more than the cap")?; + let (n, count) = (rev.n, entries.len()); + store.admit_revision(rev, entries).map_err(refused)?; + println!("admitted rev:{n} with {count} entries"); + } Command::Scene(SceneCommand::Put { group, to, diff --git a/crates/weksa/src/run.rs b/crates/weksa/src/run.rs index 3d0da2a..51be889 100644 --- a/crates/weksa/src/run.rs +++ b/crates/weksa/src/run.rs @@ -40,7 +40,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::context::{EditFeedback, EncoderOffer, Prompt, TrainItems}; use crate::corpus::{CorpusItem, CorpusItemId, ModelFamily, Source}; -use crate::encode::{EditDraft, LoweringDraft, PacketDraft, encoder_id}; +use crate::encode::{EditDraft, LoweringDraft, MAX_EDIT_CHANGES, PacketDraft, encoder_id}; use crate::form::{is_attestation, is_language_tag, is_slug}; use crate::model::{ModelPort, PortError}; use crate::packet::{Packet, Stance}; @@ -2257,7 +2257,7 @@ pub fn cycle( let Some(draft) = answer.as_deref().and_then(|a| EditDraft::parse(a).ok()) else { return reject(run, Why::EditParse); }; - let Some((revision, entries)) = draft.into_revision(&inventory, base_rev) else { + let Some((revision, entries)) = draft.into_revision(&inventory, base_rev, MAX_EDIT_CHANGES) else { return reject(run, Why::EditAdmit); }; let candidate_rev = revision.n; diff --git a/crates/weksa/tests/cli.rs b/crates/weksa/tests/cli.rs index 65563ea..17cabfc 100644 --- a/crates/weksa/tests/cli.rs +++ b/crates/weksa/tests/cli.rs @@ -1875,3 +1875,31 @@ fn scene_put_and_dump() { assert!(!o.status.success()); assert!(!out(&o).contains("scene-1"), "{}", out(&o)); } + +#[test] +fn inventory_admit_numbers_a_draft_and_refuses_an_empty_one() { + let d = tempfile::tempdir().unwrap(); + assert!(weksa(d.path(), &["inventory", "seed"]).status.success()); + let path = |name: &str, text: &str| { + let p = d.path().join(name); + std::fs::write(&p, text).unwrap(); + p.to_str().unwrap().to_owned() + }; + let before = out(&weksa(d.path(), &["inventory", "dump"])); + + for (name, text) in [("empty.json", "{}"), ("junk.json", "not json at all")] { + let refused = weksa(d.path(), &["inventory", "admit", &path(name, text)]); + assert!(!refused.status.success(), "{}", out(&refused)); + assert_eq!(before, out(&weksa(d.path(), &["inventory", "dump"]))); + } + + let draft = path( + "draft.json", + r#"{"add":[{"family":"Concept","gloss":"bath_house"},{"family":"Frame","gloss":"fly_a_kite"}]}"#, + ); + let admitted = weksa(d.path(), &["inventory", "admit", &draft]); + assert!(admitted.status.success(), "{}", out(&admitted)); + assert!(out(&admitted).contains("admitted rev:2 with 2 entries")); + let dump = out(&weksa(d.path(), &["inventory", "dump"])); + assert!(dump.contains("bath_house") && dump.contains("fly_a_kite"), "{dump}"); +} diff --git a/crates/weksa/tests/original_corpus.rs b/crates/weksa/tests/original_corpus.rs index 6a651e7..cbfb177 100644 --- a/crates/weksa/tests/original_corpus.rs +++ b/crates/weksa/tests/original_corpus.rs @@ -240,23 +240,49 @@ fn the_first_six_original_items_are_unchanged() { } } +/// Revision 5 of `data/original/inventory.cc` is the original bed's own +/// lexicon: concepts and frames drafted by claude-sonnet-5-5 (Weksa Hands, +/// model-drafted original-bed lexicon from Train and Validation passages) so an +/// encoder can name baths, kites and persimmons. It is Validation-informed on +/// purpose: numbers on this bed measure the lowering half (register, scene, +/// ontology), never inventory generalization or convergence; scored runs use +/// `data/interlingua`. The Dao De Jing inventory is its revision prefix. +const ORIGINAL_REV5_ADDS: usize = 233; + #[test] -fn the_original_inventory_is_the_dao_inventory() { - let dao = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data/daodejing/inventory.cc"); - let own = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data/original/inventory.cc"); - assert_eq!(std::fs::read(dao).unwrap(), std::fs::read(&own).unwrap()); +fn the_dao_inventory_is_a_prefix_of_the_original_inventory() { + use weksa::inventory::{Change, Family}; + let data = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data"); let tmp = tempfile::tempdir().unwrap(); - let copy = tmp.path().join("inventory.cc"); - std::fs::copy(&own, ©).unwrap(); - let inventory = InventoryStore::open(©).unwrap().inventory().unwrap(); - // Revision 4 is the speaker-state vocabulary: the same six features the - // generator admits, ids and all. - assert_eq!(inventory.latest().unwrap().n, 4); - let (rev, entries) = weksa::inventory::speaker_scene_revision(4); - assert_eq!(inventory.latest().unwrap().changes, rev.changes); - for e in &entries { - assert_eq!(inventory.entry(&e.id), Some(e), "{}", e.gloss); + let open = |name: &str| { + let copy = tmp.path().join(format!("{name}.cc")); + std::fs::copy(data.join(name).join("inventory.cc"), ©).unwrap(); + InventoryStore::open(©).unwrap().inventory().unwrap() + }; + let (dao, own) = (open("daodejing"), open("original")); + assert_eq!(dao.latest().unwrap().n, 4); + let latest = own.latest().unwrap(); + assert_eq!((latest.n, latest.parent), (5, Some(4))); + for n in 1..=4 { + let revision = |inv: &weksa::Inventory| inv.revisions().find(|r| r.n == n).cloned(); + assert_eq!(revision(&dao), revision(&own), "rev {n}"); } + let mut glosses = std::collections::BTreeSet::new(); + for e in dao.entries() { + assert_eq!(own.entry(&e.id), Some(e), "{}", e.gloss); + glosses.insert(e.gloss.clone()); + } + assert_eq!(latest.changes.len(), ORIGINAL_REV5_ADDS); + for change in &latest.changes { + let Change::Add(id) = change else { + panic!("rev 5 retires {change:?}"); + }; + let e = own.entry(id).unwrap(); + assert!(matches!(id.family(), Family::Concept | Family::Frame), "{id}"); + assert_eq!(e.introduced_rev, 5, "{id}"); + assert!(glosses.insert(e.gloss.clone()), "{} repeats a gloss", e.gloss); + } + assert_eq!(own.entries().count(), dao.entries().count() + ORIGINAL_REV5_ADDS); } /// The committed seed ontology: every entry model-drafted, exactly the two @@ -889,135 +915,6 @@ fn the_eight_ja_test_sentences_are_pinned_verbatim() { } } -/// The concepts the pinned inventory holds, as each language names them in the -/// passages: English by word stem, Japanese by a multi-character surface. This -/// is a tripwire, not the encoder: it fails a passage that names no live -/// concept, and `FALSE_FRIENDS` strips the idioms that hold a concept's -/// character without its meaning (天気, 心配, 腹が立つ ...) before matching. -const LEXICON: [(&str, &[&str], &[&str]); 21] = [ - ("endanger", &["endanger"], &["危う", "危ない"]), - ("prevent", &["prevent"], &["防い", "防げ", "妨げ"]), - ("control", &["control"], &["操ろ", "操る", "操れ", "支配"]), - ( - "request", - &["request"], - &["いただけませんか", "もらえますか", "くれませんか", "お願い"], - ), - ("sage", &["sage"], &["賢者"]), - ("mind", &["mind"], &["心が", "心は", "心を", "心の", "心も"]), - ( - "belly", - &["belly", "bellies"], - &["お腹", "空腹", "満腹", "腹の虫"], - ), - ("will", &["will"], &["意志"]), - ("bone", &["bone"], &["骨が", "骨を", "骨は", "骨の"]), - ("empty", &["empt"], &["空っぽ", "空にし", "空の器"]), - ("fill", &["fill"], &["満た", "いっぱい"]), - ("weaken", &["weaken"], &["弱ら", "弱め", "弱っ"]), - ("strengthen", &["strengthen"], &["強くな", "強め", "強くし"]), - ( - "heaven", - &["heaven"], - &["天が", "天を", "天は", "天上", "天の"], - ), - ("earth", &["earth"], &["大地"]), - ("water", &["water"], &["水が", "水を", "水は", "水面"]), - ("compare", &["compare"], &["比べ"]), - ("resemble", &["resemble"], &["似て", "似た", "似る"]), - ("continues_long", &[], &["長く続", "久しく続"]), - ("true_words", &[], &["誠の言葉", "真実の言葉"]), - ("seem_paradoxical", &[], &["逆さま"]), -]; - -/// Compounds and idioms that contain a surface above without naming its concept. -const FALSE_FRIENDS: [&str; 8] = [ - "天気", - "心配", - "腹が立", - "腹を立", - "水曜", - "骨が折れ", - "天の川", - "天井", -]; - -/// The live concepts a passage names, by `LEXICON`, in `lang_col` (0 en-US, 1 ja-JP). -fn concepts_in(text: &str, col: usize) -> Vec<&'static str> { - let mut text = text.to_lowercase(); - for f in FALSE_FRIENDS { - text = text.replace(f, ""); - } - let words: Vec<&str> = text.split(|c: char| !c.is_alphabetic()).collect(); - LEXICON - .iter() - .filter(|(_, en, ja)| { - if col == 0 { - en.iter().any(|s| words.iter().any(|w| w.starts_with(s))) - } else { - ja.iter().any(|s| text.contains(s)) - } - }) - .map(|(c, _, _)| *c) - .collect() -} - -#[test] -fn the_concept_lexicon_does_not_match_a_word_that_merely_holds_a_character() { - for ja in [ - "今日の天気はどうだろう。", - "試験が心配でならない。", - "腹が立って仕方がない。", - "水曜に会いましょう。", - "骨が折れる仕事だ。", - ] { - assert!(concepts_in(ja, 1).is_empty(), "{ja}"); - } - assert!(concepts_in("I attempt it, with a reminder.", 0).is_empty()); - assert_eq!(concepts_in("水が澄んでいる。", 1), ["water"]); -} - -#[test] -fn every_original_passage_rests_on_a_live_inventory_concept() { - use weksa::inventory::Family; - let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data/original"); - let tmp = tempfile::tempdir().unwrap(); - std::fs::copy(dir.join("inventory.cc"), tmp.path().join("inventory.cc")).unwrap(); - let inventory = InventoryStore::open(&tmp.path().join("inventory.cc")) - .unwrap() - .inventory() - .unwrap(); - let rev = inventory.latest().unwrap().n; - let live: Vec<&str> = inventory - .entries() - .filter(|e| e.family() == Family::Concept && e.live_at(rev)) - .map(|e| e.gloss.as_str()) - .collect(); - for (concept, _, _) in LEXICON { - assert!(live.contains(&concept), "{concept} is not a live concept"); - } - let (corpus, _) = stores(); - for (col, lang) in PANEL.iter().enumerate() { - let mut hit = std::collections::BTreeSet::new(); - let mut passages = 0; - for item in corpus.all().unwrap() { - if item.language != *lang || !matches!(item.source, Source::ModelWritten { .. }) { - continue; - } - passages += 1; - let found = concepts_in(&item.text, col); - assert!(!found.is_empty(), "{} rests on no live concept", item.id); - hit.extend(found); - } - eprintln!( - "{lang}: {passages} passages on a live concept, {} distinct concepts", - hit.len() - ); - assert_eq!(passages, 28); - assert!(hit.len() >= 12, "{lang}: {} distinct concepts", hit.len()); - } -} - /// The committed scenes: one per live Train or Validation group of /// `data/original`, none for any other group, every one model-annotated and /// never Test evidence (a Test group gets no scene and reads `NoScene`). @@ -1105,6 +1002,16 @@ fn scene_entries_never_co_select_in_a_layer() { ); } } + // An above, out-group speaker is told by exactly one ja-JP + // Scene-conditioned grammar entry, at every intimacy. + if (st, ig) == ("above", false) { + let told = entries + .iter() + .filter(|e| e.language == "ja-JP" && e.layer.name() == "grammar") + .filter(|e| e.when.iter().any(|c| c.slot == Slot::Scene) && selects(e, rel)) + .count(); + assert_eq!(told, 1, "{rel:?}"); + } } } } diff --git a/data/ontology/ontology.cc b/data/ontology/ontology.cc index e3feabb443378f00ff9b82ae2b592b00666a0f0e..d2e0911754f1fd10dd3575d323abb50c34b69b03 100644 GIT binary patch delta 391 zcmXAj(JMny9LJl7jZ!FyB8REHm?%<1-aRRAUO2hO?%Cy>betqZ4FWrGEgL(sX3Y+=b@Xu4hFP%dE$Uiyo%atIRQa7M(>xGv;@*313 ss0&cXptb|050nOV3He=9@1I@%fAC%_UUkoa`n)ryi{a^~rRY}kFAI>f`~Uy| delta 17 Zcmdmy@+^MB6GrKcPwSL6Z&hvP0RU0K2t)t? diff --git a/data/original/inventory.cc b/data/original/inventory.cc index 3904b809b75c3558a065b8043c36df0a79b8aeaf..75fd7ca93ab9f2d6dfb522cfb0e469769f487de0 100644 GIT binary patch literal 34658 zcmbVVTaV<%b=J+#ffGr)TCF5&?Km&7+$`P~2m%8^{FVfek_QC|!|CSq(A(W?lif2K z8-DOho+WubMqV@BJ>4@qs|%DMK!9LjNI$}W{y_djkW;K8>(pV-a?b*WJmeNXepPk8 zbLw(f{hKd`%gJJx4v(_;szp&|?;Sgz{_a0cvIkZAUOqd{W^nT~f&VK{_uu`my=4Jr zfAK&3^DpmJ@V06CH|aFHcmLkr$M5~QF7yvsS>?s-rgHEaC_2ri2T<*(jMyGoDV18KvrSpe?HI>M*a4D4xf`dlOUjb`mc+ zD7x+Gb3kulP=7t2q}>J$IDl5IgVjNC0%l})c*w!~9wrz*F7sgqwLY07qdn~zagblb z$kF-FQc#agH0HorEg7#5(kfT>V^q~=>6X5+935y!+0m999hMw%zc_={NhalORoCgG zX9?#G)OwC{F-d2OtfYc*SX#6wiq&yiQEfXc0pcEU-1Xsd(NVO+Qs8Y|XK!LxfGKN711!jrTYKVAym{p7XI8~N!blGR6mM&jMVN$HK>3jiw zWeHQTgo*1e2G|n?{V_z1*`jM- zf~8j6Rl?oQ(|j)`OR(6Z2yZnhicXWVMEHInQz9ZQlpRLX%?XXNU#6p3r? z4j}Z7sAOE05G_1sHOUXDQ*&7o#6u!iJU+_Oa*-Tn84Rw+6){J*uOTgQWBYd9bv4Z= z9jYt=T7cgAxPajsWyn$>9t6AoPFha2$mX4Dvs7rY4Axb#?8wk%$q)||+;DTWRC6A7QP6?o9W1O{4kX0bj?nO5B+*`B8m?diR1u%{d%)5q?)Siv{SNh}D@y@O zn7E4qv0W6!Ud-oKW}uXrmUPV`U?~&(_`m}npMd8~s_u$Kz!E36`oNb~KPi?Yxw3B! zb%H))vJ`J|4J213r7kQ>p*SiB;iDsP$qDQhEMUMx>9RzM$Kye?1~qk}N5E1jjvhh0 zsj|hQ6FB>7ZmAP50)=Zi0fV)DYn&Uh#EFM>;b2wfou$u^B}?3bp|gf{iXq*62%lS& zq1XHdiL?P6!|V1%0{2(6}U~9bHTpovaDAoEpbLHapJa(2J0aV7--Op zSn9;?C~`Kl{GdDMidgc*mLG`~>`c(!k66;g0V8s+wc%3U*a`npAUGdm!EJ*)mi5Br!8)$r5|# zc<=~TE?^0#-Vvv(fiX*+xbtIYH-|2Q4UW#bWy})j50USS#aA!_Nk+xVOgg_qnA9v_ zMT=NL%n~eCLF{hpAye0~VwPI5-;2F9Bpf}UK`LfR6a$TxskclQLxxd&>c83$!CV5uJ& zCp@`6m~F^9vvN*{hXx9{;yaq2QqiF(}b#&)k z44zl_@Z`2yNXB%WCI&3sBLTpZn=*sYN8+g%u!N67A3S+g93H~J4DuZ4;r))+gXaVBTbD^SN`41mqflQIcV!c;STah)u?EA$^j@48hAavhci>-0z?jW=Rl*3*q?Nu^fPOmcWF=(x1gKSWUADbrTLt zc4)UxIN|4NnMRG1Ah?m$WZ|$Rh!x{R=bgMs;XJnho%nolq6YCsfPLY?5+K@#5#n-D zE@3mGC`T}kwN%&}eysrlHgPe4C4n-dCe-UDZU%JLFg~Ema)OsjgaeHbn_1CKwGg}{ zBCF2sIvsZ-fndc#G;|Zf+tr&d$tm3|5rU;ftQ+C4iW!|&3&GN%)sDd?ot&hnWDX?6 zb1P9mvR^J2Op}9C*vf5}PTS2=Gmu7_9)pd4y52_2= z=b0?cTQtQ+Zee#3``Xk(MALTRx{w1l&5mGQb&oS~SxU8-dYcULue(fHGDUe-!u21^ zt(_#Tl7kGai+WO*C0OjH++dv^z{&-Q?!1H}3ou+m{u15V5xjgOtS2|Rx^bC8xU8^g zby5u5_*Jv#ZWP+r>y|=)jOLi$|5S5Wr-2SyoMhQ7QJba-^npaUjXbnw8+? z9$78mU6mdSqvNpV64 zxx8c}EK7f|J^>4&o|Knpgy%`Vvz4J~x}=+@f|qK9HR%fod9a@i%Sz)?k0VWM_F3eO ztr=Xv3=zgAPB3K zTlmamG)Z+fzCrABK^7viNY~#;#pumkgV!fQ3lFWG!AQ;+qk-#_paq8;F}#$Uqp+g+ zpkqBO5#mOSwqwZ4%Vr%!mJD&F<)@Af!B&BNoop!(?K^nj#^B@i0t^vLkT+CA_4Nj@ zBo1Dp{xljAsM=oPjny!svpt3c%(RQhk{(7Bkb7Uj9>W2Nt-Jvm+HLXNFOP~vasUIW zBt3-H+%d#i+yC%JYA9|)k~mD&K!_;;WU26uacG{ulVtU(v(-l|AtGny!FR_{cS4Y* zLtNwVLZU5}2LzBMLJSyP$gRpwDC7;yaDC%d1-VHzy;g#K{b5P)4HE?YU3HYhK1#jd zibcp9m>~oAL4-iBT^?>l$QzlV3j5AVepO-j2X9PR z?J;Uz`Zun#2b&6d5!|$3UoTq(k=yqDHRK&sYQ5gTu@v{J5wRbfR}YB$@>zmILmWT6 zuI4gpE2&@=hYlck`Q9kp`_U#{E|ybjHmt~p@!}11NWE7EUaQqzUlUroL!mFYD6^kK zVhq`G=OuZgRtyH$s`+4-Sq4B`EW9jlG%5+;OXB4W=DzSfF|i(Anm2NYfw-6!rTj>W zkYvdZS$E*B>rc}OT;Hg~5(~2Kz&md~n{K7T%j!n10g}2;p_j9=3)yEn7C+=*g8;JR zp#@7CjoeJ+OE;rdwOcccSjs~VAJVZUS~6+Uu?-|k zb#Ix$YZN}3r!X5!sv)FEkl|s}XWo{UaVth^nAYplv{IG$?*YbN7nJ`?FwS!$B zs-?l#Ot6E+8(4&gfjbr?jtyI=gdJ6eK$H60eb=$L?wm8Y=)|f*u%pI!cb3%r8@C)}#+PirC*kOnpFi z9e7FCxNCxIg$H|#WoJo|mvxP4h8y`QdWR-UiD-HeM;Cg@&K`#G@~zQ#3~~IpKA%cX zQICqH{tRw+2x+p*GUFZN_?Ktqx$V4|q-DN1{q@D)&gG76^(owf4k*o!bS-Tf-xZrqMEuI42CuNcEWQp}zL-xr51_V#mRaC850wg>GR3@{UPkYyaJ0xjRaX5_^ELjV!g;_Ro1@k;YyA<!iGlMT@hO*lrO=dRM#&_|){N67sp_9zZ`W%Y& zvOhyFb9dQjsXX@6@cVpm-@Ak*3kbqTaKvA03%^ynk(ZFJ2%QfWx45d$x=7Ly z93s6||Ah&AR(^tN+nQuwWi6@wi!n)elft)>L&W0}#@4mhIL;pBQ*&`OFAKKFMyu9tS7{RyOD7v0);6x^P=MI)Kp?UIjBExN!!`HaG zSvCY_X?c1vf+RJFxVz#2&M|^-BStq;V|nfO4K@E74!4F)S;+wNd$<|Fn}-XK!C_Vc z2gu2gr#NP#3)nDRPM<=B<>Cj1O0Yi4Mq}-QKi&ORUcu+ia40DmbhN#}^7Wp1Pq-Vx zQCqM@ve1s>8Q#Tm{y#SFiq<)NVtHEokY{*JUl6bi>|OJkcwO%^ccRzDVF^KvH!8e) z);#t2DE);z3KeGfYJnWK^ncAI^C)&x8gW9HPTCh*W7U-Q8G`NDDBzewld!C=RroJArO#4n_ zgOEikx4Go!pmiI91!}y$?y&IgHF)k5b&ZAl-3He?2X|gCWoUYRePY41 z)%9g{VZA}kyD(l~cUX9@H?)H7B!_blA(~(;mg9VkHc|*$tZ#9tduKr|x3MNyl1ct{T7`TG-9%du1FQiXzz<<^I z!Z1nwoFMo4M5~YPo^>9VpEXI+d!YN=qXqp=2in=D2XYD`Z4}T+rC8E@^Nc>du7}C{ zfLR)Sn*zJrq8z921w1TPcS*B!+9B=9pBs?hadv@O%I%Q$w{Rd+4kya&ZE2Q@-=z%_ zz_$?a71&~_d|{V5OW1Ew`0xT|B$HwcOR`;Pmb^R79j##|B?C+sxKHGkW#6To;|=_z z3;ga(jeMyl!o`d$a`YhN(rZ;~>$ozYEPX>VH+pgl(_$%Z@Wapq=wDdEh6H8#)o zj}9J7%NCvhJh|~=^J7UEGU5$CcP$UC>HxBIYXN%e84PASfGoYju^on?5B;u}D>|qw zrNY-*1~sS;nkiu|q=I8T<<1zisIf3s2f<2m3k>IC$iuefZk7ee>LFO{y-? zcG_CJa9wA)5_&9eiRne~nIkOfD|0k&VL3@`H1S0?OJPkx0d63>nK>K$<1_sbe9+ts zPShQiA-}3&q!G)rzLj+-AeI5&)SM!|aVRjB>E6;{-lYtz@S_(tF_y{x ziN^9baE3cfachfEoLI*CU5yj0#&x;_n5jZxndoa8CEV76tmjTG63aT92A4-k&$7dO zn8U(UUAafF)|SBs)G*fJ1>85ztVdzd4l>Y{Mc{v68QALcL_;EC9vMlqi#WLEZu!UKSo92XrN00B{{Ofyre~0dym;dkKi%-6N;V*uD{*%#&%yXQ5 zH^si2?!KGezMKBOo58-D;l7*EzMFAh}P3(%)TkswAE__G55#Leo#dp-(@g2nh-%(8P9mS_zNLOUqOU0zU zR5aR4#i6}a1lmjW_V!Y}y1gXtRlVKraXG59&9&lXt`#wJt(ci>Ma^6*ZsuB%GuPT0 zn>{}#`u)S&Yls_vGjp%k`1)TTzVzb1)rQ(8Ch8`I9lrY4HFc9Vm%2%tOWmZ+rEb#Z zQnA)cVhUGZC{%snYVt0R{5LQD_TkGfefz>4o4@Y3hJ)_7=2CZDbJ=uUd#yXJz1AJq zUh9r)uXV?@*Sh1{Yu$0}weGm~TG4BhlpMlOXv1pC3&KwjeuD55gr6Y%1mPzXKix6v zVSS;V)E8FITF*3===FD|Tm)@*u(OA+ynI&oq_J|MvZgT_t8W^kz1EGUr6|cgkMPbg@j*7_=SXDNccsBUqtvtgkMDXMTB2O_(g_{D@@O!&owUrhMLgkP-qwI{SV14R(sPz=!xMG@Ul9MKI$65UWN(G5it z-B3Ky4Mh~)piFgCHzgd()S*lr%G9Au9m>?9OdZP9p-gpnH^g*ELpLZ>9pO<4%2daA zRDv?qQ680`Oa*1CqlK?jwfZK}f#DnZ*+2Od;{wy6$1 zs03|O9ehj)9evOZ+NL`Gpc1rAbp%2sXq)O7gi6ph)ltZl&|wJOplzxH5h_93REHu| zg0`s+MyLdBQyq>_3EHMQAej<6BB2|!O?6B{C1{)KsDw(;Hq~(nm7r~^BayC_!VC&~qSzL1Pr0rAc_u7zJlo(h`)Zo(ZA1P^Nk= wWJ=H&C1{KidOCz2p-lCJ2$i5r^^^#epuMjrMW_U|QyQa$o)?)%eDVAL2m6wr@&Et; delta 227 zcmaFV$MiB{!(~PuhO8+olk@YEQwvJ0CI{*%Ob%e;oy^0O!LVt ztR^opxmGEH9cuIBf;|1nacZTL6}0>(FVqd0Y-t!Z`L;>M`, an opaque digest of item, inventory revision and encoder (the item key lives in `provenance.corpus_item`, which the lowerer never sees); CultCache type `weksa.interlingua_packet`, carrying `schema_version` and `inventory_rev`. Every id inside is an opaque form type (`n` nodes, `encoder:<16 hex>`, canonical `:`). From cut `speaker-state` (schema v4) it also carries the utterance **stance**: the speaker's felt affect and restraint, as inventory features of slot `Stance` (see Speaker state below) | Minted by an encoder for one corpus item under one spec version; immutable; a new spec version means a new packet, never an edit. Admitted only through `PacketStore::admit`, which validates against the inventory store at the packet's revision | The encoder writes; the form types refuse malformed ids at construction and load, the validator refuses broken references and over-capacity packets: a feature at most once per slot, at most 64 nodes (`typed-packet`); nothing else writes packets | | Schema version | `weksa.interlingua_packet.v` constant in `crates/weksa`; published JSON Schema at `schemas/cultnet/` | Bumped by a commit that changes the Rust types; the published schema is regenerated in the same commit and a test fails on drift | A cut (Hands, under a spec); never the loop | -| Inventory entry | Key `:`, `n` canonical decimal >= 1 (families: concept, frame, role, relation, speech_act, feature, referent_kind, extension); type `weksa.inventory_entry`, with a bounded gloss, value kind (option labels bounded), the slots a feature may sit in (`Discourse`, `Referent`, `Predication`, `Relation`; `Stance` from cut `speaker-state`, `Speaker` and `Scene` from cut `speaker-scene`), `introduced_rev` and optional `retired_rev`. Speaker axes, scene relations and affects are features of those slots, so the loop grows them like any closed-class family and convergence covers them | Added or retired by an inventory revision; never edited in place (a changed meaning is a retire plus an add); a retired id still resolves for packets of earlier revisions | The loop's accepted rounds, whose minting call the context builder assembles from Train items only; Self on merge to main. Admission checks form, not text | +| Inventory entry | Key `:`, `n` canonical decimal >= 1 (families: concept, frame, role, relation, speech_act, feature, referent_kind, extension); type `weksa.inventory_entry`, with a bounded gloss, value kind (option labels bounded), the slots a feature may sit in (`Discourse`, `Referent`, `Predication`, `Relation`; `Stance` from cut `speaker-state`, `Speaker` and `Scene` from cut `speaker-scene`), `introduced_rev` and optional `retired_rev`. Speaker axes, scene relations and affects are features of those slots, so the loop grows them like any closed-class family and convergence covers them | Added or retired by an inventory revision; never edited in place (a changed meaning is a retire plus an add); a retired id still resolves for packets of earlier revisions | The loop's accepted rounds, whose minting call the context builder assembles from Train items only; `weksa inventory admit` for one model-drafted `EditDraft` outside a run (cap 256, reviewed whole in its cut; same numbering and admission check as the loop); Self on merge to main. `data/original` rev 5 (233 concepts and frames) was drafted by claude-sonnet-5-5 (Weksa Hands, model-drafted original-bed lexicon from Train and Validation passages); Validation-informed: numbers on this bed measure lowering, never generalization or convergence. Admission checks form, not text | | Inventory revision | Key `rev:`, type `weksa.inventory_revision`, with parent, change list and `frozen` | Appended per accepted round; exactly one revision is ever marked frozen, by the convergence test | The loop's accept rule appends; the convergence test freezes; Self records the freeze as a ruling | | Target profile | Locale or language-project id (`en-US`, `ja-JP`, a conlang's slug). Until entries grow in rounds there is no profile revision document: a run names the profile it lowered with by the ontology store's path and digest in its `RunRecord` (cut `language-ontology`); `profile::` revision documents arrive with follow-up `language-ontology-growth` | The ontology store is revised by commit; a run pins it and refuses drift; it likewise pins the speaker-state store (`RunRecord` keys 17 and 18, `Drift(SpeakerState)`, cut `language-ontology-scene`) | Profile authors and accepted rounds; never the lowerer's defaults (`persona-drives-register`) | | Language ontology entry (cut `language-ontology`) | Key `onto::`, `n` from 1 per language; type `weksa.ontology_entry` in `data/ontology/ontology.cc`: the language, the revision of the inventory it was admitted through (provenance: beds number revisions independently and a run reads each condition's feature at its own head), a layer (lexicon, grammar, discourse, phonology, orthography), at most four conditions (a feature id of slot `Stance`, `Discourse` or `Scene` with `AtLeast`/`AtMost` a grade or `Is` an option or flag; a `Scene` condition reads the relation of the item's speaker to its addressee that `with_scene` selected; slot `Speaker` and community ids wait on follow-up `language-ontology-communities`), a choice of 1 to 200 bytes, at most three examples of 1 to 120 bytes, and an `Annotator`. Whether an entry's text keeps politeness on the scene is a model reader's judgment, owed per change (a mint owes it under `language-ontology-growth`), not a test | Admitted in all-or-nothing batches through `OntologyStore::admit`, immutable, never edited; retire arrives with `language-ontology-growth`. The run pins the store like the entity store (path and digest, `Drift(Ontology)`), never copies it, because no loop step writes it yet. Cut `language-ontology-seed` converts by drafting, not by parse: a fresh model drafts each language's seed from that language's YAML and the inventory's feature lines with no corpus text, then deletes `data/target-language-ontology/` and `.weksa/cultural-ontology/` in the same cut | Seed: a labelled model (`Annotator::Model`), no operator gate, never Test evidence (`ja-ontology-owner-answer`); attestation (operator, native reader) appends later. Accepted rounds later, through a mint call from Train items only. Selection is `ontology::select(ontology, language, view)`, called only inside `Prompt::lower`, so no caller picks entries; the prompt prints each selected entry's id, layer, choice and examples. Its text is minted text for the leak diagnostic, which today has no runtime caller (follow-up `leak-diagnostic-in-round`) | @@ -320,8 +320,8 @@ line two", "その 制御" against the features reach a loop's lowering view. `score::leak_diagnostic` and `TestReadings::at_checkpoint` take minted text as `&[&str]` and have no caller outside tests: no round reads glosses or any minted text against - Validation. `data/original/inventory.cc` and `data/daodejing/inventory.cc` are - byte-identical; with `data/interlingua/inventory.cc` they share revision 1, + Validation. `data/daodejing/inventory.cc` is a revision prefix of + `data/original/inventory.cc` (revisions 1 to 4 equal, a test proves it; rev 5 adds the bed's own lexicon); with `data/interlingua/inventory.cc` they share revision 1, where features 6 to 8 (hedge, certainty grades, irony flag) sit in `Discourse` and `Predication` and features 9 to 18 (anger to restraint, grades) in `Stance`. `PROMPT_SOURCE_DIGEST` covers `context.rs`, `split.rs`,