ja-inventory: rev 5 concepts and frames for the original bed (model-drafted, Validation-informed), weksa inventory admit, onto:ja-JP:15 (#54)

This commit is contained in:
Weksa 2026-11-12 18:08:04 +00:00
commit 7babf8aadc
9 changed files with 130 additions and 162 deletions

View file

@ -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.

View file

@ -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<InventoryEntry>)> {
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<String> = 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]

View file

@ -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,

View file

@ -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<M: ModelPort, E: Embedder>(
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;

View file

@ -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}");
}

View file

@ -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, &copy).unwrap();
let inventory = InventoryStore::open(&copy).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"), &copy).unwrap();
InventoryStore::open(&copy).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:?}");
}
}
}
}

Binary file not shown.

Binary file not shown.

View file

@ -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<k>` nodes, `encoder:<16 hex>`, canonical `<family>:<n>`). 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<N>` 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 `<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; Self on merge to main. Admission checks form, not text |
| 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` 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:<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` 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`,