diff --git a/crates/weksa/src/encode.rs b/crates/weksa/src/encode.rs index 9aa08a0..5071316 100644 --- a/crates/weksa/src/encode.rs +++ b/crates/weksa/src/encode.rs @@ -217,8 +217,8 @@ impl LoweringDraft { } /// Whether the text can be scored as an utterance: something after trimming, - /// at most [`MAX_LOWERING_BYTES`], and no control character other than a - /// line break or tab. Nothing is judged about the words. + /// at most [`MAX_LOWERING_BYTES`] bytes (not chars), and no control character other than a + /// newline or tab (a carriage return is refused). Nothing is judged about the words. pub fn usable(&self) -> bool { !self.text.trim().is_empty() && self.text.len() <= MAX_LOWERING_BYTES diff --git a/crates/weksa/src/main.rs b/crates/weksa/src/main.rs index 148f0be..06b3cce 100644 --- a/crates/weksa/src/main.rs +++ b/crates/weksa/src/main.rs @@ -771,7 +771,7 @@ fn main() -> Result<()> { println!("unanswered {} of {asked}", prompts.len()); std::process::exit(3); } - RoundOutcome::Recorded(r) => println!("{}", run.labelled(&r.summary_line())), + RoundOutcome::Recorded(r) => println!("{}", run.round_line(&r)), } } Command::Loop(LoopCommand::Cycle { root, ollama_url }) => { @@ -791,7 +791,7 @@ fn main() -> Result<()> { std::process::exit(3); } CycleOutcome::Decided(decision) => { - println!("{}", run.labelled(&decision.summary_line())) + println!("{}", run.cycle_line(&decision)) } } } diff --git a/crates/weksa/src/run.rs b/crates/weksa/src/run.rs index 1b6d6a3..3d41aa7 100644 --- a/crates/weksa/src/run.rs +++ b/crates/weksa/src/run.rs @@ -317,9 +317,9 @@ impl RoundRecord { self.cycle } - /// The one line a recorded round prints. `n` is every item of the split, + /// The round line, unlabelled: [`RunStores::round_line`] adds how the run was lowered. `n` is every item of the split, /// `scored` those that lowered; a mean counts a refused item as 0. - pub fn summary_line(&self) -> String { + fn line(&self) -> String { let mean = |value: Option, places: usize| { value.map_or("none".to_string(), |v| format!("{v:.places$}")) }; @@ -648,6 +648,16 @@ impl RunStores { } } + /// The line a recorded round prints, labelled with how the run was lowered. + pub fn round_line(&self, round: &RoundRecord) -> String { + self.labelled(&round.line()) + } + + /// The line a closed cycle prints, labelled the same way. + pub fn cycle_line(&self, decision: &Decision) -> String { + self.labelled(&decision.line()) + } + pub fn inventory(&self) -> &InventoryStore { &self.inventory } @@ -1224,8 +1234,8 @@ impl Decision { self.verdict } - /// The one line a closed cycle prints. - pub fn summary_line(&self) -> String { + /// The cycle line, unlabelled: [`RunStores::cycle_line`] adds how the run was lowered. + fn line(&self) -> String { let candidate_rev = self .candidate_rev .map_or("none".to_string(), |n| n.to_string()); diff --git a/crates/weksa/tests/golf_loop.rs b/crates/weksa/tests/golf_loop.rs index 58ee80d..a4bba52 100644 --- a/crates/weksa/tests/golf_loop.rs +++ b/crates/weksa/tests/golf_loop.rs @@ -1428,13 +1428,13 @@ fn the_round_line_prints_each_split_under_its_own_name() { let mut dao = dao(); let mut model = fake(&good_answers()); let r = recorded(round(&mut dao.run, &mut model, &Scorer::new(Bag)).unwrap()); - let line = r.summary_line(); + let line = dao.run.round_line(&r); // Train is item 1 alone, Validation items 2 and 3; the numbers differ. let (t, v) = (r.train_mean(), r.validation_mean()); assert_eq!( line, format!( - "round 1 rev 3 train n=1 scored=1 chrf={:.2} meaning={:.4} refused=0 validation n=2 scored=2 chrf={:.2} meaning={:.4} refused parse=0 admit=0 lower=0", + "round 1 rev 3 train n=1 scored=1 chrf={:.2} meaning={:.4} refused=0 validation n=2 scored=2 chrf={:.2} meaning={:.4} refused parse=0 admit=0 lower=0 lowering=tool-less-relay", t.chrf.unwrap(), t.meaning.unwrap(), v.chrf.unwrap(), @@ -1534,11 +1534,11 @@ fn errors_echo_no_input() { } let d = decided(cycle(&mut canary_run, &mut model, &Scorer::new(Bag)).unwrap()); assert_eq!(d.verdict(), Verdict::Rejected(Why::EditParse)); - all.extend([format!("{d:?}"), d.summary_line()]); + all.extend([format!("{d:?}"), canary_run.cycle_line(&d)]); let mut non_utf8 = cycle_fake(""); non_utf8.edit = Some(Err(PortError::Io(std::io::ErrorKind::InvalidData))); let d = decided(cycle(&mut canary_run, &mut non_utf8, &Scorer::new(Bag)).unwrap()); - all.extend([format!("{d:?}"), d.summary_line()]); + all.extend([format!("{d:?}"), canary_run.cycle_line(&d)]); weksa::InventoryStore::open(&canary_root.join("inventory.cc")) .unwrap() .admit_revision( @@ -1726,9 +1726,9 @@ fn cycle_accepts_a_gain_past_the_floor() { // The line, with the readings printed to four places. let (b, c) = (d.base().composite, d.candidate().unwrap().composite); assert_eq!( - d.summary_line(), + dao.run.cycle_line(&d), format!( - "cycle 1 rev 3->4 base={b:.4} candidate={c:.4} floor=0.0000 threshold=0.0050 accepted" + "cycle 1 rev 3->4 base={b:.4} candidate={c:.4} floor=0.0000 threshold=0.0050 accepted lowering=tool-less-relay" ) ); @@ -1786,9 +1786,9 @@ fn cycle_rejects_no_gain_and_keeps_the_head() { assert_eq!(d.candidate(), Some(d.base())); let c = d.base().composite; assert_eq!( - d.summary_line(), + dao.run.cycle_line(&d), format!( - "cycle 1 rev 3->4 base={c:.4} candidate={c:.4} floor=0.0000 threshold=0.0050 rejected no-gain" + "cycle 1 rev 3->4 base={c:.4} candidate={c:.4} floor=0.0000 threshold=0.0050 rejected no-gain lowering=tool-less-relay" ) ); assert_eq!(head_rev(&dao.root), 3); @@ -2207,9 +2207,9 @@ fn run_the_editor_canary(canary_lowered: &str) { // Nothing the cycle writes or prints holds a held-out key or text. for text in [ format!("{d:?}"), - d.summary_line(), + dao.run.cycle_line(&d), format!("{d2:?}"), - d2.summary_line(), + dao.run.cycle_line(&d2), ] { for needle in &held_out { assert!( @@ -2312,9 +2312,9 @@ fn edit_refusals_are_decisions() { "edit-admit" }; assert_eq!( - d.summary_line(), + dao.run.cycle_line(&d), format!( - "cycle 1 rev 3->none base={:.4} candidate=none floor=0.0000 threshold=0.0050 rejected {tag}", + "cycle 1 rev 3->none base={:.4} candidate=none floor=0.0000 threshold=0.0050 rejected {tag} lowering=tool-less-relay", d.base().composite ), "{name}" @@ -2679,6 +2679,12 @@ fn lowering_refusals_count_at_lower() { ("non-UTF-8", Err(PortError::Io(std::io::ErrorKind::InvalidData))), ("NUL", Ok(serde_json::json!({ "text": "The sage\u{0}acts." }).to_string())), ("bell", Ok(serde_json::json!({ "text": "The sage \u{7}acts." }).to_string())), + ("carriage return", Ok(serde_json::json!({ "text": "The sage\racts." }).to_string())), + ( + "over length in bytes, within it in chars", + Ok(serde_json::json!({ "text": "\u{e9}".repeat(weksa::encode::MAX_LOWERING_BYTES / 2 + 1) }) + .to_string()), + ), ( "over length", Ok(serde_json::json!({ "text": "a".repeat(weksa::encode::MAX_LOWERING_BYTES + 1) }) @@ -2776,6 +2782,7 @@ fn a_summary_line_says_how_lowerings_were_answered() { fn lowering_text_at_the_bound_is_scored_and_line_breaks_are_text() { for text in [ "a".repeat(weksa::encode::MAX_LOWERING_BYTES), + "\u{e9}".repeat(weksa::encode::MAX_LOWERING_BYTES / 2), "The sage acts.\n\tThe sage rests.".to_string(), ] { let mut dao = dao();