Loop: lower each item in its own language, read and decide Validation per panel language

Prompt::lower takes the language; RunRecord carries the panel; RoundRecord and
Decision hold per-language readings (pooled keys retired); decide_panel is
Pareto non-regression over decide(); recall splits at UAX #29 word bounds.
Integration tests follow in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Weksa 2026-11-12 12:22:26 +00:00
commit 167ce83cfc
8 changed files with 634 additions and 309 deletions

View file

@ -8,7 +8,9 @@
//! consts in this module; every prompt also lists the inventory at the run's
//! revision. A mint prompt is the editor's: it reads [`TrainItems`], an
//! [`Inventory`] and an [`EditFeedback`] and nothing else, so no held-out text,
//! key or per-item score can reach it.
//! key or per-item score can reach it. A lowering prompt names the language it
//! is said in on a language line; every language is named by its tag, none by a
//! default.
//!
//! Item text may hold line breaks; the renderer indents every continuation line
//! (`indented`), so item text cannot forge a `[key]` line or a paragraph.
@ -25,7 +27,7 @@ use serde::Serialize;
use crate::corpus::{CorpusItem, CorpusItemId};
use crate::inventory::{Inventory, InventoryEntry, OptionLabel, ValueKind};
use crate::lower::{EntityForms, LowerError, TARGET_LANGUAGE, entity};
use crate::lower::{EntityForms, LowerError, entity};
use crate::packet::{
Context, Discourse, EntityId, Extension, Predication, Referent, Relation, Stance,
};
@ -37,7 +39,7 @@ use crate::validate::LoweringInput;
/// The instructions a mint prompt starts with: the editor's whole brief. Fixed
/// per run by `PROMPT_SOURCE_DIGEST`.
pub const MINT_INSTRUCTIONS: &str = "\
Propose one edit to the inventory below, so that the lines below can be encoded and lowered to English more faithfully.
Propose one edit to the inventory below, so that the lines below can be encoded and lowered, each in its own language, more faithfully.
Answer with one JSON object and nothing else: no code fence, no commentary.
Shape: {\"add\": [{\"family\", \"gloss\", \"value_kind\", \"applies_to\"}], \"retire\": [ids]}; 1 to 8 changes in all, adds and retires together.
- family: one of Concept, Frame, Role, Relation, SpeechAct, Feature, ReferentKind, Extension.
@ -65,7 +67,7 @@ A feature is {feature, value}; value is {\"Grade\": 0 to 1000}, {\"Option\": lab
/// The instructions a lowering prompt starts with: the lowerer's whole brief.
/// Fixed per run by `PROMPT_SOURCE_DIGEST`.
pub const LOWER_INSTRUCTIONS: &str = "\
Say the packet below in English (en-US), as one utterance of its speaker: all of its meaning and nothing it does not carry.
Say the packet below in the language on the language line, as one utterance of its speaker: all of its meaning and nothing it does not carry.
Every id is the inventory's listed below and every feature value is as listed; a grade is permille, 0 to 1000. The stance is the speaker's felt affect and restraint: it shapes how the utterance is said, not what it says.
A referent with an entity is named by one of that entity's forms on the entities line.
Answer with one JSON object and nothing else: {\"text\": \"...\"}. No code fence, no commentary.";
@ -81,14 +83,14 @@ Answer with one JSON object and nothing else: {\"text\": \"...\"}, the continuat
/// reads; it cannot be built from a slice, and no accessor returns text or an
/// item.
///
/// ```
/// ```no_run
/// # let dir = tempfile::tempdir().unwrap();
/// # let p = |n: &str| dir.path().join(n);
/// # for n in ["c.cc", "i.cc", "e.cc"] { std::fs::write(p(n), [0x90]).unwrap(); }
/// # let paths = weksa::run::RunPaths { corpus: &p("c.cc"), inventory: &p("i.cc"), entities: &p("e.cc") };
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded).unwrap();
/// # let panel = ["en-US".to_string()];
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded, &panel).unwrap();
/// let t = run.train().unwrap();
/// assert!(t.is_empty());
/// let _ = t.len();
/// ```
///
/// ```compile_fail
@ -100,7 +102,7 @@ Answer with one JSON object and nothing else: {\"text\": \"...\"}, the continuat
/// # let p = |n: &str| dir.path().join(n);
/// # for n in ["c.cc", "i.cc", "e.cc"] { std::fs::write(p(n), [0x90]).unwrap(); }
/// # let paths = weksa::run::RunPaths { corpus: &p("c.cc"), inventory: &p("i.cc"), entities: &p("e.cc") };
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded).unwrap();
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded, &["en-US".to_string()]).unwrap();
/// let t = run.train().unwrap();
/// let _u = t.clone();
/// ```
@ -111,7 +113,7 @@ Answer with one JSON object and nothing else: {\"text\": \"...\"}, the continuat
/// # let p = |n: &str| dir.path().join(n);
/// # for n in ["c.cc", "i.cc", "e.cc"] { std::fs::write(p(n), [0x90]).unwrap(); }
/// # let paths = weksa::run::RunPaths { corpus: &p("c.cc"), inventory: &p("i.cc"), entities: &p("e.cc") };
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded).unwrap();
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded, &["en-US".to_string()]).unwrap();
/// let t = run.train().unwrap();
/// s(&t);
/// ```
@ -120,11 +122,14 @@ pub struct TrainItems {
}
impl TrainItems {
pub(crate) fn from_corpus(corpus: &CorpusStore) -> anyhow::Result<Self> {
/// The live Train items whose language is in `panel`.
pub(crate) fn from_corpus(corpus: &CorpusStore, panel: &[String]) -> anyhow::Result<Self> {
let items = corpus
.all()?
.into_iter()
.filter(|item| SplitScheme::V1.live(item) == Some(Split::Train))
.filter(|item| {
SplitScheme::V1.live(item) == Some(Split::Train) && panel.contains(&item.language)
})
.collect();
Ok(Self { items })
}
@ -252,11 +257,11 @@ impl<'cp> EncoderOffer<'cp> {
}
/// What the editor is told of how its last edit fared: whether it was kept, and
/// the held-out composite at the current inventory, rounded to the nearest
/// [`LADDER_STEP`]. The head composite changes only when an edit is accepted and
/// an accept needs a gain above one step, so a run releases at most
/// `1 / LADDER_STEP` distinct values (Blum and Hardt 2015, the Ladder). Nothing
/// else about the held-out split is a field.
/// the held-out composite at the current inventory of each panel language,
/// rounded to the nearest [`LADDER_STEP`]. A head composite changes only when an
/// edit is accepted and an accept needs a gain above one step, so a run releases
/// at most `1 / LADDER_STEP` distinct values per language (Blum and Hardt 2015,
/// the Ladder). Nothing else about the held-out split is a field.
///
/// ```
/// let f = weksa::context::EditFeedback::none();
@ -266,18 +271,18 @@ impl<'cp> EncoderOffer<'cp> {
/// Its fields are private:
///
/// ```compile_fail,E0451
/// let _ = weksa::context::EditFeedback { last: None, head: None };
/// let _ = weksa::context::EditFeedback { last: None, head: vec![] };
/// ```
///
/// and only the run builds one with a reading:
///
/// ```compile_fail,E0624
/// let _ = weksa::context::EditFeedback::new(Some(true), Some(0.5));
/// let _ = weksa::context::EditFeedback::new(Some(true), vec![]);
/// ```
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub struct EditFeedback {
last: Option<bool>,
head: Option<f64>,
head: Vec<(String, f64)>,
}
impl EditFeedback {
@ -285,16 +290,20 @@ impl EditFeedback {
pub fn none() -> Self {
Self {
last: None,
head: None,
head: Vec::new(),
}
}
/// `last`: whether the previous edit was accepted; `head`: the composite at
/// the current inventory, rounded here to the nearest [`LADDER_STEP`].
pub(crate) fn new(last: Option<bool>, head: Option<f64>) -> Self {
/// `last`: whether the previous edit was accepted; `head`: each panel
/// language's composite at the current inventory, in panel order, rounded
/// here to the nearest [`LADDER_STEP`].
pub(crate) fn new(last: Option<bool>, head: Vec<(String, f64)>) -> Self {
Self {
last,
head: head.map(|x| (x / LADDER_STEP).round() * LADDER_STEP),
head: head
.into_iter()
.map(|(tag, x)| (tag, (x / LADDER_STEP).round() * LADDER_STEP))
.collect(),
}
}
@ -304,7 +313,15 @@ impl EditFeedback {
Some(false) => "rejected",
None => "none",
};
let head = self.head.map_or("none".to_string(), |x| format!("{x:.3}"));
let head = if self.head.is_empty() {
"none".to_string()
} else {
self.head
.iter()
.map(|(tag, x)| format!("{tag} {x:.3}"))
.collect::<Vec<_>>()
.join(", ")
};
format!("last edit: {last}; validation composite at head: {head}")
}
}
@ -312,12 +329,12 @@ impl EditFeedback {
/// Text for a model call, and the keys of the items it holds. The only
/// constructors are [`Prompt::mint`], [`Prompt::encode`] and [`Prompt::lower`].
///
/// ```
/// ```no_run
/// # let dir = tempfile::tempdir().unwrap();
/// # let p = |n: &str| dir.path().join(n);
/// # for n in ["c.cc", "i.cc", "e.cc"] { std::fs::write(p(n), [0x90]).unwrap(); }
/// # let paths = weksa::run::RunPaths { corpus: &p("c.cc"), inventory: &p("i.cc"), entities: &p("e.cc") };
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded).unwrap();
/// # let panel = ["en-US".to_string()];
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded, &panel).unwrap();
/// let t = run.train().unwrap();
/// let inv = run.inventory().inventory().unwrap();
/// let feedback = weksa::context::EditFeedback::none();
@ -335,7 +352,7 @@ impl EditFeedback {
/// # let p = |n: &str| dir.path().join(n);
/// # for n in ["c.cc", "i.cc", "e.cc"] { std::fs::write(p(n), [0x90]).unwrap(); }
/// # let paths = weksa::run::RunPaths { corpus: &p("c.cc"), inventory: &p("i.cc"), entities: &p("e.cc") };
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded).unwrap();
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded, &["en-US".to_string()]).unwrap();
/// let t = run.train().unwrap();
/// let inv = run.inventory().inventory().unwrap();
/// let feedback = weksa::context::EditFeedback::none();
@ -348,7 +365,7 @@ impl EditFeedback {
/// # let p = |n: &str| dir.path().join(n);
/// # for n in ["c.cc", "i.cc", "e.cc"] { std::fs::write(p(n), [0x90]).unwrap(); }
/// # let paths = weksa::run::RunPaths { corpus: &p("c.cc"), inventory: &p("i.cc"), entities: &p("e.cc") };
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded).unwrap();
/// # let run = weksa::run::RunStores::create(&p("run"), "t", paths, "m", "e", weksa::run::LowererReach::ToolLessRelay, weksa::corpus::ModelFamily::Unrecorded, &["en-US".to_string()]).unwrap();
/// let t = run.train().unwrap();
/// let inv = run.inventory().inventory().unwrap();
/// let feedback = weksa::context::EditFeedback::none();
@ -502,17 +519,19 @@ impl Prompt {
}
}
/// [`LOWER_INSTRUCTIONS`], the meaning of `input` (no kind, constraints,
/// scene, speaker or group), the en-US forms of every entity its referents
/// cite, and the entries of `inventory` live at the view's revision. `item`
/// [`LOWER_INSTRUCTIONS`], the `language: <tag>` line, the meaning of
/// `input` (no kind, constraints, scene, speaker or group), the forms in
/// `language` of every entity its referents cite, and the entries of `inventory` live at the view's revision. `item`
/// goes only to [`Prompt::item_keys`], never into the text. The view's
/// speaker and group fields are never read; a speaker's entity id appears
/// only when the encoder cites that entity on a referent, which is naming a
/// cited entity like any other. A cited entity the port lacks, or one with
/// no en-US form, is an error and no prompt.
/// no form in `language`, is an error and no prompt. The caller chooses the
/// language; the view has none.
pub fn lower(
input: &LoweringInput,
item: &CorpusItemId,
language: &str,
inventory: &Inventory,
entities: &dyn EntityForms,
) -> Result<Prompt, LowerError> {
@ -520,7 +539,7 @@ impl Prompt {
for id in input.referents().iter().filter_map(|r| r.entity.as_ref()) {
let found = entity(entities, id)?;
let named = found
.forms_in(TARGET_LANGUAGE)
.forms_in(language)
.filter(|f| !f.is_empty())
.ok_or_else(|| LowerError::NoForm { entity: id.clone() })?;
forms.insert(id.as_str(), named.to_vec());
@ -536,7 +555,7 @@ impl Prompt {
};
let rev = input.inventory_rev();
let text = format!(
"{LOWER_INSTRUCTIONS}\n\npacket: {}\n\nentities: {}\n\ninventory rev:{rev}{}",
"{LOWER_INSTRUCTIONS}\n\nlanguage: {language}\n\npacket: {}\n\nentities: {}\n\ninventory rev:{rev}{}",
serde_json::to_string(&meaning).expect("a meaning is plain data"),
serde_json::to_string(&forms).expect("forms are plain data"),
inventory_lines(inventory, rev)
@ -583,34 +602,39 @@ mod tests {
#[test]
fn the_feedback_line_prints_exact_values() {
let en = |x: f64| vec![("en-US".to_string(), x)];
let line = |last, head| EditFeedback::new(last, head).line();
assert_eq!(
EditFeedback::none().line(),
"last edit: none; validation composite at head: none"
);
assert_eq!(
line(Some(true), Some(0.5)),
"last edit: accepted; validation composite at head: 0.500"
line(Some(true), en(0.5)),
"last edit: accepted; validation composite at head: en-US 0.500"
);
assert_eq!(
line(Some(false), Some(0.0)),
"last edit: rejected; validation composite at head: 0.000"
line(Some(false), en(0.0)),
"last edit: rejected; validation composite at head: en-US 0.000"
);
assert_eq!(line(None, None), EditFeedback::none().line());
assert_eq!(line(None, vec![]), EditFeedback::none().line());
// The head is rounded to the nearest step before it is printed.
assert_eq!(
line(None, Some(0.5012)),
"last edit: none; validation composite at head: 0.500"
line(None, en(0.5012)),
"last edit: none; validation composite at head: en-US 0.500"
);
assert_eq!(
line(None, Some(0.5026)),
"last edit: none; validation composite at head: 0.505"
line(None, en(0.5026)),
"last edit: none; validation composite at head: en-US 0.505"
);
assert_eq!(
line(None, Some(0.9999)),
"last edit: none; validation composite at head: 1.000"
line(None, en(0.9999)),
"last edit: none; validation composite at head: en-US 1.000"
);
// One value per panel language, in the order given.
assert_eq!(
line(None, vec![("en-US".into(), 0.5026), ("ja-JP".into(), 0.41)]),
"last edit: none; validation composite at head: en-US 0.505, ja-JP 0.410"
);
assert_eq!(EditFeedback::new(None, Some(0.5026)).head, Some(0.505));
}
#[test]

View file

@ -43,8 +43,10 @@ use crate::phones::{Phone, PhoneInventory, SegmentError};
use crate::store::EntityStore;
use crate::validate::LoweringInput;
/// The language tag this lowerer's entity forms are read under, spelled as an
/// entity's `LanguageForms::language` and a corpus item's language are.
/// The language tag of the template table and the phones: what `weksa lower`
/// says in. The golf loop lowers each item in its own language and does not
/// read this. Spelled as an entity's `LanguageForms::language` and a corpus
/// item's language are.
pub const TARGET_LANGUAGE: &str = "en-US";
/// The lowerer's whole view of the entity store: one entity by id. Narrow so a

View file

@ -99,6 +99,10 @@ enum LoopCommand {
/// that family wrote. Defaults to unrecorded.
#[arg(long, default_value = "unrecorded")]
answerer_family: ModelFamilyArg,
/// The languages the run offers, reads and decides on: BCP 47 tags,
/// comma-separated. Every one needs a live Train and Validation item.
#[arg(long, value_delimiter = ',', required = true)]
panel: Vec<String>,
},
/// Run one round of the run at ROOT. The stores are read only from the
/// run's record: the global --corpus, --inventory and --entities are not
@ -329,7 +333,8 @@ struct AddArgs {
/// NFC-normalized here; `--name` byte offsets index the normalized text.
#[arg(long)]
text: String,
#[arg(long, default_value = "en-US")]
/// The item's language, a BCP 47 tag; no language is the default.
#[arg(long)]
language: String,
/// An entity id.
#[arg(long)]
@ -765,6 +770,7 @@ fn main() -> Result<()> {
embed_model,
lowerer_reach,
answerer_family,
panel,
}) => {
let run = RunStores::create(
&root,
@ -778,6 +784,7 @@ fn main() -> Result<()> {
&embed_model,
lowerer_reach,
answerer_family.into(),
&panel,
)?;
println!("opened run:{}", run.record().label);
}
@ -821,12 +828,12 @@ fn main() -> Result<()> {
std::process::exit(3);
}
AblationOutcome::Read(readings) => {
for (split, reading) in readings {
for (language, split, reading) in readings {
let name = match split {
Split::Train => "train",
_ => "validation",
};
println!("{name} {}", reading.line());
println!("{language} {name} {}", reading.line());
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,10 @@
//! word, never one word per character: per-character words would make every
//! honest single-word gloss share bigrams with a held-out sentence, which is
//! the leak signature. A change between such a run and any other script also
//! ends a word. A leak in these scripts shows at n = 1 and in chrF; a
//! dictionary segmenter is the ja-JP panel's to add.
//! ends a word. The n >= 2 coverage signature is therefore blind on these
//! scripts: a leak there shows at n = 1 and in max pairwise chrF, which is
//! character n-grams and script-blind. A dictionary segmenter is follow-up
//! `ja-word-segmenter`.
use caseless::default_case_fold_str;
use unicode_normalization::UnicodeNormalization;

View file

@ -1,12 +1,9 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Decision",
"description": "One closed edit-accept cycle: digests and numbers, no text. Key\n`run:<label>:cycle:<k>`. Only [`cycle`] builds one.\n\n```\nfn f(d: &weksa::run::Decision) -> u32 { d.k() }\n```\n\nEvery field is given, so the literal below fails only on privacy; its twin is\nthe same literal in the unit test `a_decision_literal_compiles_in_the_lib`.\n\n```compile_fail,E0451\nuse weksa::run::{Call, Decision, Reading, Verdict};\nfn f() -> Decision {\n let r = Reading { chrf: 0.0, meaning: 0.0, composite: 0.0 };\n Decision {\n k: 1,\n base_round: 1,\n base_rev: 1,\n base_inventory_digest: String::new(),\n edit: Call { prompt: String::new(), items: vec![] },\n edit_answer_digest: None,\n candidate_rev: None,\n candidate_inventory_digest: None,\n candidate_round: None,\n base: r,\n candidate: None,\n floor: r,\n threshold: 0.0,\n verdict: Verdict::Accepted,\n }\n}\n```",
"description": "One closed edit-accept cycle: digests and numbers, no text. Key\n`run:<label>:cycle:<k>`. Only [`cycle`] builds one.\n\n```\nfn f(d: &weksa::run::Decision) -> u32 { d.k() }\n```\n\nEvery field is given, so the literal below fails only on privacy; its twin is\nthe same literal in the unit test `a_decision_literal_compiles_in_the_lib`.\n\n```compile_fail,E0451\nuse weksa::run::{Call, Decision, Verdict};\nfn f() -> Decision {\n Decision {\n k: 1,\n base_round: 1,\n base_rev: 1,\n base_inventory_digest: String::new(),\n edit: Call { prompt: String::new(), items: vec![] },\n edit_answer_digest: None,\n candidate_rev: None,\n candidate_inventory_digest: None,\n candidate_round: None,\n panel: vec![],\n verdict: Verdict::Accepted,\n }\n}\n```",
"type": "object",
"properties": {
"base": {
"$ref": "#/$defs/Reading"
},
"base_inventory_digest": {
"type": "string"
},
@ -21,16 +18,6 @@
"format": "uint32",
"minimum": 0
},
"candidate": {
"anyOf": [
{
"$ref": "#/$defs/Reading"
},
{
"type": "null"
}
]
},
"candidate_inventory_digest": {
"type": [
"string",
@ -64,17 +51,17 @@
"null"
]
},
"floor": {
"$ref": "#/$defs/Reading"
},
"k": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"threshold": {
"type": "number",
"format": "double"
"panel": {
"description": "One row per panel language, in panel order.",
"type": "array",
"items": {
"$ref": "#/$defs/PanelReading"
}
},
"verdict": {
"$ref": "#/$defs/Verdict"
@ -86,10 +73,8 @@
"base_rev",
"base_inventory_digest",
"edit",
"base",
"floor",
"threshold",
"verdict"
"verdict",
"panel"
],
"$defs": {
"Call": {
@ -119,6 +104,41 @@
"Meaning"
]
},
"PanelReading": {
"description": "One panel language's row of a cycle: the head's Validation reading, the\ncandidate's (none when the edit never reached a round), the language's\nmeasured floor and the threshold derived from it.",
"type": "object",
"properties": {
"base": {
"$ref": "#/$defs/Reading"
},
"candidate": {
"anyOf": [
{
"$ref": "#/$defs/Reading"
},
{
"type": "null"
}
]
},
"floor": {
"$ref": "#/$defs/Reading"
},
"language": {
"type": "string"
},
"threshold": {
"type": "number",
"format": "double"
}
},
"required": [
"language",
"base",
"floor",
"threshold"
]
},
"Reading": {
"description": "One validation reading: chrF 0 to 100, meaning 0 to 1, and their composite.",
"type": "object",

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RoundRecord",
"description": "One scored round. It holds no text and no Test reading: per-item rows for\nTrain only, Validation only as means and refusal counts by stage. Only\n`run::round` builds one.\n\n```\nfn f(r: &weksa::run::RoundRecord) -> u32 { r.n() }\n```\n\nEvery field is given, so the literal below fails only on privacy; its twin\nis the same literal in the unit test `a_round_record_literal_compiles_in_the_lib`.\n\n```compile_fail,E0451\nuse weksa::run::{RefusalCounts, RoundRecord};\nuse weksa::score::SplitMean;\nfn f() -> RoundRecord {\n RoundRecord {\n n: 1,\n inventory_rev: 1,\n prompt_digest: String::new(),\n corpus_digest: String::new(),\n entity_digest: String::new(),\n model: String::new(),\n embedder: String::new(),\n chrf_version: String::new(),\n calls: vec![],\n train: vec![],\n train_mean: SplitMean::default(),\n train_scored: 0,\n validation_mean: SplitMean::default(),\n validation_scored: 0,\n validation_refused: RefusalCounts::default(),\n inventory_digest: String::new(),\n cycle: None,\n admit_rules: vec![],\n parse_reasons: vec![],\n own_family: 0,\n }\n}\n```",
"description": "One scored round. It holds no text and no Test reading: per-item rows for\nTrain only, Validation only as means and refusal counts by stage. Only\n`run::round` builds one.\n\n```\nfn f(r: &weksa::run::RoundRecord) -> u32 { r.n() }\n```\n\nEvery field is given, so the literal below fails only on privacy; its twin\nis the same literal in the unit test `a_round_record_literal_compiles_in_the_lib`.\n\n```compile_fail,E0451\nuse weksa::run::RoundRecord;\nfn f() -> RoundRecord {\n RoundRecord {\n n: 1,\n inventory_rev: 1,\n prompt_digest: String::new(),\n corpus_digest: String::new(),\n entity_digest: String::new(),\n model: String::new(),\n embedder: String::new(),\n chrf_version: String::new(),\n calls: vec![],\n train: vec![],\n inventory_digest: String::new(),\n cycle: None,\n admit_rules: vec![],\n parse_reasons: vec![],\n own_family: 0,\n languages: vec![],\n }\n}\n```",
"type": "object",
"properties": {
"admit_rules": {
@ -49,6 +49,13 @@
"format": "uint32",
"minimum": 0
},
"languages": {
"description": "One reading per panel language, in panel order. No pooled mean exists.",
"type": "array",
"items": {
"$ref": "#/$defs/LanguageRound"
}
},
"model": {
"type": "string"
},
@ -80,27 +87,6 @@
"items": {
"$ref": "#/$defs/TrainRow"
}
},
"train_mean": {
"$ref": "#/$defs/SplitMean"
},
"train_scored": {
"description": "Train items that lowered and scored; `train_mean` is over all of them\nand counts a refused item as 0.",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"validation_mean": {
"$ref": "#/$defs/SplitMean"
},
"validation_refused": {
"$ref": "#/$defs/RefusalCounts"
},
"validation_scored": {
"description": "Validation items that lowered and scored; `validation_mean` is over all\nof them and counts a refused item as 0, so refusing a weak item cannot\nraise it.",
"type": "integer",
"format": "uint32",
"minimum": 0
}
},
"required": [
@ -114,12 +100,8 @@
"chrf_version",
"calls",
"train",
"train_mean",
"validation_mean",
"validation_refused",
"train_scored",
"validation_scored",
"inventory_digest"
"inventory_digest",
"languages"
],
"$defs": {
"Call": {
@ -141,6 +123,44 @@
"items"
]
},
"LanguageRound": {
"description": "One panel language's readings in a round: the means over that language's\nitems only (a refused item counts 0) and how many items lowered and scored.",
"type": "object",
"properties": {
"language": {
"type": "string"
},
"train_mean": {
"$ref": "#/$defs/SplitMean"
},
"train_scored": {
"description": "Train items that lowered and scored; `train_mean` is over all of them.",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"validation_mean": {
"$ref": "#/$defs/SplitMean"
},
"validation_refused": {
"$ref": "#/$defs/RefusalCounts"
},
"validation_scored": {
"description": "Validation items that lowered and scored; `validation_mean` is over all\nof them and counts a refused item as 0, so refusing a weak item cannot\nraise it.",
"type": "integer",
"format": "uint32",
"minimum": 0
}
},
"required": [
"language",
"train_mean",
"train_scored",
"validation_mean",
"validation_scored",
"validation_refused"
]
},
"Outcome": {
"oneOf": [
{

View file

@ -49,6 +49,13 @@
"description": "The answering model.",
"type": "string"
},
"panel": {
"description": "The languages the run offers, reads and decides on: BCP 47-form tags,\nsorted, unique, at least one, set once at open. An item of any other\nlanguage is never offered, minted from, probed or scored.",
"type": "array",
"items": {
"type": "string"
}
},
"prompt_digest": {
"type": "string"
}
@ -65,7 +72,8 @@
"embedder",
"chrf_version",
"inventory_digest",
"lowerer_reach"
"lowerer_reach",
"panel"
],
"$defs": {
"LowererReach": {