Skip to main content

talk_core/
format.rs

1//! The formatter seam (pure). `talk-core` owns the restraint POLICY: the
2//! `Formatter` contract, the always-safe deterministic fallback, and the
3//! diff-guarded call site. The real Candle 0.5B inference lives in the binary's
4//! `src/format/` and implements this same trait (Plan 3 T7).
5
6use crate::cleanup::{apply_backtrack, apply_spoken_commands, deterministic_light, guard_accepts, Level};
7
8/// Turn one phrase into cleaned text at a given level. Implementors do ONLY their
9/// transform — the deterministic pre-layer and the diff-guard are applied by
10/// `guarded_format`, never here. (So a formatter receives already-pre-processed
11/// text and must not re-apply spoken commands / backtrack.)
12pub trait Formatter {
13    fn format(&self, level: Level, text: &str) -> String;
14}
15
16/// The always-safe formatter: deterministic-Light, no model. Guard-safe by
17/// construction (caps / punctuation / leading-filler only).
18pub struct DeterministicFormatter;
19
20impl Formatter for DeterministicFormatter {
21    /// `level` is intentionally ignored: with no model, every level collapses to
22    /// deterministic-Light (Medium/High word-removal would be rejected by the guard
23    /// anyway). This is the always-present fallback.
24    fn format(&self, _level: Level, text: &str) -> String {
25        deterministic_light(text)
26    }
27}
28
29/// The moat. Pre-layer → format → accept iff the content-word guard passes, else
30/// deterministic-Light. `None` short-circuits to the pre-processed text (no
31/// formatting). The result is ALWAYS guard-safe relative to the pre-processed
32/// phrase — fail-safe is always your words.
33pub fn guarded_format(f: &dyn Formatter, level: Level, raw: &str) -> String {
34    let pre = apply_backtrack(&apply_spoken_commands(raw));
35    if level == Level::None {
36        return pre;
37    }
38    let candidate = f.format(level, &pre);
39    if guard_accepts(&pre, &candidate) {
40        candidate
41    } else {
42        deterministic_light(&pre)
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use crate::test_support::{Faithful, OverEditing};
50
51    #[test]
52    fn none_level_returns_pre_layer_unchanged() {
53        assert_eq!(guarded_format(&Faithful, Level::None, "i am not done"), "i am not done");
54    }
55
56    #[test]
57    fn pre_layer_runs_before_formatting() {
58        let out = guarded_format(&Faithful, Level::Light, "the answer is yes scratch that the answer is no");
59        assert!(!out.contains("yes"));
60        assert!(out.contains("answer is no"));
61    }
62
63    #[test]
64    fn guard_rejects_a_meaning_substitution_and_falls_back() {
65        let out = guarded_format(&OverEditing, Level::Light, "i love her");
66        assert!(!out.contains("hate"));
67        assert!(out.to_lowercase().contains("love"));
68    }
69
70    #[test]
71    fn guard_rejects_a_dropped_negation_and_falls_back() {
72        let out = guarded_format(&OverEditing, Level::Light, "i am not angry");
73        assert!(out.to_lowercase().contains("not"));
74    }
75
76    #[test]
77    fn faithful_output_passes_the_guard_unchanged() {
78        assert_eq!(guarded_format(&Faithful, Level::Light, "um so i keep avoiding it"), "So I keep avoiding it.");
79    }
80
81    #[test]
82    fn guard_fires_at_medium_too() {
83        // Medium/High would remove words, so any LLM rewrite there is rejected and
84        // falls back to deterministic-Light. The guard is level-agnostic.
85        let out = guarded_format(&OverEditing, Level::Medium, "i love her");
86        assert!(!out.contains("hate"));
87        assert!(out.to_lowercase().contains("love"));
88    }
89}