From de46ced9ea64087b236e2a47162456c9ee046776 Mon Sep 17 00:00:00 2001 From: Weksa Date: Tue, 10 Nov 2026 08:40:35 +0000 Subject: [PATCH] Round lows 2: length-framed prompt source digest, refused create takes its lock, Prompts drift text, window pins for entities and head, one-write asserts Accepted Recorded prompt digests change with the framing (they drift by design). Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/src/run.rs | 21 ++++++-- crates/weksa/src/split.rs | 24 +++++---- crates/weksa/tests/golf_loop.rs | 92 +++++++++++++++++++++++++++++++-- 3 files changed, 120 insertions(+), 17 deletions(-) diff --git a/crates/weksa/src/run.rs b/crates/weksa/src/run.rs index ee03f0c..1efe18e 100644 --- a/crates/weksa/src/run.rs +++ b/crates/weksa/src/run.rs @@ -40,8 +40,10 @@ use crate::store::{CorpusStore, EntityStore, InventoryStore, PacketStore, open_c /// The digest of the prompt builder's source: `context.rs`, then `split.rs`, /// whose scheme and digest function decide which lines the prompt holds as -/// Train and name every prompt. Any edit to either, comments included, ends a -/// run; code outside those files is not covered. +/// Train and name every prompt. Each file enters the digest behind its length, +/// so the pair cannot be re-cut into another pair with the same digest. Any +/// edit to either, comments included, ends a run; code outside those files is +/// not covered. pub const PROMPT_SOURCE_DIGEST: u64 = fnv1a64_of(&[include_bytes!("context.rs"), include_bytes!("split.rs")]); @@ -349,6 +351,9 @@ impl fmt::Display for RunError { RunError::Exists => f.write_str("the root already holds a run"), RunError::NoRun => f.write_str("the root holds no run"), RunError::Malformed(what) => write!(f, "malformed {what}"), + RunError::Drift(DriftOf::Prompts) => f.write_str( + "drift in Prompts (the prompt source digest covers context.rs and split.rs)", + ), RunError::Drift(of) => write!(f, "drift in {of:?}"), RunError::NoInventory => f.write_str("the inventory holds no revision"), RunError::Io(kind) => write!(f, "io error {kind:?}"), @@ -517,7 +522,7 @@ impl RunStores { let head = root.join(INVENTORY_FILE); copy_new(Path::new(&record.inventory), &head)?; // The copy is ours from here: any later failure takes it away again, or - // it would refuse every retry. + // it would refuse every retry, and so with the lock the open left beside it. let pinned = (|| { let record = RunRecord { corpus_digest: file_digest(&record.corpus).ok_or(RunError::Store)?, @@ -537,7 +542,7 @@ impl RunStores { Ok(run) })(); if pinned.is_err() { - let _ = std::fs::remove_file(&head); + discard_inventory(root); } pinned } @@ -1803,6 +1808,14 @@ mod tests { "write {fail_at} refused" ); assert_eq!(out.is_ok(), fail_at == 2); + if fail_at == 2 { + // The fixture accepts: a drifted literal that made it reject + // would write one round and one decision all the same. + match &out { + Ok(CycleOutcome::Decided(d)) => assert_eq!(d.verdict(), Verdict::Accepted), + _ => panic!("the second write was not a decided cycle"), + } + } } } } diff --git a/crates/weksa/src/split.rs b/crates/weksa/src/split.rs index d360013..898439d 100644 --- a/crates/weksa/src/split.rs +++ b/crates/weksa/src/split.rs @@ -25,26 +25,32 @@ pub enum SplitScheme { } pub const fn fnv1a64(bytes: &[u8]) -> u64 { - fnv1a64_of(&[bytes]) + fnv1a64_fold(0xcbf2_9ce4_8422_2325, bytes) } -/// FNV-1a 64 of the parts laid end to end. +/// FNV-1a 64 of the parts, each preceded by its length as eight little-endian +/// bytes, so no two different splits of the same bytes share a digest. pub const fn fnv1a64_of(parts: &[&[u8]]) -> u64 { let mut h: u64 = 0xcbf2_9ce4_8422_2325; let mut p = 0; while p < parts.len() { - let bytes = parts[p]; - let mut i = 0; - while i < bytes.len() { - h ^= bytes[i] as u64; - h = h.wrapping_mul(0x0000_0100_0000_01b3); - i += 1; - } + h = fnv1a64_fold(h, &(parts[p].len() as u64).to_le_bytes()); + h = fnv1a64_fold(h, parts[p]); p += 1; } h } +const fn fnv1a64_fold(mut h: u64, bytes: &[u8]) -> u64 { + let mut i = 0; + while i < bytes.len() { + h ^= bytes[i] as u64; + h = h.wrapping_mul(0x0000_0100_0000_01b3); + i += 1; + } + h +} + /// 16 lowercase hex digits, zero-padded: how a digest is written everywhere. pub(crate) fn hex16(n: u64) -> String { format!("{n:016x}") diff --git a/crates/weksa/tests/golf_loop.rs b/crates/weksa/tests/golf_loop.rs index 4931140..a044538 100644 --- a/crates/weksa/tests/golf_loop.rs +++ b/crates/weksa/tests/golf_loop.rs @@ -370,8 +370,8 @@ fn a_store_changed_between_its_digest_and_its_open_is_drift() { Some(RunError::Drift(DriftOf::Corpus)) ); - // The same window at create: nothing is pinned and nothing is left behind, - // so the retry creates. + // The same window at create: nothing is pinned and the inventory copy and its + // lock are taken away again, so the retry creates. let mut w = world(); let dir = w.dir.path().to_path_buf(); let root = dir.join("run"); @@ -389,21 +389,101 @@ fn a_store_changed_between_its_digest_and_its_open_is_drift() { Some(RunError::Drift(DriftOf::Corpus)) ); assert!(!root.join("inventory.cc").exists()); + // Not the copy and not the lock the store's open left beside it. + assert!(!root.join("inventory.cc.lock").exists()); assert_eq!(RunStores::open(&root).err(), Some(RunError::NoRun)); RunStores::create(&root, "t", paths(), "fake-model", "bag").unwrap(); } +/// Runs `change` in the window between the pins and the open, at `open` and at +/// `create`, and returns what each refused with plus what `create` left behind. +fn window( + change: impl Fn(&mut common::World, &Path), +) -> (Option, Option, bool, Option) { + let mut w = world(); + let dir = w.dir.path().to_path_buf(); + let root = dir.join("run"); + run_over(&dir); + let opened = RunStores::open_between(&root, || change(&mut w, &root)).err(); + + let mut w = world(); + let dir = w.dir.path().to_path_buf(); + let root = dir.join("run"); + let (c, i, e) = (dir.join("c.cc"), dir.join("i.cc"), dir.join("entities.cc")); + let paths = weksa::run::RunPaths { + corpus: &c, + inventory: &i, + entities: &e, + }; + let created = RunStores::create_between(&root, "t", paths, "fake-model", "bag", || { + change(&mut w, &root) + }) + .err(); + let left = root.join("inventory.cc").exists() || root.join("inventory.cc.lock").exists(); + (opened, created, left, RunStores::open(&root).err()) +} + +#[test] +fn an_entity_store_changed_between_its_digest_and_its_open_is_drift() { + let (opened, created, left, after) = window(|w, _| { + w.entities.put(ent("kay", "en-US", &["Kay"])).unwrap(); + }); + assert_eq!(opened, Some(RunError::Drift(DriftOf::Entities))); + assert_eq!(created, Some(RunError::Drift(DriftOf::Entities))); + assert!(!left); + assert_eq!(after, Some(RunError::NoRun)); +} + +#[test] +fn a_head_inventory_changed_between_its_digest_and_its_open_is_drift() { + let (opened, created, left, after) = window(|_, root| { + weksa::InventoryStore::open(&root.join("inventory.cc")) + .unwrap() + .admit_revision( + weksa::InventoryRevision { + n: 5, + parent: Some(4), + changes: vec![], + frozen: false, + }, + vec![], + ) + .unwrap(); + }); + assert_eq!(opened, Some(RunError::Drift(DriftOf::Inventory))); + assert_eq!(created, Some(RunError::Drift(DriftOf::Inventory))); + assert!(!left); + assert_eq!(after, Some(RunError::NoRun)); +} + #[test] fn source_digests_cover_their_files() { let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); let digest = |name: &str| fnv1a64(&std::fs::read(src.join(name)).unwrap()); // The prompt's partition and names come from split.rs: it is covered too. - let both = [ + let (context, split) = ( std::fs::read(src.join("context.rs")).unwrap(), std::fs::read(src.join("split.rs")).unwrap(), + ); + // Each file enters behind its length, so the pair has one framing. + let framed = [ + (context.len() as u64).to_le_bytes().as_slice(), + &context, + (split.len() as u64).to_le_bytes().as_slice(), + &split, ] .concat(); - assert_eq!(weksa::run::PROMPT_SOURCE_DIGEST, fnv1a64(&both)); + assert_eq!(weksa::run::PROMPT_SOURCE_DIGEST, fnv1a64(&framed)); + assert_ne!( + weksa::run::PROMPT_SOURCE_DIGEST, + fnv1a64(&[context.clone(), split.clone()].concat()), + "bare concatenation is not the digest" + ); + // Moving the boundary between the parts moves the digest. + assert_ne!( + weksa::split::fnv1a64_of(&[b"ab", b"c"]), + weksa::split::fnv1a64_of(&[b"a", b"bc"]) + ); assert_ne!(weksa::run::PROMPT_SOURCE_DIGEST, digest("context.rs")); assert_eq!(weksa::run::LOWERER_SOURCE_DIGEST, digest("lower.rs")); assert_ne!( @@ -1387,6 +1467,10 @@ fn errors_echo_no_input() { format!("{:?}", RunError::Drift(DriftOf::Model)), "drift in Model" ); + // The Prompts text names the two files its digest covers, and only them. + let prompts = RunError::Drift(DriftOf::Prompts).to_string(); + assert!(prompts.contains("context.rs") && prompts.contains("split.rs")); + assert_eq!(format!("{:?}", RunError::Drift(DriftOf::Prompts)), prompts); } #[test] -- 2.47.3