From e7b4d9a68b3337c304653d0ad46f775c1c45126d Mon Sep 17 00:00:00 2001 From: Weksa Date: Tue, 10 Nov 2026 03:45:49 +0000 Subject: [PATCH 1/9] RunStores is the one public producer of TrainItems and EncoderOffer: a run pins its corpus and entity stores by digest TrainItems::from_corpus and EncoderOffer::for_round become pub(crate). RunStores (run.rs) creates a run over three stores, records their canonical paths and FNV-1a 64 digests plus the prompt and lowerer source digests in run.cc, and reopens only from that record, refusing on drift. RunRecord and RoundRecord are published CultCache types. tests/context.rs and the context.rs doctests move to RunStores; the two mutated-clone gate cases become a unit test. Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/src/context.rs | 85 +++- crates/weksa/src/lib.rs | 1 + crates/weksa/src/run.rs | 433 +++++++++++++++++++ crates/weksa/src/schema.rs | 3 + crates/weksa/src/score/mod.rs | 4 +- crates/weksa/src/split.rs | 13 +- crates/weksa/src/store.rs | 2 +- crates/weksa/tests/cli.rs | 2 + crates/weksa/tests/common/mod.rs | 22 + crates/weksa/tests/context.rs | 105 +++-- crates/weksa/tests/golf_loop.rs | 145 +++++++ schemas/cultnet/weksa.loop_round.schema.json | 227 ++++++++++ schemas/cultnet/weksa.loop_run.schema.json | 58 +++ 13 files changed, 1029 insertions(+), 71 deletions(-) create mode 100644 crates/weksa/src/run.rs create mode 100644 crates/weksa/tests/golf_loop.rs create mode 100644 schemas/cultnet/weksa.loop_round.schema.json create mode 100644 schemas/cultnet/weksa.loop_run.schema.json diff --git a/crates/weksa/src/context.rs b/crates/weksa/src/context.rs index a4f2749..2936d7a 100644 --- a/crates/weksa/src/context.rs +++ b/crates/weksa/src/context.rs @@ -3,8 +3,14 @@ //! [`TrainItems`] or by encoding one item an [`EncoderOffer`] holds: Train-only //! minting and per-item encoding (leak-owned-by-flow-and-scorer-3, //! loop-context-builder-2). Instruction text is [`MINT_INSTRUCTIONS`] and -//! [`ENCODE_INSTRUCTIONS`], consts in this module, and nothing else: `mint` and -//! `encode` take none, so no run-time text can become instructions. +//! [`ENCODE_INSTRUCTIONS`], consts in this module; an encode prompt also lists +//! the inventory at the run's revision. +//! +//! Item blocks are data from the run's pinned corpus, reached only through +//! `RunStores`, the one public producer of `TrainItems` and `EncoderOffer` +//! (forged-item-text-in-prompt). A forged item in a scratch store is a different +//! run with a different corpus digest, visible in its record; nothing inside a +//! process its forger controls can stop it. use std::marker::PhantomData; @@ -25,9 +31,12 @@ pub const ENCODE_INSTRUCTIONS: &str = "Encode the line below."; /// item. /// /// ``` -/// let dir = tempfile::tempdir().unwrap(); -/// let store = weksa::CorpusStore::open(&dir.path().join("c.cc")).unwrap(); -/// let t = weksa::context::TrainItems::from_corpus(&store).unwrap(); +/// # 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").unwrap(); +/// let t = run.train().unwrap(); /// assert!(t.is_empty()); /// ``` /// @@ -36,17 +45,23 @@ pub const ENCODE_INSTRUCTIONS: &str = "Encode the line below."; /// ``` /// /// ```compile_fail -/// let dir = tempfile::tempdir().unwrap(); -/// let store = weksa::CorpusStore::open(&dir.path().join("c.cc")).unwrap(); -/// let t = weksa::context::TrainItems::from_corpus(&store).unwrap(); +/// # 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").unwrap(); +/// let t = run.train().unwrap(); /// let _u = t.clone(); /// ``` /// /// ```compile_fail /// fn s(_: &T) {} -/// let dir = tempfile::tempdir().unwrap(); -/// let store = weksa::CorpusStore::open(&dir.path().join("c.cc")).unwrap(); -/// let t = weksa::context::TrainItems::from_corpus(&store).unwrap(); +/// # 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").unwrap(); +/// let t = run.train().unwrap(); /// s(&t); /// ``` pub struct TrainItems { @@ -54,7 +69,7 @@ pub struct TrainItems { } impl TrainItems { - pub fn from_corpus(corpus: &CorpusStore) -> anyhow::Result { + pub(crate) fn from_corpus(corpus: &CorpusStore) -> anyhow::Result { let items = corpus .all()? .into_iter() @@ -134,8 +149,9 @@ pub struct EncoderOffer<'cp> { } impl<'cp> EncoderOffer<'cp> { - /// A round offers Train and Validation items. - pub fn for_round(item: &CorpusItem) -> Result, OfferRefusal> { + /// A round offers Train and Validation items. `RunStores::offer` and + /// `RunStores::offers` are the public producers. + pub(crate) fn for_round(item: &CorpusItem) -> Result, OfferRefusal> { match Self::gate(item)? { Split::Test => Err(OfferRefusal::TestBeforeCheckpoint), _ => Ok(Self::of(item)), @@ -188,9 +204,12 @@ impl<'cp> EncoderOffer<'cp> { /// constructors are [`Prompt::mint`] and [`Prompt::encode`]. /// /// ``` -/// let dir = tempfile::tempdir().unwrap(); -/// let store = weksa::CorpusStore::open(&dir.path().join("c.cc")).unwrap(); -/// let t = weksa::context::TrainItems::from_corpus(&store).unwrap(); +/// # 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").unwrap(); +/// let t = run.train().unwrap(); /// let p = weksa::context::Prompt::mint(&t); /// assert_eq!(p.text(), weksa::context::MINT_INSTRUCTIONS); /// ``` @@ -198,17 +217,23 @@ impl<'cp> EncoderOffer<'cp> { /// Instructions are not a parameter: /// /// ```compile_fail -/// let dir = tempfile::tempdir().unwrap(); -/// let store = weksa::CorpusStore::open(&dir.path().join("c.cc")).unwrap(); -/// let t = weksa::context::TrainItems::from_corpus(&store).unwrap(); +/// # 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").unwrap(); +/// let t = run.train().unwrap(); /// let p = weksa::context::Prompt::mint("x", &t); /// ``` /// /// ```compile_fail /// fn s(_: &T) {} -/// let dir = tempfile::tempdir().unwrap(); -/// let store = weksa::CorpusStore::open(&dir.path().join("c.cc")).unwrap(); -/// let t = weksa::context::TrainItems::from_corpus(&store).unwrap(); +/// # 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").unwrap(); +/// let t = run.train().unwrap(); /// let p = weksa::context::Prompt::mint(&t); /// s(&p); /// ``` @@ -303,6 +328,20 @@ mod tests { assert_eq!(check(&both), Some(OfferRefusal::Withdrawn)); } + #[test] + fn round_offer_gate_precedence() { + let round = |i: &CorpusItem| EncoderOffer::for_round(i).err(); + let mut no_split = item("item:c:2", "m", &group_in(Split::Test), true); + no_split.withdrawn = true; + // Withdrawn wins over NoSplit. + assert_eq!(round(&no_split), Some(OfferRefusal::Withdrawn)); + // A withdrawn Test item is Withdrawn, not TestBeforeCheckpoint. + let mut test = item("item:c:1", "x", &group_in(Split::Test), false); + assert_eq!(round(&test), Some(OfferRefusal::TestBeforeCheckpoint)); + test.withdrawn = true; + assert_eq!(round(&test), Some(OfferRefusal::Withdrawn)); + } + #[test] fn checkpoint_offers_each_item_once() { let mut cp = Checkpoint::for_test(1); diff --git a/crates/weksa/src/lib.rs b/crates/weksa/src/lib.rs index f8043a3..1b8c3c8 100644 --- a/crates/weksa/src/lib.rs +++ b/crates/weksa/src/lib.rs @@ -12,6 +12,7 @@ pub mod inventory; pub mod lower; pub mod packet; pub mod phones; +pub mod run; pub mod schema; pub mod score; pub mod speaker; diff --git a/crates/weksa/src/run.rs b/crates/weksa/src/run.rs new file mode 100644 index 0000000..8b892d0 --- /dev/null +++ b/crates/weksa/src/run.rs @@ -0,0 +1,433 @@ +//! One golf-loop run: the stores it pinned at open, the rounds it has recorded, +//! and the one writer of both (`RunStores::create` and, for rounds, `round`). +//! +//! A run pins its corpus and entity store files by canonical path and FNV-1a 64 +//! digest of their bytes. Every round reopens them only from that record and +//! refuses on drift, and every round record carries the digests. Nothing inside +//! a process whose writer controls the code can stop an item faked into a +//! scratch store; what this makes true is that a run over a scratch store is a +//! different run with a different corpus digest, visible in its record, and a +//! later reader compares the rounds of one run only. `RunStores` is the one +//! public producer of [`TrainItems`] and [`EncoderOffer`]: the CLI paths that +//! take caller stores (`validate`, `packet proof|dao`, `lower`) are unchanged. + +use std::fmt; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; + +use cultcache_rs::{CultCache, DatabaseEntry}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::context::{EncoderOffer, TrainItems}; +use crate::form::{is_attestation, is_slug}; +use crate::score::{CHRF_VERSION, SplitMean}; +use crate::split::{fnv1a64, hex16}; +use crate::store::{CorpusStore, EntityStore, InventoryStore, open_cache}; + +/// The digest of the prompt builder's source. Any edit to `context.rs`, +/// comments included, ends a run; code outside that file is not covered. +pub const PROMPT_SOURCE_DIGEST: u64 = fnv1a64(include_bytes!("context.rs")); + +/// The digest of the lowerer's source. Any edit to `lower.rs`, comments +/// included, ends a run; code outside that file is not covered. +pub const LOWERER_SOURCE_DIGEST: u64 = fnv1a64(include_bytes!("lower.rs")); + +/// What a run pinned at open. Key `run: