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:
parent
3d83ba5fd2
commit
43f2e118f2
4 changed files with 160 additions and 13 deletions
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() };
|
||||
/// ```
|
||||
|
|
|
|||
101
crates/weksa/tests/checkpoint_door.rs
Normal file
101
crates/weksa/tests/checkpoint_door.rs
Normal 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());
|
||||
}
|
||||
Loading…
Reference in a new issue