Pin the panel sum rule's liveness, fix stale Ladder docs, name the composite verdict in the map record column

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Weksa 2026-11-12 13:34:48 +00:00
commit f05906b1f7
4 changed files with 55 additions and 13 deletions

View file

@ -258,10 +258,12 @@ 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 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.
/// rounded to the nearest [`LADDER_STEP`]. The heads change only when an edit is
/// accepted, and an accept raises the panel's composite sum by more than one
/// step, so a run over `L` languages releases at most `1.5 * L / LADDER_STEP`
/// changes of this line (Blum and Hardt 2015, the Ladder; ruling
/// `weksa-lowering:ruling:panel-accept-sum-rule`). Nothing else about the
/// held-out split is a field.
///
/// ```
/// let f = weksa::context::EditFeedback::none();

View file

@ -67,10 +67,12 @@ pub const PROMPT_SOURCE_DIGEST: u64 = fnv1a64_of(&[
]);
/// The least composite gain an edit must show over item noise to be kept, and
/// the grid the editor's one held-out number is rounded to. A run releases at
/// most `1 / LADDER_STEP` = 200 distinct head composites (the Ladder, Blum and
/// Hardt 2015), because the head changes only on an accept and an accept needs
/// a gain above one step.
/// the grid the editor's one held-out number is rounded to. Every accept raises
/// the panel's composite sum by more than one step and a composite lies in
/// -0.5 to 1, so a run over `L` languages accepts, and changes the released
/// heads, at most `1.5 * L / LADDER_STEP` times (`300` for one language; the
/// Ladder, Blum and Hardt 2015; ruling
/// `weksa-lowering:ruling:panel-accept-sum-rule`, see [`decide_panel`]).
pub const LADDER_STEP: f64 = 0.005;
/// What a run pinned at open. Key `run:<label>`.
@ -1807,7 +1809,8 @@ pub fn decide(base: &Reading, candidate: &Reading, floor: &Reading) -> Verdict {
/// readings ratchets every base to a favourable draw and strangles the loop
/// (the convergence test would then fire on noise). Wang, Lipton and Tsvetkov
/// (EMNLP 2020) measure negative interference between languages sharing
/// parameters, which the per-language regression test still bounds.
/// parameters, which the per-language regression test still bounds. The rule is
/// ruling `weksa-lowering:ruling:panel-accept-sum-rule`.
///
/// Termination (the Ladder bound, Blum and Hardt 2015): every accept raises
/// the sum of the panel's composites by more than [`LADDER_STEP`]. A composite
@ -1837,7 +1840,10 @@ pub fn decide_panel(rows: &[(Reading, Reading, Reading)]) -> Verdict {
/// One panel language's row of a cycle: the head's Validation reading, the
/// candidate's (none when the edit never reached a round), the language's
/// measured floor and the threshold derived from it.
/// measured floor and the threshold derived from it. The threshold decides only
/// in a one-language panel ([`decide`]); with two or more languages it is
/// recorded beside the floor and the accept is the sum rule of
/// [`decide_panel`].
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct PanelReading {
pub language: String,
@ -2047,7 +2053,7 @@ fn readings_of(
/// One edit-accept cycle. The editor is asked for one inventory edit from a
/// prompt built over Train items, the head inventory and an [`EditFeedback`];
/// the edit is admitted as a candidate revision in `<root>/cycle-<k>/inventory.cc`;
/// a candidate round scores it; [`decide`] accepts or rejects on Validation
/// a candidate round scores it; [`decide_panel`] accepts or rejects on Validation
/// against the head's establishing round. Accepting is one write: the candidate
/// round and the decision go into `run.cc` in one batch, and the decision names
/// the new head. While any prompt is unanswered, or the candidate round fails,
@ -2451,6 +2457,40 @@ mod tests {
assert_eq!(net(LADDER_STEP + 4e-10), rejected(Why::NoGain));
assert_eq!(net(LADDER_STEP + 6e-10), Verdict::Accepted);
assert_eq!(net(LADDER_STEP + 2e-9), Verdict::Accepted);
// Liveness: no language need pass its own threshold. Two languages each
// gaining 0.6 of a step at zero floor accept; so does one gaining two
// steps under a 0.02 floor (its own threshold) with the others flat; so
// does a gain with another language falling inside its own floor.
let each = 0.6 * LADDER_STEP;
assert_eq!(
decide_panel(&[
(base, reading(50.0, 0.5, 0.5 + each), zero),
(base, reading(50.0, 0.5, 0.5 + each), zero)
]),
Verdict::Accepted
);
let big_floor = reading(0.0, 0.0, 0.02);
assert_eq!(
decide_panel(&[(base, reading(50.0, 0.5, 0.5 + 2.0 * LADDER_STEP), big_floor), (base, flat, big_floor)]),
Verdict::Accepted
);
let own_floor = reading(0.0, 0.0, 0.004);
assert_eq!(
decide_panel(&[
(base, reading(50.0, 0.5, 0.51), reading(0.0, 0.0, 0.03)),
(base, reading(50.0, 0.5, 0.497), own_floor)
]),
Verdict::Accepted,
"a fall inside the language's floor, net 0.007"
);
// The same fall with the net exactly a step rejects.
assert_eq!(
decide_panel(&[
(base, reading(50.0, 0.5, 0.508), reading(0.0, 0.0, 0.03)),
(base, reading(50.0, 0.5, 0.497), own_floor)
]),
rejected(Why::NoGain)
);
// The sum is over the deltas, with each language's fall counted.
assert_eq!(
decide_panel(&[(base, reading(50.0, 0.5, 0.504), zero), (base, small_fall, zero)]),

View file

@ -41,7 +41,7 @@ means no store holds it and nothing writes it.
| Corpus item | Key `item:<source-slug>:<n>`, `n` canonical decimal >= 1 (no sign, no leading zero, from cut 2b); type `weksa.corpus_item` with text, language, speaker, split group, prior turns, source. Source `ModelWritten` carries a `ModelFamily` enum, the generator string as the fixture wrote it, and a prompt digest (cut 2b). The item carries no speaker state of its own: its speaker slug and group name the Speaker and Scene documents | Admitted once; the whole item is immutable (re-adding an identical item is a no-op, any difference is refused; a correction is a new item that `supersedes` the old); text already held under another key is refused unless the new item supersedes that key (cut 2b); withdrawn, never deleted, so old scores stay explainable | Importers and `weksa corpus add`, under `corpus-cast-plus-public-domain`; the source enum has no variant for Discord or any training-forbidden source (`corpus-clean`) |
| Split assignment | Derived: `split(scheme, item.group)` | Fixed for a scheme; a new scheme is a new constant, never a reassignment. A model-written item whose group derives test is in no split; a superseding item keeps its predecessor's group | The hash function. Nobody assigns by hand, so there is no writer to forbid |
| Loop round | Key `run:<label>:round:<n>`; type `weksa.loop_round` with spec version, profile revisions, model ids and prompt digests, sampled train items, Validation means and refusal counts (from cut `ja-panel-loop`: one set per language of the run's panel, no pooled mean), the digest of the inventory file it was scored under and, for a cycle's candidate round, its cycle `k` | Scored and recorded whole; immutable. A candidate round is put in one batch with its Decision. Lives in the run store on Yggdrasil | `run::round` and `run::cycle` (both through `score_round`) |
| Loop decision | Key `run:<label>:cycle:<k>`; type `weksa.loop_decision` with the base round and revision, the edit prompt's digest and Train keys, the editor's answer digest, the candidate revision, inventory digest and round, the base, candidate and floor readings (chrF, meaning, composite), the threshold and the verdict (Accepted, or Rejected for edit-parse, edit-admit, no-gain, regressed chrF or meaning); digests and numbers, no text | Immutable. The latest Accepted one names the run's head inventory (`<root>/cycle-<k>/inventory.cc`, its candidate digest, its candidate round); with none the head is the copy `create` made at `<root>/inventory.cc`. Lives in the run store on Yggdrasil | `run::cycle`'s accept rule, `run::decide`: the validation composite gains more than max(noise floor, `LADDER_STEP`), no metric falls past max(its floor, one step), the composite does not fall past max(its floor, one step), and a non-finite reading rejects (the one regression test, `run::regression`); differences compared in billionths so a gain of exactly one step is a tie. From cut `ja-panel-loop` the readings, floor and threshold are kept per panel language and `decide` runs per language and `run::regression` is the one regression test: any language regressing rejects (verdicts `regressed-chrf`, `regressed-meaning`, `regressed-composite`), otherwise a one-language panel accepts as `decide` does and a panel of two or more accepts when the sum of composite deltas exceeds one `LADDER_STEP` (billionths). Given up: strict Pareto non-regression; a language may lose up to its own noise floor if the panel nets more than a step, because zero tolerance ratchets every base to a favourable noise draw and strangles the loop. Termination: each accept raises the composite sum by more than a step and composites lie in -0.5 to 1, so a run accepts at most 1.5 * languages / `LADDER_STEP` edits |
| Loop decision | Key `run:<label>:cycle:<k>`; type `weksa.loop_decision` with the base round and revision, the edit prompt's digest and Train keys, the editor's answer digest, the candidate revision, inventory digest and round, the base, candidate and floor readings (chrF, meaning, composite), the threshold and the verdict (Accepted, or Rejected for edit-parse, edit-admit, no-gain, regressed chrF, meaning or composite); digests and numbers, no text | Immutable. The latest Accepted one names the run's head inventory (`<root>/cycle-<k>/inventory.cc`, its candidate digest, its candidate round); with none the head is the copy `create` made at `<root>/inventory.cc`. Lives in the run store on Yggdrasil | `run::cycle`'s accept rule, `run::decide`: the validation composite gains more than max(noise floor, `LADDER_STEP`), no metric falls past max(its floor, one step), the composite does not fall past max(its floor, one step), and a non-finite reading rejects (the one regression test, `run::regression`); differences compared in billionths so a gain of exactly one step is a tie. From cut `ja-panel-loop` the readings, floor and threshold are kept per panel language and `decide` runs per language and `run::regression` is the one regression test: any language regressing rejects (verdicts `regressed-chrf`, `regressed-meaning`, `regressed-composite`), otherwise a one-language panel accepts as `decide` does and a panel of two or more accepts when the sum of composite deltas exceeds one `LADDER_STEP` (billionths). Given up: strict Pareto non-regression; a language may lose up to its own noise floor if the panel nets more than a step, because zero tolerance ratchets every base to a favourable noise draw and strangles the loop. Termination: each accept raises the composite sum by more than a step and composites lie in -0.5 to 1, so a run accepts at most 1.5 * languages / `LADDER_STEP` edits |
| Score | Key `<lowering-key>:<metric>@<metric-version>`; type `weksa.score` | Immutable; a new metric version scores again under a new key | The scorer, deterministic given the metric version and the embedder's model id. Cut `scorer` computes scores as values; cut `golf-loop-en` stores them, because the lowering keys they hang from arrive with the loop. Lands with follow-up `golf-loop-score-docs`, in the round's directory; nothing in the cycle reads them |
| Lowering trace | Key `lowering:<packet-key>:<profile>@<rev>:<lowerer-digest>:<sample>`; type `weksa.lowering` holding the surface text and its trace together | Immutable. A surface text cannot exist without its trace, because they are one record (`lowering-traced`) | The lowerer emits; the lowerer sees the packet without its `trace` (the input type has no such field), never the corpus item |
| Conlang lexicon entry | Key `lex:<language>:<concept-id>`; root form, morphology class | Root placed at its concept's embedding when the concept first needs a form in that language; stable once accepted; diachrony derives later forms, it does not overwrite | The language project's owner: the operator for her settings' languages, the loop proposing roots for her review |

View file

@ -114,7 +114,7 @@
]
},
"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.",
"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. The threshold decides only\nin a one-language panel ([`decide`]); with two or more languages it is\nrecorded beside the floor and the accept is the sum rule of\n[`decide_panel`].",
"type": "object",
"properties": {
"base": {