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 e3feabb..d2e0911 100644 Binary files a/data/ontology/ontology.cc and b/data/ontology/ontology.cc differ diff --git a/data/original/inventory.cc b/data/original/inventory.cc index 3904b80..75fd7ca 100644 Binary files a/data/original/inventory.cc and b/data/original/inventory.cc differ diff --git a/docs/targets/interlingua-lowering-map.md b/docs/targets/interlingua-lowering-map.md index b32c83c..7301adc 100644 --- a/docs/targets/interlingua-lowering-map.md +++ b/docs/targets/interlingua-lowering-map.md @@ -31,7 +31,7 @@ means no store holds it and nothing writes it. |---|---|---|---| | Packet | Key `packet:<16 hex>`, 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`,