1use crate::kernel::*;
8
9pub struct InputBuilder {
27 input: KernelInput,
28}
29
30impl InputBuilder {
31 #[must_use]
33 pub fn new(request_sequence: u64, requested_model_id: u32) -> Self {
34 Self {
35 input: KernelInput {
36 request_sequence,
37 requested_model_id,
38 input_tokens: 0,
39 output_tokens: 0,
40 business_value_microunits: 0,
41 budget_limit_microunits: u64::MAX,
42 risk_bps: 0,
43 confidence_bps: BASIS_POINTS as u16,
44 minimum_quality_bps: 0,
45 max_p95_latency_ms: 0,
46 required_capabilities: 0,
47 allowed_provider_mask: ALL_PROVIDERS,
48 required_region_mask: 0,
49 },
50 }
51 }
52
53 #[must_use]
54 pub fn tokens(mut self, input: u32, output: u32) -> Self {
55 self.input.input_tokens = input;
56 self.input.output_tokens = output;
57 self
58 }
59
60 #[must_use]
61 pub fn business_value(mut self, microunits: i64) -> Self {
62 self.input.business_value_microunits = microunits;
63 self
64 }
65
66 #[must_use]
67 pub fn budget_limit(mut self, microunits: u64) -> Self {
68 self.input.budget_limit_microunits = microunits;
69 self
70 }
71
72 #[must_use]
74 pub fn risk(mut self, risk_bps: u16, confidence_bps: u16) -> Self {
75 self.input.risk_bps = risk_bps;
76 self.input.confidence_bps = confidence_bps;
77 self
78 }
79
80 #[must_use]
81 pub fn minimum_quality(mut self, bps: u16) -> Self {
82 self.input.minimum_quality_bps = bps;
83 self
84 }
85
86 #[must_use]
87 pub fn max_latency(mut self, ms: u32) -> Self {
88 self.input.max_p95_latency_ms = ms;
89 self
90 }
91
92 #[must_use]
93 pub fn capabilities(mut self, mask: u64) -> Self {
94 self.input.required_capabilities = mask;
95 self
96 }
97
98 #[must_use]
99 pub fn providers(mut self, mask: u64) -> Self {
100 self.input.allowed_provider_mask = mask;
101 self
102 }
103
104 #[must_use]
105 pub fn regions(mut self, mask: u64) -> Self {
106 self.input.required_region_mask = mask;
107 self
108 }
109
110 #[must_use]
117 pub fn build(self) -> KernelInput {
118 self.try_build()
119 .expect("KernelInput validation failed — use try_build() for fallible construction")
120 }
121
122 pub fn try_build(self) -> Result<KernelInput, InputError> {
124 self.input.validate()?;
125 Ok(self.input)
126 }
127}
128
129pub struct ModelBuilder {
144 model: KernelModel,
145}
146
147impl ModelBuilder {
148 #[must_use]
150 pub fn new(model_id: u32, provider_id: u16) -> Self {
151 Self {
152 model: KernelModel {
153 model_id,
154 provider_id,
155 quality_bps: 8_000,
156 risk_ceiling_bps: 9_500,
157 enabled: 1,
158 p95_latency_ms: 200,
159 capabilities: 0,
160 region_mask: ALL_REGIONS,
161 input_cost_microunits_per_million_tokens: 0,
162 output_cost_microunits_per_million_tokens: 0,
163 },
164 }
165 }
166
167 #[must_use]
168 pub fn quality(mut self, bps: u16) -> Self {
169 self.model.quality_bps = bps;
170 self
171 }
172
173 #[must_use]
174 pub fn risk_ceiling(mut self, bps: u16) -> Self {
175 self.model.risk_ceiling_bps = bps;
176 self
177 }
178
179 #[must_use]
180 pub fn enabled(mut self, yes: bool) -> Self {
181 self.model.enabled = u8::from(yes);
182 self
183 }
184
185 #[must_use]
186 pub fn latency(mut self, p95_ms: u32) -> Self {
187 self.model.p95_latency_ms = p95_ms;
188 self
189 }
190
191 #[must_use]
192 pub fn capabilities(mut self, mask: u64) -> Self {
193 self.model.capabilities = mask;
194 self
195 }
196
197 #[must_use]
198 pub fn regions(mut self, mask: u64) -> Self {
199 self.model.region_mask = mask;
200 self
201 }
202
203 #[must_use]
205 pub fn cost(mut self, input_per_m: u64, output_per_m: u64) -> Self {
206 self.model.input_cost_microunits_per_million_tokens = input_per_m;
207 self.model.output_cost_microunits_per_million_tokens = output_per_m;
208 self
209 }
210
211 #[must_use]
213 pub fn build(self) -> KernelModel {
214 self.model
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
220pub enum BuildError {
221 #[error("config error: {0}")]
222 Config(#[from] crate::config::ConfigError),
223 #[error("policy error: {0}")]
224 Policy(#[from] crate::kernel::PolicyError),
225 #[error("catalog too large: {len} models exceeds max_catalog_size {max}")]
226 CatalogTooLarge { len: usize, max: usize },
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
235pub enum TrustedBuildError {
236 #[error("config error: {0}")]
237 Config(#[from] crate::config::ConfigError),
238 #[error("catalog too large: {len} models exceeds max_catalog_size {max}")]
239 CatalogTooLarge { len: usize, max: usize },
240 #[error("trusted policy error: {0}")]
241 Trust(#[from] crate::kernel::TrustPolicyError),
242}
243
244pub struct PolicyBuilder {
260 config: crate::config::EngineConfig,
261 policy_epoch: u64,
262 catalog_epoch: u64,
263 models: Vec<KernelModel>,
264}
265
266impl PolicyBuilder {
267 #[must_use]
269 pub fn new(config: crate::config::EngineConfig) -> Self {
270 Self {
271 config,
272 policy_epoch: 1,
273 catalog_epoch: 1,
274 models: Vec::new(),
275 }
276 }
277
278 #[must_use]
280 pub fn epochs(mut self, policy: u64, catalog: u64) -> Self {
281 self.policy_epoch = policy;
282 self.catalog_epoch = catalog;
283 self
284 }
285
286 #[must_use]
288 pub fn model(mut self, model: KernelModel) -> Self {
289 self.models.push(model);
290 self
291 }
292
293 #[must_use]
295 pub fn models(mut self, models: impl IntoIterator<Item = KernelModel>) -> Self {
296 self.models.extend(models);
297 self
298 }
299
300 pub fn build(self) -> Result<PolicySnapshot, BuildError> {
305 match self.build_trusted() {
306 Ok(snapshot) => Ok(snapshot),
307 Err(TrustedBuildError::Config(error)) => Err(BuildError::Config(error)),
308 Err(TrustedBuildError::CatalogTooLarge { len, max }) => {
309 Err(BuildError::CatalogTooLarge { len, max })
310 }
311 Err(TrustedBuildError::Trust(crate::kernel::TrustPolicyError::Policy(error))) => {
312 Err(BuildError::Policy(error))
313 }
314 Err(TrustedBuildError::Trust(crate::kernel::TrustPolicyError::CatalogTooLarge {
315 len,
316 max,
317 })) => Err(BuildError::CatalogTooLarge { len, max }),
318 Err(TrustedBuildError::Trust(crate::kernel::TrustPolicyError::ReservedModelId)) => Err(
319 BuildError::Policy(crate::kernel::PolicyError::DuplicateModelId { model_id: 0 }),
320 ),
321 Err(TrustedBuildError::Trust(
322 crate::kernel::TrustPolicyError::InvalidEnabledFlag { value, .. },
323 )) => Err(BuildError::Policy(
324 crate::kernel::PolicyError::OutOfRangeBps {
325 field: "model.enabled",
326 value: u16::from(value),
327 max: 1,
328 },
329 )),
330 }
331 }
332
333 pub fn build_trusted(self) -> Result<PolicySnapshot, TrustedBuildError> {
335 self.config.validate()?;
336 if self.models.len() > self.config.max_catalog_size {
337 return Err(TrustedBuildError::CatalogTooLarge {
338 len: self.models.len(),
339 max: self.config.max_catalog_size,
340 });
341 }
342 Ok(PolicySnapshot::try_new_trusted(
343 self.policy_epoch,
344 self.catalog_epoch,
345 self.config.hard_risk_limit_bps,
346 self.config.minimum_confidence_bps,
347 self.config.risk_penalty_multiplier_bps,
348 self.config.latency_penalty_microunits_per_ms,
349 self.models,
350 )?)
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn input_builder_defaults() {
360 let input = InputBuilder::new(1, 10).tokens(500, 200).build();
361 assert_eq!(input.request_sequence, 1);
362 assert_eq!(input.requested_model_id, 10);
363 assert_eq!(input.input_tokens, 500);
364 assert_eq!(input.allowed_provider_mask, ALL_PROVIDERS);
365 assert_eq!(input.required_region_mask, 0);
366 assert_eq!(input.budget_limit_microunits, u64::MAX);
367 }
368
369 #[test]
370 fn model_builder_defaults() {
371 let model = ModelBuilder::new(1, 0).cost(100, 400).build();
372 assert_eq!(model.model_id, 1);
373 assert_eq!(model.enabled, 1);
374 assert_eq!(model.quality_bps, 8_000);
375 assert_eq!(model.risk_ceiling_bps, 9_500);
376 }
377
378 #[test]
379 fn policy_builder_roundtrip() {
380 let config = crate::config::EngineConfig::new();
381 let snap = PolicyBuilder::new(config)
382 .epochs(7, 11)
383 .model(
384 ModelBuilder::new(1, 0)
385 .quality(9000)
386 .cost(250, 1000)
387 .build(),
388 )
389 .model(ModelBuilder::new(2, 1).quality(7000).cost(25, 125).build())
390 .build()
391 .unwrap();
392 assert_eq!(snap.policy_epoch, 7);
393 assert_eq!(snap.models().len(), 2);
394 }
395
396 #[test]
397 fn policy_builder_rejects_reserved_model_id() {
398 let result = PolicyBuilder::new(crate::config::EngineConfig::new())
399 .model(ModelBuilder::new(0, 0).build())
400 .build();
401 assert!(matches!(
402 result,
403 Err(BuildError::Policy(
404 crate::kernel::PolicyError::DuplicateModelId { model_id: 0 }
405 ))
406 ));
407 }
408
409 #[test]
410 fn trusted_policy_builder_preserves_precise_trust_boundary_errors() {
411 let reserved = PolicyBuilder::new(crate::config::EngineConfig::new())
412 .model(ModelBuilder::new(0, 0).build())
413 .build_trusted();
414 assert!(matches!(
415 reserved,
416 Err(TrustedBuildError::Trust(
417 crate::kernel::TrustPolicyError::ReservedModelId
418 ))
419 ));
420
421 let mut model = ModelBuilder::new(1, 0).build();
422 model.enabled = 2;
423 let invalid_enabled = PolicyBuilder::new(crate::config::EngineConfig::new())
424 .model(model)
425 .build_trusted();
426 assert!(matches!(
427 invalid_enabled,
428 Err(TrustedBuildError::Trust(
429 crate::kernel::TrustPolicyError::InvalidEnabledFlag {
430 model_id: 1,
431 value: 2
432 }
433 ))
434 ));
435 }
436
437 #[test]
438 fn policy_builder_rejects_noncanonical_enabled_flag() {
439 let mut model = ModelBuilder::new(1, 0).build();
440 model.enabled = 2;
441 let result = PolicyBuilder::new(crate::config::EngineConfig::new())
442 .model(model)
443 .build();
444 assert!(matches!(
445 result,
446 Err(BuildError::Policy(
447 crate::kernel::PolicyError::OutOfRangeBps {
448 field: "model.enabled",
449 value: 2,
450 max: 1
451 }
452 ))
453 ));
454 }
455
456 #[test]
457 fn builder_integrates_with_prescribe() {
458 let config = crate::config::EngineConfig::new();
459 let snap = PolicyBuilder::new(config)
460 .model(ModelBuilder::new(1, 0).quality(9000).cost(100, 400).build())
461 .build()
462 .unwrap();
463 let input = InputBuilder::new(1, 1)
464 .tokens(1000, 500)
465 .business_value(100_000)
466 .risk(1000, 9000)
467 .minimum_quality(5000)
468 .build();
469 let decision = snap.prescribe(input);
470 assert!(decision.is_executable());
471 }
472
473 #[test]
474 fn disabled_model_via_builder() {
475 let model = ModelBuilder::new(1, 0).enabled(false).build();
476 assert_eq!(model.enabled, 0);
477 }
478
479 #[test]
480 fn catalog_too_large_rejected() {
481 let config = crate::config::EngineConfig::new().max_catalog_size(1);
482 let result = PolicyBuilder::new(config)
483 .model(ModelBuilder::new(1, 0).cost(100, 400).build())
484 .model(ModelBuilder::new(2, 1).cost(10, 40).build())
485 .build();
486 assert!(matches!(result, Err(BuildError::CatalogTooLarge { .. })));
487 }
488
489 #[test]
490 fn invalid_config_rejected_at_build() {
491 let config = crate::config::EngineConfig::new().hard_risk_limit(10_001);
492 let result = PolicyBuilder::new(config)
493 .model(ModelBuilder::new(1, 0).cost(100, 400).build())
494 .build();
495 assert!(matches!(result, Err(BuildError::Config(_))));
496 }
497
498 use proptest::prelude::*;
499
500 proptest! {
501 #[test]
502 fn builder_prescribe_never_panics(
503 seq in any::<u64>(),
504 model_id in 1_u32..=2,
505 input_tokens in any::<u32>(),
506 output_tokens in any::<u32>(),
507 value in any::<i64>(),
508 risk in 0_u16..=MAX_BPS,
509 confidence in 0_u16..=MAX_BPS,
510 ) {
511 let config = crate::config::EngineConfig::new();
512 let snap = PolicyBuilder::new(config)
513 .model(ModelBuilder::new(1, 0).quality(9000).cost(100, 400).build())
514 .model(ModelBuilder::new(2, 1).quality(7000).cost(10, 40).build())
515 .build()
516 .unwrap();
517 let input = InputBuilder::new(seq, model_id)
518 .tokens(input_tokens, output_tokens)
519 .business_value(value)
520 .risk(risk, confidence)
521 .try_build()
522 .expect("valid bounded bps");
523 let _ = snap.prescribe(input);
524 }
525 }
526}