Checkpoint: offers borrow it; door pinned by source shape; doc states the real guard

EncoderOffer<'cp> borrows its Checkpoint so a Test offer cannot be encoded
after the reading spends it. tests/checkpoint_door.rs scans split.rs for any
public minting item under any name. The split.rs doc says operator tools read
corpus text and the guarded invariant is the round path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Weksa 2026-11-10 02:59:11 +00:00
commit 43f2e118f2
4 changed files with 160 additions and 13 deletions

View file

@ -6,6 +6,8 @@
//! [`ENCODE_INSTRUCTIONS`], consts in this module, and nothing else: `mint` and
//! `encode` take none, so no run-time text can become instructions.
use std::marker::PhantomData;
use crate::corpus::CorpusItem;
use crate::packet::EntityId;
use crate::split::{Checkpoint, Split, SplitScheme};
@ -98,14 +100,42 @@ pub enum OfferRefusal {
/// One item the encoder may be shown, with the entities a packet for it may
/// cite. Built per item, so a call sees one item and nothing of its group.
pub struct EncoderOffer {
///
/// An offer made under a [`Checkpoint`] borrows it, so it is dead before
/// `TestReadings::at_checkpoint` can spend the checkpoint, and a Test offer is
/// never encoded after (or beside) the reading. A round offer borrows nothing.
///
/// ```
/// use weksa::context::{EncoderOffer, TrainItems};
/// use weksa::split::Checkpoint;
/// use weksa::{CorpusItem, score::TestReadings};
/// fn f(mut c: Checkpoint, i: &CorpusItem, t: &TrainItems) {
/// if let Ok(o) = EncoderOffer::for_checkpoint(i, &mut c) {
/// let _ = weksa::context::Prompt::encode(&o, t);
/// }
/// let _ = TestReadings::at_checkpoint(c, &[], &[], &[]);
/// }
/// ```
///
/// ```compile_fail
/// use weksa::context::{EncoderOffer, TrainItems};
/// use weksa::split::Checkpoint;
/// use weksa::{CorpusItem, score::TestReadings};
/// fn f(mut c: Checkpoint, i: &CorpusItem, t: &TrainItems) {
/// let o = EncoderOffer::for_checkpoint(i, &mut c).unwrap();
/// let _ = TestReadings::at_checkpoint(c, &[], &[], &[]);
/// let _ = weksa::context::Prompt::encode(&o, t);
/// }
/// ```
pub struct EncoderOffer<'cp> {
item: CorpusItem,
entities: Vec<EntityId>,
_checkpoint: PhantomData<&'cp mut Checkpoint>,
}
impl EncoderOffer {
impl<'cp> EncoderOffer<'cp> {
/// A round offers Train and Validation items.
pub fn for_round(item: &CorpusItem) -> Result<Self, OfferRefusal> {
pub fn for_round(item: &CorpusItem) -> Result<EncoderOffer<'static>, OfferRefusal> {
match Self::gate(item)? {
Split::Test => Err(OfferRefusal::TestBeforeCheckpoint),
_ => Ok(Self::of(item)),
@ -117,7 +147,7 @@ impl EncoderOffer {
/// item records no key.
pub fn for_checkpoint(
item: &CorpusItem,
checkpoint: &mut Checkpoint,
checkpoint: &'cp mut Checkpoint,
) -> Result<Self, OfferRefusal> {
Self::gate(item)?;
if !checkpoint.first_offer(&item.id) {
@ -133,13 +163,14 @@ impl EncoderOffer {
SplitScheme::V1.live(item).ok_or(OfferRefusal::NoSplit)
}
fn of(item: &CorpusItem) -> Self {
fn of<'a>(item: &CorpusItem) -> EncoderOffer<'a> {
let mut entities: Vec<EntityId> = item.cited_entities().cloned().collect();
entities.sort();
entities.dedup();
Self {
EncoderOffer {
item: item.clone(),
entities,
_checkpoint: PhantomData,
}
}

View file

@ -158,7 +158,7 @@ pub fn aggregate(scored: &[(&CorpusItem, ItemScore)]) -> Aggregate {
/// The Test split's readings, scalars only: no item key, no minted string, no
/// pair. The checkpoint decision (convergence-test) needs numbers; a key beside
/// a minted string is the handle an edit step would act on. The only producer
/// is `at_checkpoint`, which consumes a `Checkpoint`, which only the lib can make.
/// is `at_checkpoint`, which consumes a `Checkpoint`; only the lib can make one.
///
/// ```
/// use weksa::score::TestReadings;

View file

@ -1,7 +1,11 @@
//! The train/validation/test split is derived from an item's group and nothing
//! else. No stored assignment exists, so no writer can reassign one. An item is
//! read in its live split; Test is read only under a Checkpoint, and only the
//! lib can make one (convergence-freeze adds the door).
//! read in its live split. The round path (round records, prompts, `aggregate`,
//! `leak_diagnostic`) carries no Test reading; a typed `TestReadings` and a Test
//! `EncoderOffer` need a Checkpoint, and only the lib can make one
//! (convergence-freeze adds the door). Operator tools (`weksa corpus dump`,
//! `weksa score`, `CorpusStore`) read corpus text of any split; that is not
//! gated here.
use std::collections::BTreeSet;
@ -66,14 +70,18 @@ impl SplitScheme {
}
}
/// Permission to read Test once. Only the weksa lib can make one: there is no
/// public constructor, so nothing outside it (the binary, a driver crate, a
/// doctest, an integration test) can read Test. The one door is added by
/// Permission to read Test once, as typed `TestReadings` or as Test
/// `EncoderOffer`s. Only the weksa lib can make one: there is no public
/// constructor, so no code outside it can obtain those. This is not a seal on
/// corpus text: operator tools and `CorpusStore` can read any item, and the
/// guarded invariant is that the round path holds no Test reading. The one door
/// is added by
/// convergence-freeze, which builds a `Checkpoint` from the convergence window
/// over the run's persisted round records (convergence-test).
/// `TestReadings::at_checkpoint` consumes it, so one `Checkpoint` buys one Test
/// reading. One `Checkpoint` also offers each item at most once
/// (`EncoderOffer::for_checkpoint`).
/// (`EncoderOffer::for_checkpoint`), and an offer borrows its Checkpoint, so it
/// cannot outlive the reading that spends it.
///
/// ```
/// fn f(c: &weksa::split::Checkpoint) -> u32 { c.round() }
@ -85,6 +93,13 @@ impl SplitScheme {
/// let _ = weksa::split::Checkpoint::declare(1);
/// ```
///
/// The test constructor is not exported (its name today; the door scan in
/// `tests/checkpoint_door.rs` pins the shape under any name):
///
/// ```compile_fail
/// let _ = weksa::split::Checkpoint::for_test(1);
/// ```
///
/// ```compile_fail
/// let _ = weksa::split::Checkpoint { round: 1, offered: Default::default() };
/// ```

View file

@ -0,0 +1,101 @@
//! The Checkpoint door is pinned by source shape, not by a name: no public item
//! of the lib can mint one, and nothing may derive, implement or default one.
//! `Checkpoint::for_test` is `cfg(test)`; a doctest names it today, and this
//! scan catches a rename.
/// Why `src` lets code outside the lib make a `Checkpoint`, if it does.
fn door_leaks(split_rs: &str) -> Vec<String> {
let mut leaks = Vec::new();
let lines: Vec<&str> = split_rs.lines().collect();
let mut in_impl = false;
let mut in_struct = false;
for (n, line) in lines.iter().enumerate() {
let t = line.trim();
if t.starts_with("impl Checkpoint") {
in_impl = true;
} else if t.starts_with("impl") && t.contains("Checkpoint") {
leaks.push(format!("{}: trait impl: {t}", n + 1));
}
if t.starts_with("pub struct Checkpoint") {
let derived = lines[..n]
.iter()
.rev()
.take_while(|l| l.trim().starts_with("#[") || l.trim().starts_with("///"))
.any(|l| l.contains("derive"));
if derived {
leaks.push(format!("{}: derive on Checkpoint", n + 1));
}
in_struct = true;
continue;
}
if in_struct {
if t.starts_with('}') {
in_struct = false;
} else if t.starts_with("pub ") {
leaks.push(format!("{}: public field: {t}", n + 1));
}
}
if in_impl && t.starts_with("pub ") && t.contains("fn ") {
let mints = t.contains("-> Self") || t.contains("-> Checkpoint");
let test_only = lines[..n]
.iter()
.rev()
.take_while(|l| l.trim().starts_with("#["))
.any(|l| l.contains("cfg(test)"));
if mints && !test_only {
leaks.push(format!("{}: pub constructor: {t}", n + 1));
}
}
}
leaks
}
#[test]
fn no_public_item_mints_a_checkpoint() {
let src =
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/split.rs")).unwrap();
assert_eq!(door_leaks(&src), Vec::<String>::new());
// No other module returns one either.
for entry in std::fs::read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/src")).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_some_and(|e| e == "rs") && !path.ends_with("split.rs") {
let text = std::fs::read_to_string(&path).unwrap();
let minting = text
.lines()
.filter(|l| l.contains("-> Checkpoint") || l.contains("-> Result<Checkpoint"))
.count();
assert_eq!(minting, 0, "{}", path.display());
}
}
}
#[test]
fn the_scan_sees_each_way_to_open_the_door() {
let good = "impl Checkpoint {\n #[cfg(test)]\n pub(crate) fn for_test(r: u32) -> Self { x }\n}\n";
assert!(door_leaks(good).is_empty());
// Test constructor made public, under its own name or another.
let public = "impl Checkpoint {\n pub fn for_test(r: u32) -> Self { x }\n}\n";
assert_eq!(door_leaks(public).len(), 1);
let renamed = "impl Checkpoint {\n #[cfg(test)]\n #[allow(x)]\n pub fn new(r: u32) -> Checkpoint { x }\n}\n";
assert!(
door_leaks(renamed).is_empty(),
"cfg(test) above another attribute counts"
);
let renamed = "impl Checkpoint {\n pub fn new(r: u32) -> Checkpoint { x }\n}\n";
assert_eq!(door_leaks(renamed).len(), 1);
// cfg(test) on the previous item does not cover this one.
let stray =
"impl Checkpoint {\n #[cfg(test)]\n fn a() {}\n pub fn b() -> Self { x }\n}\n";
assert_eq!(door_leaks(stray).len(), 1);
assert_eq!(door_leaks("impl Default for Checkpoint {}\n").len(), 1);
assert_eq!(door_leaks("impl From<u32> for Checkpoint {}\n").len(), 1);
assert_eq!(
door_leaks("#[derive(Clone)]\npub struct Checkpoint {\n").len(),
1
);
assert_eq!(
door_leaks("pub struct Checkpoint {\n pub round: u32,\n}\n").len(),
1
);
assert!(door_leaks("pub struct Checkpoint {\n round: u32,\n}\n").is_empty());
}