From 0e4f4949163040c487b88491331e645074b2526a Mon Sep 17 00:00:00 2001 From: Weksa Date: Thu, 12 Nov 2026 19:27:57 +0000 Subject: [PATCH 1/5] ja-seed-pairs: rebuild the ontology store with certainty and restraint conditions on ja-JP :5, :7, :8 Same keys, choice and examples; only when and annotator change. Every other entry dumps byte-identical. Co-Authored-By: Claude Sonnet 5.5 --- data/ontology/ontology.cc | Bin 11710 -> 11917 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/ontology/ontology.cc b/data/ontology/ontology.cc index 5e7919a7d0786dd5b84ccb75ef2b01542728a879..3b2f70c380697a393ba4a2c525e5c1a683deb146 100644 GIT binary patch delta 505 zcmdlN-5a|>l##{K%E)}PB%>>omBi%E1QA=y5(H&2v-3h(BAkp+mL#_#REHf8CsfRv zF90ewpZ^4ul`ZH4W$hL!hO&%Au0UB_V(X!-z2fCimW8AY)Vv630Vpd+_AXS`S6&t> zmZ#_i72Biq0LogRlFTR%@{yIPNz_rsTQinsCF*(wurjW6$t+IJFD)ugoiuqDzcCOi z3Ysq|Ni8mET;^Efn_paVmRWvtsG24tqxs~Ij0&8|`FSasC7Jnoll66^Cm&E(h6KuU z^;kw(gazxM7O*oe_erhD1ZtW*IZ@MavV^wUnG{a8%4gE+rX$CnWR5U!a) delta 357 zcmeB;-50$>omBi%E1QA=y5(H&2v-3h(BAkp+mL#_#REHf8CsfRv zF90ewpZ^4ul`ZH4W$hL!hO&%Au0UB_V(X!-z2fCimW8AY)Vv630Vpd+_AXS`S6&t> zmZ#_i72Biq0LogRlFTRs@{yIPNz_rsr8AahCF*(wOkT_HxVb}3gK_daRj0{c)s-PW zl+lP~6h^4_gsNtr>?mS0Sxf5zShc;jvOGdf7E}!f<4Tvz;^h3&qT&plLPy}LH -- 2.47.3 From a2707af237bc65547416f9d2d30f4c551bed6094 Mon Sep 17 00:00:00 2001 From: Weksa Date: Thu, 12 Nov 2026 19:28:30 +0000 Subject: [PATCH 2/5] ja-seed-pairs: pair-disjointness test, generator whitelist, docs Co-Authored-By: Claude Sonnet 5.5 --- README.md | 1 + crates/weksa/tests/original_corpus.rs | 51 ++++++++++++++++++++++++ docs/targets/interlingua-lowering-map.md | 2 +- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6702748..be5f9b5 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ addressee, and none for an above-status in-group addressee below intimacy 700 (` an in-group addressee at intimacy 700 or more, whatever the status). Two entries for the above-status cases were withdrawn: a paired measure found they made the lowerer match the reference register less often, and they stay out until a native reader attests them. +ja-JP `:5` (hedging), `:7` (affection) and `:8` (restrained sadness) carry certainty and restraint conditions, so the hedging, certainty, affection, restraint and sadness entries never co-select on the pairs `:5`/`:6`, `:4`/`:7`, `:6`/`:7` and `:6`/`:8`; the store has no retire, so that was a rebuild of `data/ontology/ontology.cc` with the ja-JP text unchanged. For the first AquaSynth speech-training handoff, see [Tiny PanPhon v0.1 Speech Curriculum](./examples/speech-training/tiny-panphon-v0.1/README.md). diff --git a/crates/weksa/tests/original_corpus.rs b/crates/weksa/tests/original_corpus.rs index 75b44e9..52955ea 100644 --- a/crates/weksa/tests/original_corpus.rs +++ b/crates/weksa/tests/original_corpus.rs @@ -313,6 +313,7 @@ fn committed_ontology_is_model_drafted_and_reads_every_inventory() { "claude-sonnet-5-5 (Weksa Hands, model-drafted seed ontology)", "claude-sonnet-5-5 (Weksa Hands, model-drafted scene ontology)", "claude-sonnet-5-5 (Weksa Hands, reworded seed entry)", + "claude-sonnet-5-5 (Weksa Hands, conditioned seed entry)", ] .contains(&generator.as_str()), "{}", @@ -960,6 +961,56 @@ fn committed_scenes_cover_original_groups() { } } +/// ja-JP seed entries :4 to :8 over the stance and discourse grades that bear on +/// them (hedge, certainty, sadness, affection, restraint): the pairs that gave +/// opposed instructions (hedge against certainty, particles, final force) never +/// hold together. Fails if :5, :7 or :8 loses a conditioning clause. +#[test] +fn ja_seed_pairs_never_co_select() { + use weksa::inventory::Slot; + use weksa::ontology::{Condition, Test}; + use weksa::OntologyStore; + let data = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data"); + let tmp = tempfile::tempdir().unwrap(); + let onto = tmp.path().join("ontology.cc"); + std::fs::copy(data.join("ontology/ontology.cc"), &onto).unwrap(); + let inv = InventoryStore::open(&data.join("original/inventory.cc")).unwrap().inventory().unwrap(); + let id = |gloss: &str| inv.entries().find(|e| e.gloss == gloss).unwrap().id; + let feats = [id("hedge"), id("certainty"), id("sadness"), id("affection"), id("restraint")]; + let entries = OntologyStore::open(&onto).unwrap().all().unwrap(); + let ja = |n: u32| { + entries + .iter() + .find(|e| e.language == "ja-JP" && e.n == n) + .unwrap_or_else(|| panic!("onto:ja-JP:{n} is absent")) + }; + let holds = |e: &weksa::OntologyEntry, point: &[u16; 5]| { + e.when.iter().all(|c: &Condition| { + assert!(matches!(c.slot, Slot::Stance | Slot::Discourse), "{}", e.key()); + let at = feats + .iter() + .position(|f| *f == c.feature) + .unwrap_or_else(|| panic!("{} reads a feature this test does not sweep", e.key())); + match &c.test { + Test::AtLeast(g) => point[at] >= g.get(), + Test::AtMost(g) => point[at] <= g.get(), + other => panic!("{} has {other:?}", e.key()), + } + }) + }; + let grid = [0u16, 300, 500, 599, 600, 700, 799, 800, 1000]; + let pairs = [(5, 6), (4, 7), (6, 7), (6, 8)]; + for p in 0..grid.len().pow(5) { + let point: [u16; 5] = std::array::from_fn(|i| grid[p / grid.len().pow(i as u32) % grid.len()]); + for (a, b) in pairs { + assert!( + !(holds(ja(a), &point) && holds(ja(b), &point)), + "onto:ja-JP:{a} and :{b} co-select at (hedge, certainty, sadness, affection, restraint) = {point:?}" + ); + } + } +} + /// The committed ontology's Scene-conditioned entries, over every scene relation /// (relative_status x in_group x intimacy in steps of 50): two entries of one /// language and layer are never selected by the same relation, so the ontology diff --git a/docs/targets/interlingua-lowering-map.md b/docs/targets/interlingua-lowering-map.md index 46c1435..3d5bad3 100644 --- a/docs/targets/interlingua-lowering-map.md +++ b/docs/targets/interlingua-lowering-map.md @@ -34,7 +34,7 @@ means no store holds it and nothing writes it. | 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` revs 5 (233 concepts and frames) and 6 (five concepts closing named gaps) were 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. Residue after rev 6, not amended (each fresh coverage reader names new gaps; the loop's growth path owns them): simultaneity (ながら), distal deixis (あの), 'first' (まず), counterfactual mood, 'at least', emphatic question; heaven versus sky for the visible 天 stays weak. 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`). The store was rebuilt without ja-JP `:14` and `:15` (above-status Scene entries; a paired measure found them net-negative on register, follow-up `ja-scene-entries-lower-register-agreement`), so ja-JP ends at `:13`: an above-status out-group addressee, and an above-status in-group addressee below intimacy 700, select no ja-JP Scene entry (`:12` still selects in-group at intimacy 700 or more regardless of status) until a native reader attests a replacement (`ja-native-reader-scenes`) | +| 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`). The store was rebuilt without ja-JP `:14` and `:15` (above-status Scene entries; a paired measure found them net-negative on register, follow-up `ja-scene-entries-lower-register-agreement`), so ja-JP ends at `:13`: an above-status out-group addressee, and an above-status in-group addressee below intimacy 700, select no ja-JP Scene entry (`:12` still selects in-group at intimacy 700 or more regardless of status) until a native reader attests a replacement (`ja-native-reader-scenes`). ja-JP `:5`, `:7` and `:8` were revised by certainty and restraint conditions in a store rebuild (the store has no retire, so a conditions change is a rebuild), which makes ja-JP's hedging, certainty, affection, restraint and sadness entries disjoint on `:5`/`:6`, `:4`/`:7`, `:6`/`:7`, `:6`/`:8` | | Community (cut `language-ontology`, follow-up `language-ontology-communities`) | Key `community:`; type `weksa.community`, converted from `data/cultural-ontology/cultures/` and `referents/` (YAML deleted in the same cut) | Revised by commit; a speaker names the communities it belongs to by id | The cultural ontology's author (owner-attested evidence only; Discord-only content stays deleted, `discord-only-content-deleted`) | | Speaker (cut `speaker-scene`) | Key `speaker:@`; type `weksa.speaker`: stable axes as feature values of slot `Speaker` (origin, socioeconomic standing, age cohort, education and whatever the loop adds), at an inventory revision, and an `Annotator` | A new revision appends, never edits; a corpus item and every prior turn name a speaker slug, and a round records the speaker revisions it read | The annotator it names: `PersonaSnapshot` (a projection of a `gamecult.persona_state.v0` document, naming its source document and `updatedAt`), `Operator`, `WorkMetadata` (a public-domain work's own cast list and narration), or `Model { family, generator }` (the 2b `ModelFamily`) | | Scene (cut `speaker-scene`) | Key `scene:@`, the item's split group; type `weksa.scene`: participants (speaker slugs) and, per ordered pair, relation features of slot `Scene` (relative status, intimacy, in-group, audience and whatever the loop adds), at an inventory revision (provenance; a run reads the features at its own head), and an `Annotator` | As Speaker. Keyed by the group, so a scene never straddles the split. `data/original/speaker-state.cc` holds Model-annotated keeper scenes, one per Train or Validation group of both languages (`weksa scene put|dump`); a Test group has none and reads `NoScene` | As Speaker; a model-annotated scene is never Test evidence | -- 2.47.3 From dc4f95558223fd10cd7faa69cf65585ce6a6fd2b Mon Sep 17 00:00:00 2001 From: Weksa Date: Thu, 12 Nov 2026 19:34:07 +0000 Subject: [PATCH 3/5] ontology: AtMostOrUnset test; ja-JP :5, :7, :8 bounds use it so an unrated certainty or restraint no longer gates them Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- crates/weksa/src/ontology.rs | 39 ++++++++++++++++++++--- crates/weksa/src/store.rs | 2 +- data/ontology/ontology.cc | Bin 11917 -> 11945 bytes docs/targets/interlingua-lowering-map.md | 2 +- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index be5f9b5..3b02136 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ addressee, and none for an above-status in-group addressee below intimacy 700 (` an in-group addressee at intimacy 700 or more, whatever the status). Two entries for the above-status cases were withdrawn: a paired measure found they made the lowerer match the reference register less often, and they stay out until a native reader attests them. -ja-JP `:5` (hedging), `:7` (affection) and `:8` (restrained sadness) carry certainty and restraint conditions, so the hedging, certainty, affection, restraint and sadness entries never co-select on the pairs `:5`/`:6`, `:4`/`:7`, `:6`/`:7` and `:6`/`:8`; the store has no retire, so that was a rebuild of `data/ontology/ontology.cc` with the ja-JP text unchanged. +ja-JP `:5` (hedging), `:7` (affection) and `:8` (restrained sadness) carry certainty and restraint conditions, through `AtMostOrUnset` bounds (an unrated certainty or restraint does not defeat them), so the hedging, certainty, affection, restraint and sadness entries never co-select on the pairs `:5`/`:6`, `:4`/`:7`, `:6`/`:7` and `:6`/`:8`; the store has no retire, so that was a rebuild of `data/ontology/ontology.cc` with the ja-JP text unchanged. For the first AquaSynth speech-training handoff, see [Tiny PanPhon v0.1 Speech Curriculum](./examples/speech-training/tiny-panphon-v0.1/README.md). diff --git a/crates/weksa/src/ontology.rs b/crates/weksa/src/ontology.rs index 5919588..bd68b69 100644 --- a/crates/weksa/src/ontology.rs +++ b/crates/weksa/src/ontology.rs @@ -49,6 +49,10 @@ pub enum Test { AtLeast(Grade), AtMost(Grade), Is(FeatValue), + /// Holds when the feature is absent or a grade at most `g`: a bound that + /// costs nothing where the feature is unrated. `AtMost` still fails on an + /// absent feature. + AtMostOrUnset(Grade), } /// One feature of one slot of the view, tested. `Stance`, `Discourse` and @@ -63,11 +67,11 @@ pub struct Condition { impl Condition { fn holds(&self, features: &[FeatureValue]) -> bool { let Some(found) = features.iter().find(|f| f.feature == self.feature) else { - return false; + return matches!(self.test, Test::AtMostOrUnset(_)); }; match (&self.test, &found.value) { (Test::AtLeast(g), FeatValue::Grade(v)) => v >= g, - (Test::AtMost(g), FeatValue::Grade(v)) => v <= g, + (Test::AtMost(g) | Test::AtMostOrUnset(g), FeatValue::Grade(v)) => v <= g, (Test::Is(want), v) => want == v, _ => false, } @@ -78,7 +82,7 @@ impl Condition { FeatureValue { feature: self.feature, value: match &self.test { - Test::AtLeast(g) | Test::AtMost(g) => FeatValue::Grade(*g), + Test::AtLeast(g) | Test::AtMost(g) | Test::AtMostOrUnset(g) => FeatValue::Grade(*g), Test::Is(v) => v.clone(), }, } @@ -153,7 +157,7 @@ impl Ontology { /// The entries of exactly `language` whose every condition holds against the /// view's stance, discourse and scene-relation features (the scene's only when /// `with_scene` selected a relation), in `n` order. An absent feature meets no -/// condition. Nothing else of the view is read. +/// condition except `AtMostOrUnset`. Nothing else of the view is read. pub fn select<'a>( o: &'a Ontology, language: &str, @@ -172,3 +176,30 @@ pub fn select<'a>( }) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + + fn cond(test: Test) -> Condition { + Condition { slot: Slot::Discourse, feature: InvId::parse("feature:7").unwrap(), test } + } + fn has(v: FeatValue) -> Vec { + vec![FeatureValue { feature: InvId::parse("feature:7").unwrap(), value: v }] + } + fn grade(g: u16) -> Grade { + Grade::new(g).unwrap() + } + + #[test] + fn at_most_or_unset_holds_when_absent_or_at_most_and_nothing_else() { + let c = cond(Test::AtMostOrUnset(grade(799))); + assert!(c.holds(&[])); + assert!(c.holds(&has(FeatValue::Grade(grade(799))))); + assert!(c.holds(&has(FeatValue::Grade(grade(0))))); + assert!(!c.holds(&has(FeatValue::Grade(grade(800))))); + assert!(!c.holds(&has(FeatValue::Flag(true)))); + // AtMost keeps its meaning: an absent feature fails it. + assert!(!cond(Test::AtMost(grade(799))).holds(&[])); + } +} diff --git a/crates/weksa/src/store.rs b/crates/weksa/src/store.rs index 7408dce..b0b83a9 100644 --- a/crates/weksa/src/store.rs +++ b/crates/weksa/src/store.rs @@ -1287,7 +1287,7 @@ impl OntologyStore { /// The rules of one ontology entry against an inventory: the annotator, the /// bounded and normalized text of the choice and examples, the condition count /// and each condition's feature (slot `Stance`, `Discourse` or `Scene`, a grade -/// feature for `AtLeast` and `AtMost`, an option or flag value for `Is`) read at +/// feature for `AtLeast`, `AtMost` and `AtMostOrUnset`, an option or flag value for `Is`) read at /// `rev`. The store passes the entry's own `inventory_rev` on admission; a run /// passes its inventory's head, because beds number revisions independently. pub(crate) fn check_ontology_entry( diff --git a/data/ontology/ontology.cc b/data/ontology/ontology.cc index 3b2f70c380697a393ba4a2c525e5c1a683deb146..a6d2edd0651d0eeceee77f2cd666baa3aa0ba92d 100644 GIT binary patch delta 403 zcmeB;T^YMUoRQVo*vi;&vkapPjG4;h#sn4Jz!C^!va$0(nTy3aVS+N;3NT%cJRC4l zKR$n$=pz2(FlL^hH;lPYs0hY16}b#!@`|m4F%O8B!I;*P(lB$QrTJmZeAzoN1p)Fh zFwsIqPnhU_rTdJ6V6R0TWz?CmG%HcpD}ZIPqlo6_8!EbtoG__}llj#0dDl9Y_~sXv z_!oud6{l`)R+|npMqVQZp?1&Y7R^jS6tyL1nZhUU;#U+gUs94vm@2jXWa>Ar E0AK)M&;S4c diff --git a/docs/targets/interlingua-lowering-map.md b/docs/targets/interlingua-lowering-map.md index 3d5bad3..b436b3b 100644 --- a/docs/targets/interlingua-lowering-map.md +++ b/docs/targets/interlingua-lowering-map.md @@ -34,7 +34,7 @@ means no store holds it and nothing writes it. | 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` revs 5 (233 concepts and frames) and 6 (five concepts closing named gaps) were 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. Residue after rev 6, not amended (each fresh coverage reader names new gaps; the loop's growth path owns them): simultaneity (ながら), distal deixis (あの), 'first' (まず), counterfactual mood, 'at least', emphatic question; heaven versus sky for the visible 天 stays weak. 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`). The store was rebuilt without ja-JP `:14` and `:15` (above-status Scene entries; a paired measure found them net-negative on register, follow-up `ja-scene-entries-lower-register-agreement`), so ja-JP ends at `:13`: an above-status out-group addressee, and an above-status in-group addressee below intimacy 700, select no ja-JP Scene entry (`:12` still selects in-group at intimacy 700 or more regardless of status) until a native reader attests a replacement (`ja-native-reader-scenes`). ja-JP `:5`, `:7` and `:8` were revised by certainty and restraint conditions in a store rebuild (the store has no retire, so a conditions change is a rebuild), which makes ja-JP's hedging, certainty, affection, restraint and sadness entries disjoint on `:5`/`:6`, `:4`/`:7`, `:6`/`:7`, `:6`/`:8` | +| 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`/`AtMostOrUnset` 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`). The store was rebuilt without ja-JP `:14` and `:15` (above-status Scene entries; a paired measure found them net-negative on register, follow-up `ja-scene-entries-lower-register-agreement`), so ja-JP ends at `:13`: an above-status out-group addressee, and an above-status in-group addressee below intimacy 700, select no ja-JP Scene entry (`:12` still selects in-group at intimacy 700 or more regardless of status) until a native reader attests a replacement (`ja-native-reader-scenes`). ja-JP `:5`, `:7` and `:8` were revised by certainty and restraint `AtMostOrUnset` conditions (holds when the feature is unrated or at most the grade; `AtMost` still fails on an unrated feature) in a store rebuild (the store has no retire, so a conditions change is a rebuild), which makes ja-JP's hedging, certainty, affection, restraint and sadness entries disjoint on `:5`/`:6`, `:4`/`:7`, `:6`/`:7`, `:6`/`:8` | | Community (cut `language-ontology`, follow-up `language-ontology-communities`) | Key `community:`; type `weksa.community`, converted from `data/cultural-ontology/cultures/` and `referents/` (YAML deleted in the same cut) | Revised by commit; a speaker names the communities it belongs to by id | The cultural ontology's author (owner-attested evidence only; Discord-only content stays deleted, `discord-only-content-deleted`) | | Speaker (cut `speaker-scene`) | Key `speaker:@`; type `weksa.speaker`: stable axes as feature values of slot `Speaker` (origin, socioeconomic standing, age cohort, education and whatever the loop adds), at an inventory revision, and an `Annotator` | A new revision appends, never edits; a corpus item and every prior turn name a speaker slug, and a round records the speaker revisions it read | The annotator it names: `PersonaSnapshot` (a projection of a `gamecult.persona_state.v0` document, naming its source document and `updatedAt`), `Operator`, `WorkMetadata` (a public-domain work's own cast list and narration), or `Model { family, generator }` (the 2b `ModelFamily`) | | Scene (cut `speaker-scene`) | Key `scene:@`, the item's split group; type `weksa.scene`: participants (speaker slugs) and, per ordered pair, relation features of slot `Scene` (relative status, intimacy, in-group, audience and whatever the loop adds), at an inventory revision (provenance; a run reads the features at its own head), and an `Annotator` | As Speaker. Keyed by the group, so a scene never straddles the split. `data/original/speaker-state.cc` holds Model-annotated keeper scenes, one per Train or Validation group of both languages (`weksa scene put|dump`); a Test group has none and reads `NoScene` | As Speaker; a model-annotated scene is never Test evidence | -- 2.47.3 From c4f506307ca256229869ade64d14a038cd6abd5c Mon Sep 17 00:00:00 2001 From: Weksa Date: Thu, 12 Nov 2026 19:34:07 +0000 Subject: [PATCH 4/5] ja-seed-pairs: sweep unrated features and pin reach witnesses for :5, :7, :8 Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/tests/original_corpus.rs | 49 +++++++++++++++++++++------ 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/crates/weksa/tests/original_corpus.rs b/crates/weksa/tests/original_corpus.rs index 52955ea..10fe5cf 100644 --- a/crates/weksa/tests/original_corpus.rs +++ b/crates/weksa/tests/original_corpus.rs @@ -962,9 +962,11 @@ fn committed_scenes_cover_original_groups() { } /// ja-JP seed entries :4 to :8 over the stance and discourse grades that bear on -/// them (hedge, certainty, sadness, affection, restraint): the pairs that gave -/// opposed instructions (hedge against certainty, particles, final force) never -/// hold together. Fails if :5, :7 or :8 loses a conditioning clause. +/// them (hedge, certainty, sadness, affection, restraint, each also ABSENT): +/// the pairs that gave opposed instructions (hedge against certainty, +/// particles, final force) never hold together; and each of :5, :7, :8 still +/// fires where it should (hedged at certainty 700-799, affection with certainty +/// or restraint unrated), so a tightened bound does not pass as a disjoint one. #[test] fn ja_seed_pairs_never_co_select() { use weksa::inventory::Slot; @@ -984,24 +986,27 @@ fn ja_seed_pairs_never_co_select() { .find(|e| e.language == "ja-JP" && e.n == n) .unwrap_or_else(|| panic!("onto:ja-JP:{n} is absent")) }; - let holds = |e: &weksa::OntologyEntry, point: &[u16; 5]| { + type Point = [Option; 5]; + let holds = |e: &weksa::OntologyEntry, point: &Point| { e.when.iter().all(|c: &Condition| { assert!(matches!(c.slot, Slot::Stance | Slot::Discourse), "{}", e.key()); let at = feats .iter() .position(|f| *f == c.feature) .unwrap_or_else(|| panic!("{} reads a feature this test does not sweep", e.key())); - match &c.test { - Test::AtLeast(g) => point[at] >= g.get(), - Test::AtMost(g) => point[at] <= g.get(), - other => panic!("{} has {other:?}", e.key()), + match (&c.test, point[at]) { + (Test::AtLeast(g), Some(v)) => v >= g.get(), + (Test::AtMost(g) | Test::AtMostOrUnset(g), Some(v)) => v <= g.get(), + (Test::AtMostOrUnset(_), None) => true, + (Test::AtLeast(_) | Test::AtMost(_), None) => false, + (other, _) => panic!("{} has {other:?}", e.key()), } }) }; - let grid = [0u16, 300, 500, 599, 600, 700, 799, 800, 1000]; + let grid = [None, Some(0u16), Some(300), Some(500), Some(599), Some(600), Some(700), Some(799), Some(800), Some(1000)]; let pairs = [(5, 6), (4, 7), (6, 7), (6, 8)]; for p in 0..grid.len().pow(5) { - let point: [u16; 5] = std::array::from_fn(|i| grid[p / grid.len().pow(i as u32) % grid.len()]); + let point: Point = std::array::from_fn(|i| grid[p / grid.len().pow(i as u32) % grid.len()]); for (a, b) in pairs { assert!( !(holds(ja(a), &point) && holds(ja(b), &point)), @@ -1009,6 +1014,30 @@ fn ja_seed_pairs_never_co_select() { ); } } + // Reach: each entry fires at its witnesses and not just outside its core. + let (n, s) = (None, Some); + let fires: [(u32, Point); 8] = [ + (5, [s(700), s(799), n, n, n]), + (5, [s(700), n, n, n, n]), + (7, [n, s(799), n, s(700), s(599)]), + (7, [n, n, n, s(700), n]), + (7, [s(1000), s(0), s(1000), s(600), s(0)]), + (8, [n, s(799), s(700), n, s(700)]), + (8, [n, n, s(600), n, s(600)]), + (6, [n, s(800), n, n, n]), + ]; + for (e, point) in fires { + assert!(holds(ja(e), &point), "onto:ja-JP:{e} does not fire at {point:?}"); + } + let silent: [(u32, Point); 4] = [ + (5, [s(599), n, n, n, n]), + (7, [n, n, n, s(599), n]), + (7, [n, n, n, s(700), s(600)]), + (8, [n, n, s(600), n, s(599)]), + ]; + for (e, point) in silent { + assert!(!holds(ja(e), &point), "onto:ja-JP:{e} fires at {point:?}"); + } } /// The committed ontology's Scene-conditioned entries, over every scene relation -- 2.47.3 From ed2ecd0d1da1d7564d740db0543f549fb7f8c77e Mon Sep 17 00:00:00 2001 From: Weksa Date: Thu, 12 Nov 2026 19:41:40 +0000 Subject: [PATCH 5/5] ja-seed-pairs: publish the AtMostOrUnset schema; sweep through the production Condition::holds; pin :5 and :8 lower bounds Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/src/ontology.rs | 4 ++- crates/weksa/tests/original_corpus.rs | 33 ++++++++++++------- .../cultnet/weksa.ontology_entry.schema.json | 13 ++++++++ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/crates/weksa/src/ontology.rs b/crates/weksa/src/ontology.rs index bd68b69..154c562 100644 --- a/crates/weksa/src/ontology.rs +++ b/crates/weksa/src/ontology.rs @@ -65,7 +65,9 @@ pub struct Condition { } impl Condition { - fn holds(&self, features: &[FeatureValue]) -> bool { + /// Whether this condition holds over a slot's feature values: the one + /// evaluator `select` and the corpus tests share. + pub fn holds(&self, features: &[FeatureValue]) -> bool { let Some(found) = features.iter().find(|f| f.feature == self.feature) else { return matches!(self.test, Test::AtMostOrUnset(_)); }; diff --git a/crates/weksa/tests/original_corpus.rs b/crates/weksa/tests/original_corpus.rs index 10fe5cf..715233c 100644 --- a/crates/weksa/tests/original_corpus.rs +++ b/crates/weksa/tests/original_corpus.rs @@ -987,20 +987,26 @@ fn ja_seed_pairs_never_co_select() { .unwrap_or_else(|| panic!("onto:ja-JP:{n} is absent")) }; type Point = [Option; 5]; + // The production evaluator over the slot's feature values: absent features + // are left out of the list, as a view leaves them out. let holds = |e: &weksa::OntologyEntry, point: &Point| { + use weksa::packet::{FeatValue, FeatureValue, Grade}; e.when.iter().all(|c: &Condition| { assert!(matches!(c.slot, Slot::Stance | Slot::Discourse), "{}", e.key()); - let at = feats + assert!( + matches!(c.test, Test::AtLeast(_) | Test::AtMost(_) | Test::AtMostOrUnset(_)), + "{}", + e.key() + ); + let values: Vec = feats .iter() - .position(|f| *f == c.feature) - .unwrap_or_else(|| panic!("{} reads a feature this test does not sweep", e.key())); - match (&c.test, point[at]) { - (Test::AtLeast(g), Some(v)) => v >= g.get(), - (Test::AtMost(g) | Test::AtMostOrUnset(g), Some(v)) => v <= g.get(), - (Test::AtMostOrUnset(_), None) => true, - (Test::AtLeast(_) | Test::AtMost(_), None) => false, - (other, _) => panic!("{} has {other:?}", e.key()), - } + .zip(point) + .filter_map(|(f, v)| { + v.map(|v| FeatureValue { feature: *f, value: FeatValue::Grade(Grade::new(v).unwrap()) }) + }) + .collect(); + assert!(feats.contains(&c.feature), "{} reads a feature this test does not sweep", e.key()); + c.holds(&values) }) }; let grid = [None, Some(0u16), Some(300), Some(500), Some(599), Some(600), Some(700), Some(799), Some(800), Some(1000)]; @@ -1016,7 +1022,8 @@ fn ja_seed_pairs_never_co_select() { } // Reach: each entry fires at its witnesses and not just outside its core. let (n, s) = (None, Some); - let fires: [(u32, Point); 8] = [ + let fires: [(u32, Point); 9] = [ + (5, [s(600), n, n, n, n]), (5, [s(700), s(799), n, n, n]), (5, [s(700), n, n, n, n]), (7, [n, s(799), n, s(700), s(599)]), @@ -1029,7 +1036,9 @@ fn ja_seed_pairs_never_co_select() { for (e, point) in fires { assert!(holds(ja(e), &point), "onto:ja-JP:{e} does not fire at {point:?}"); } - let silent: [(u32, Point); 4] = [ + let silent: [(u32, Point); 6] = [ + (8, [n, n, s(599), n, s(700)]), + (8, [n, n, n, n, s(700)]), (5, [s(599), n, n, n, n]), (7, [n, n, n, s(599), n]), (7, [n, n, n, s(700), s(600)]), diff --git a/schemas/cultnet/weksa.ontology_entry.schema.json b/schemas/cultnet/weksa.ontology_entry.schema.json index 8e0529b..4d086d7 100644 --- a/schemas/cultnet/weksa.ontology_entry.schema.json +++ b/schemas/cultnet/weksa.ontology_entry.schema.json @@ -257,6 +257,19 @@ "required": [ "Is" ] + }, + { + "description": "Holds when the feature is absent or a grade at most `g`: a bound that\ncosts nothing where the feature is unrated. `AtMost` still fails on an\nabsent feature.", + "type": "object", + "properties": { + "AtMostOrUnset": { + "$ref": "#/$defs/Grade" + } + }, + "additionalProperties": false, + "required": [ + "AtMostOrUnset" + ] } ] } -- 2.47.3