mint-prompt-door: the mint prompt teaches the shape the admit door admits #64

Merged
weksa merged 2 commits from weksa/mint-prompt-door into main 2026-11-12 23:54:02 +00:00
3 changed files with 96 additions and 13 deletions

View file

@ -35,9 +35,10 @@ use serde::Serialize;
use crate::corpus::{CorpusItem, CorpusItemId};
use crate::inventory::{Inventory, InventoryEntry, OptionLabel, Slot, ValueKind};
use crate::lower::{EntityForms, LowerError, entity};
use crate::ontology::{Ontology, select};
use crate::encode::{EntryOntologyDraft, OntologyDraft};
use crate::ontology::{Condition, Layer, Ontology, Test, select};
use crate::packet::{
Context, Discourse, EntityId, Extension, Predication, Referent, Relation, Stance,
Context, Discourse, EntityId, Extension, FeatValue, Grade, Predication, Referent, Relation, Stance,
};
use crate::run::LADDER_STEP;
use crate::split::{Checkpoint, Split, SplitScheme, fnv1a64, hex16};
@ -92,7 +93,7 @@ Answer with one JSON object and nothing else: {\"text\": \"...\"}, the continuat
pub const MINT_ONTOLOGY_INSTRUCTIONS: &str = "\
Below are lines written in one language (the language line), the features an ontology condition may test, and the entries that language's ontology already has. Draft new entries: choices this language's culture makes, for a speaker in a stance and scene, about word, grammar, discourse, sound or script, that the lines show.
Answer with one JSON object and nothing else: {\"entries\": [{\"layer\": \"...\", \"when\": [...], \"choice\": \"...\", \"examples\": [...]}]}. No code fence, no commentary.
Give 1 to 8 entries. layer is one of lexicon, grammar, discourse, phonology, orthography. when holds at most 4 conditions, each {\"slot\": \"Stance\" | \"Discourse\" | \"Scene\", \"feature\": \"<id>\", \"test\": {\"AtLeast\": <permille>} | {\"AtMost\": <permille>} | {\"AtMostOrUnset\": <permille>} | {\"Is\": <value>}}, over the listed feature ids only; when may be empty when a choice holds for every line of the language. choice states a pattern the lines show, in at most 200 bytes, without quoting a line. examples are original short phrases of the language, never copied from the lines below, at most 3.";
Give 1 to 8 entries. layer is exactly one of Lexicon, Grammar, Discourse, Phonology, Orthography. when holds at most 4 conditions, each {\"slot\": \"Stance\" | \"Discourse\" | \"Scene\", \"feature\": \"<id>\", \"test\": ...}, where <id> is the id printed before the colon of a listed feature (\"feature:10\", never its name) and its slot is one that feature lists. test is {\"AtLeast\": <permille>}, {\"AtMost\": <permille>} or {\"AtMostOrUnset\": <permille>} on a grade feature, or {\"Is\": {\"Option\": \"<label>\"}} on an options feature with one of its labels, or {\"Is\": {\"Flag\": true}} on a flag feature. The example line shows each form on listed features; its choice and examples are placeholders, not content to copy. when may be empty when a choice holds for every line of the language. choice states a pattern the lines show, in at most 200 bytes, without quoting a line. examples are original short phrases of the language, never copied from the lines below, at most 3, each at most 120 bytes.";
/// The live Train items of a corpus, in key order. The only input minting
/// reads; it cannot be built from a slice, and no accessor returns text or an
@ -466,6 +467,53 @@ fn entry_line(entry: &InventoryEntry) -> String {
line
}
/// The slot a condition on `entry` reads it in: the first of its slots a
/// lowering view carries.
fn condition_slot(entry: &InventoryEntry) -> Option<Slot> {
entry
.applies_to
.iter()
.copied()
.find(|s| matches!(s, Slot::Stance | Slot::Discourse | Slot::Scene))
}
/// One entry in the form the admit door reads, serialized from its own draft
/// type so the taught shape cannot drift from the door: a condition on the
/// first listed grade, options and flag feature each, over placeholder text.
fn mint_example(readable: &[&InventoryEntry]) -> String {
let when = [
readable.iter().find(|e| e.value_kind == Some(ValueKind::Grade)).map(|e| {
(e, Test::AtLeast(Grade::new(500).expect("500 is a permille")))
}),
readable.iter().find_map(|e| match &e.value_kind {
Some(ValueKind::Options(set)) => {
let first = Vec::<OptionLabel>::from(set.clone()).remove(0);
Some((e, Test::Is(FeatValue::Option(first))))
}
_ => None,
}),
readable
.iter()
.find(|e| e.value_kind == Some(ValueKind::Flag))
.map(|e| (e, Test::Is(FeatValue::Flag(true)))),
]
.into_iter()
.flatten()
.filter_map(|(e, test)| {
Some(Condition { slot: condition_slot(e)?, feature: e.id, test })
})
.collect();
let draft = OntologyDraft {
entries: vec![EntryOntologyDraft {
layer: Layer::Grammar,
when,
choice: "a pattern the lines show".into(),
examples: vec!["a short phrase of your own".into()],
}],
};
serde_json::to_string(&draft).expect("a draft serializes")
}
/// Each entry of `inventory` live at `rev`, one `\n` and one line apiece: the
/// listing both prompts print under `inventory rev:<rev>`.
fn inventory_lines(inventory: &Inventory, rev: u32) -> String {
@ -534,17 +582,11 @@ impl Prompt {
if items.is_empty() {
return None;
}
let features: String = inventory
let readable: Vec<&InventoryEntry> = inventory
.entries()
.filter(|e| {
e.live_at(rev)
&& e.value_kind.is_some()
&& e.applies_to
.iter()
.any(|s| matches!(s, Slot::Stance | Slot::Discourse | Slot::Scene))
})
.map(|e| format!("\n{}", entry_line(e)))
.filter(|e| e.live_at(rev) && e.value_kind.is_some() && condition_slot(e).is_some())
.collect();
let features: String = readable.iter().map(|e| format!("\n{}", entry_line(e))).collect();
let existing: String = ontology
.entries()
.iter()
@ -556,6 +598,7 @@ impl Prompt {
format!("language: {language}"),
format!("features rev:{rev}{features}"),
format!("entries{existing}"),
format!("example {}", mint_example(&readable)),
];
parts.extend(items.iter().map(|item| block(item, None)));
Some(Prompt {

View file

@ -56,6 +56,7 @@ const REF_ONLY: &str = "feature:62";
const PRED_ONLY: &str = "feature:63";
const STANCE_RETIRED: &str = "feature:64";
const SCENE_G: &str = "feature:65";
const SCENE_OPT: &str = "feature:66";
/// A fixture: items in en-US and ja-JP across every split and a withdrawal, an
/// inventory with condition-readable and unreadable features, an ontology with
@ -94,6 +95,7 @@ fn fixture(dir: &Path, panel: &[&str]) -> RunStores {
f(PRED_ONLY, ValueKind::Grade, &[Slot::Predication]),
f(STANCE_RETIRED, ValueKind::Grade, &[Slot::Stance]),
f(SCENE_G, ValueKind::Grade, &[Slot::Scene]),
f(SCENE_OPT, ValueKind::Options(weksa::OptionSet::new(vec![weksa::OptionLabel::parse("below").unwrap(), weksa::OptionLabel::parse("above").unwrap()]).unwrap()), &[Slot::Scene]),
];
let changes = features.iter().map(|e| Change::Add(e.id)).collect();
let mut features = features;
@ -360,3 +362,41 @@ fn a_mint_answer_admits_into_a_copy_and_a_new_run_pins_the_copy() {
let mut grown = OntologyStore::open(&copy).unwrap();
assert!(grown.admit(entries, &inv).is_err());
}
/// The line after `example ` in a mint prompt.
fn example_of(text: &str) -> &str {
text.lines().find_map(|l| l.strip_prefix("example ")).expect("the prompt carries an example")
}
#[test]
fn the_mint_prompts_own_example_parses_and_admits_through_the_door() {
let dir = tempfile::tempdir().unwrap();
let run = fixture(dir.path(), &["en-US"]);
let mut m = Record::new(DRAFT);
run.mint_ontology(&mut m, "en-US").unwrap();
let example = example_of(&m.prompts[0].0);
// It teaches the door's spelling: a capitalized layer, feature ids, each test form.
assert!(example.contains(r#""layer":"Grammar""#), "{example}");
for form in [r#"{"AtLeast":500}"#, r#"{"Is":{"Option":"#, r#"{"Is":{"Flag":true}}"#] {
assert!(example.contains(form), "{form} in {example}");
}
let draft = weksa::encode::OntologyDraft::parse(example).unwrap();
let inv = InventoryStore::open(&dir.path().join("i.cc")).unwrap();
let copy = dir.path().join("grown.cc");
std::fs::copy(dir.path().join("ontology.cc"), &copy).unwrap();
let mut grown = OntologyStore::open(&copy).unwrap();
let annotator = Annotator::Model { family: ModelFamily::Anthropic, generator: "minter".into() };
grown.admit(draft.into_entries("en-US", 3, 2, &annotator), &inv).unwrap();
}
#[test]
fn the_mint_example_names_only_features_the_prompt_lists() {
let dir = tempfile::tempdir().unwrap();
let run = fixture(dir.path(), &["en-US"]);
let mut m = Record::new(DRAFT);
run.mint_ontology(&mut m, "en-US").unwrap();
let example = example_of(&m.prompts[0].0);
for unreadable in [REF_ONLY, PRED_ONLY, STANCE_RETIRED] {
assert!(!example.contains(unreadable), "{unreadable}");
}
}

View file

@ -34,7 +34,7 @@ means no store holds it and nothing writes it.
| Inventory entry | Key `<family>:<n>`, `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:<n>`, 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:<language>:<n>` 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:<language>:<n>`, `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)`) and never writes it; slice 1 of growth is outside any run: `weksa loop mint-ontology` drafts entries from the run's Train items of one language into `<root>/ontology-mint/<digest>.answer`, `weksa ontology admit --ontology <copy>` admits them into a file copy, and a new run pins the copy (a run-owned copy waits on follow-up `language-ontology-growth`). 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`, and ja-JP `:3` (anger) carries `AtMostOrUnset` hedge and affection bounds, as en-US `:3`, which makes `:3`/`:5` and `:3`/`:7` disjoint; en-US `:3` carries `AtMostOrUnset` hedge and affection bounds, `:5` an `AtMostOrUnset` certainty bound (the clause it held in prose) and `:8` one too (as ja-JP `:7`), which makes `:3`/`:5`, `:3`/`:8`, `:5`/`:6` and `:6`/`:8` disjoint, while `:6`/`:10` stay co-selectable and are kept apart by scoping in `:6`'s text; ja-JP `:9` was reworded in a rebuild to state its question ending as available, so `:6`/`:9`, `:5`/`:9` and `:3`/`:9` stay co-selectable and are reconciled by wording, not by condition |
| Language ontology entry (cut `language-ontology`) | Key `onto:<language>:<n>`, `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`, as the door spells them; the mint prompt ends with an `example` line serialized from the draft type itself), 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)`) and never writes it; slice 1 of growth is outside any run: `weksa loop mint-ontology` drafts entries from the run's Train items of one language into `<root>/ontology-mint/<digest>.answer`, `weksa ontology admit --ontology <copy>` admits them into a file copy, and a new run pins the copy (a run-owned copy waits on follow-up `language-ontology-growth`). 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`, and ja-JP `:3` (anger) carries `AtMostOrUnset` hedge and affection bounds, as en-US `:3`, which makes `:3`/`:5` and `:3`/`:7` disjoint; en-US `:3` carries `AtMostOrUnset` hedge and affection bounds, `:5` an `AtMostOrUnset` certainty bound (the clause it held in prose) and `:8` one too (as ja-JP `:7`), which makes `:3`/`:5`, `:3`/`:8`, `:5`/`:6` and `:6`/`:8` disjoint, while `:6`/`:10` stay co-selectable and are kept apart by scoping in `:6`'s text; ja-JP `:9` was reworded in a rebuild to state its question ending as available, so `:6`/`:9`, `:5`/`:9` and `:3`/`:9` stay co-selectable and are reconciled by wording, not by condition |
| Community (cut `language-ontology`, follow-up `language-ontology-communities`) | Key `community:<slug>`; 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:<slug>@<n>`; 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:<group>@<n>`, 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 |