Skip to main content

poly_kv/
policy.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::{PolyKvError, Result};
4
5/// String identifier for a codec.
6pub type CodecId = String;
7
8/// Well-known codec identifiers.
9pub const CODEC_FIB_K4_N32: &str = "fib_k4_n32";
10pub const CODEC_TURBO_8BIT: &str = "turbo_8bit";
11pub const CODEC_EXACT_FALLBACK: &str = "exact_f32_fallback";
12
13/// FibQuant configuration parameters.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct FibConfig {
16    /// Block dimension k.
17    pub k: u32,
18    /// Codebook size N.
19    pub n: u32,
20    /// Number of training samples for Lloyd-Max refinement.
21    pub training_samples: u32,
22    /// Number of Lloyd restarts.
23    pub lloyd_restarts: u32,
24    /// Number of Lloyd iterations per restart.
25    pub lloyd_iterations: u32,
26}
27
28impl FibConfig {
29    /// The benchmark-proven configuration: k=4, N=32, 50× compression with 100% recall.
30    pub fn default_k4_n32() -> Self {
31        Self {
32            k: 4,
33            n: 32,
34            training_samples: 2048,
35            lloyd_restarts: 4,
36            lloyd_iterations: 8,
37        }
38    }
39
40    /// Validate the configuration.
41    pub fn validate(&self) -> Result<()> {
42        if self.k == 0 {
43            return Err(PolyKvError::InvalidPolicy("fib k must be > 0".into()));
44        }
45        if self.n < 2 {
46            return Err(PolyKvError::InvalidPolicy("fib N must be >= 2".into()));
47        }
48        if self.k > 256 {
49            return Err(PolyKvError::InvalidPolicy("fib k must be <= 256".into()));
50        }
51        if self.n > 1_048_576 {
52            return Err(PolyKvError::InvalidPolicy(
53                "fib N must be <= 1,048,576".into(),
54            ));
55        }
56        if self.training_samples == 0 {
57            return Err(PolyKvError::InvalidPolicy(
58                "fib training_samples must be > 0".into(),
59            ));
60        }
61        if self.lloyd_restarts == 0 {
62            return Err(PolyKvError::InvalidPolicy(
63                "fib lloyd_restarts must be > 0".into(),
64            ));
65        }
66        if self.lloyd_iterations == 0 {
67            return Err(PolyKvError::InvalidPolicy(
68                "fib lloyd_iterations must be > 0".into(),
69            ));
70        }
71        Ok(())
72    }
73
74    /// Expected compression ratio (input f32 bytes / compressed bytes).
75    /// For k=4, N=32: each block of 4 f32 values (16 bytes) maps to a
76    /// ceil(log2(32)) = 5-bit index (+norm header), yielding roughly 50:1.
77    /// Uses fib-quant's actual nominal_compression_ratio when available.
78    pub fn nominal_compression_ratio(&self) -> f64 {
79        50.0
80    }
81
82    /// Expected codebook-based compression ratio (50× target for k=4,N=32).
83    pub fn expected_compression_ratio(&self) -> f64 {
84        50.0
85    }
86}
87
88/// TurboQuant configuration parameters.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct TurboConfig {
91    /// Bits per scalar (8 for the benchmark-proven path).
92    pub bits: u8,
93    /// Number of QJL projections for the residual sketch (32 for benchmark path).
94    pub projections: usize,
95}
96
97impl TurboConfig {
98    /// The benchmark-proven configuration: 8-bit, 32 projections, 8× compression.
99    pub fn default_8bit() -> Self {
100        Self {
101            bits: 8,
102            projections: 32,
103        }
104    }
105
106    /// Validate the configuration.
107    pub fn validate(&self) -> Result<()> {
108        if self.bits < 2 || self.bits > 16 {
109            return Err(PolyKvError::InvalidPolicy(format!(
110                "turbo bits must be 2–16, got {}",
111                self.bits
112            )));
113        }
114        if self.projections == 0 {
115            return Err(PolyKvError::InvalidPolicy(
116                "turbo projections must be > 0".into(),
117            ));
118        }
119        Ok(())
120    }
121
122    /// Expected compression ratio (8× target for 8-bit).
123    pub fn expected_compression_ratio(&self) -> f64 {
124        8.0
125    }
126}
127
128/// Hard-coded two-tier compression policy.
129///
130/// Derived from empirical benchmarks run on 2026-06-01:
131/// - Shared pool (cold tier): fib-quant at k=4, N=32 → 50× compression, 100% recall
132/// - Agent shells (hot tier): turbo-quant at 8-bit → 8× compression, 99.9% score retention
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134pub struct CompressionPolicy {
135    /// Codec used for the shared pool (cold tier).
136    pub shared_codec: CodecId,
137    /// Codec used for agent shells (hot tier).
138    pub shell_codec: CodecId,
139    /// FibQuant configuration.
140    pub fib_config: FibConfig,
141    /// TurboQuant configuration.
142    pub turbo_config: TurboConfig,
143}
144
145impl CompressionPolicy {
146    /// Create the default benchmark-proven two-tier policy.
147    pub fn default_two_tier() -> Self {
148        Self {
149            shared_codec: CODEC_FIB_K4_N32.into(),
150            shell_codec: CODEC_TURBO_8BIT.into(),
151            fib_config: FibConfig::default_k4_n32(),
152            turbo_config: TurboConfig::default_8bit(),
153        }
154    }
155
156    /// Validate the policy.
157    pub fn validate(&self) -> Result<()> {
158        if self.shared_codec != CODEC_FIB_K4_N32 && self.shared_codec != CODEC_EXACT_FALLBACK {
159            return Err(PolyKvError::InvalidPolicy(format!(
160                "shared_codec must be '{}' or '{}', got '{}'",
161                CODEC_FIB_K4_N32, CODEC_EXACT_FALLBACK, self.shared_codec
162            )));
163        }
164        if self.shell_codec != CODEC_TURBO_8BIT && self.shell_codec != CODEC_EXACT_FALLBACK {
165            return Err(PolyKvError::InvalidPolicy(format!(
166                "shell_codec must be '{}' or '{}', got '{}'",
167                CODEC_TURBO_8BIT, CODEC_EXACT_FALLBACK, self.shell_codec
168            )));
169        }
170        self.fib_config.validate()?;
171        self.turbo_config.validate()?;
172        Ok(())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_default_policy_validates() {
182        let policy = CompressionPolicy::default_two_tier();
183        assert!(policy.validate().is_ok());
184    }
185
186    #[test]
187    fn test_fib_config_invalid_k_rejected() {
188        let mut config = FibConfig::default_k4_n32();
189        config.k = 0;
190        assert!(config.validate().is_err());
191    }
192
193    #[test]
194    fn test_fib_config_invalid_n_rejected() {
195        let mut config = FibConfig::default_k4_n32();
196        config.n = 1;
197        assert!(config.validate().is_err());
198    }
199
200    #[test]
201    fn test_turbo_config_invalid_bits_rejected() {
202        let mut config = TurboConfig::default_8bit();
203        config.bits = 1;
204        assert!(config.validate().is_err());
205    }
206}