Skip to main content

calybris_core/
kernel.rs

1//! Allocation-free prescriptive decision kernel.
2//!
3//! This module deliberately contains no HTTP, JSON, UUID, WAL, clock, or floating-point work.
4//! Snapshots may allocate when they are built; [`PolicySnapshot::prescribe`](crate::kernel::PolicySnapshot::prescribe) does not allocate.
5//!
6//! # Performance
7//!
8//! ~8.6M decisions/sec on CodSpeed CI (Linux x86_64, release, ~115ns/decision,
9//! 22-model synthetic catalog). Hardware and workload dependent — see `benches/kernel_bench.rs`.
10//! All arithmetic is `u64`/`i64`/`i128` — no `f64` anywhere in the hot path.
11//!
12//! # Safety
13//!
14//! 11 constraint gates are evaluated per decision. If no model passes all gates
15//! with positive utility, the request is rejected (fail-closed).
16
17use std::sync::Arc;
18
19/// One basis point = 1/10,000. Used for quality, risk, and confidence values.
20pub const BASIS_POINTS: u64 = 10_000;
21const SCALED_BASIS_POINTS: u64 = BASIS_POINTS * BASIS_POINTS;
22const COST_SCALE: u64 = 1_000_000;
23const COST_ROUNDING: u64 = COST_SCALE - 1;
24/// Bitmask that admits all providers (all 64 bits set).
25pub const ALL_PROVIDERS: u64 = u64::MAX;
26/// Bitmask that admits all regions (all 64 bits set).
27pub const ALL_REGIONS: u64 = u64::MAX;
28/// Maximum representable provider ID. IDs >= 64 are unconditionally rejected.
29pub const MAX_PROVIDER_ID: u16 = 63;
30/// Largest catalog whose count and zero-based indices fit the public `u16` decision fields.
31pub const MAX_CATALOG_MODELS: usize = u16::MAX as usize;
32
33/// A candidate model in the decision catalog.
34#[repr(C)]
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
38pub struct KernelModel {
39    /// Unique identifier for this model.
40    pub model_id: u32,
41    /// Provider index (0–63). IDs > [`MAX_PROVIDER_ID`] are rejected.
42    pub provider_id: u16,
43    /// Quality score in basis points (0–10,000).
44    pub quality_bps: u16,
45    /// Maximum risk this model can handle, in basis points.
46    pub risk_ceiling_bps: u16,
47    /// 1 = enabled, 0 = disabled (skipped during evaluation).
48    pub enabled: u8,
49    /// 95th-percentile latency in milliseconds.
50    pub p95_latency_ms: u32,
51    /// Bitmask of capabilities this model supports.
52    pub capabilities: u64,
53    /// Bitmask of regions where this model is available.
54    pub region_mask: u64,
55    /// Input cost per million tokens, in microunits (1 cent = 1,000,000).
56    pub input_cost_microunits_per_million_tokens: u64,
57    /// Output cost per million tokens, in microunits.
58    pub output_cost_microunits_per_million_tokens: u64,
59}
60
61/// A decision request to be evaluated against the policy.
62#[repr(C)]
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
66pub struct KernelInput {
67    /// Monotonic sequence number for this request.
68    pub request_sequence: u64,
69    /// The model the caller originally requested.
70    pub requested_model_id: u32,
71    /// Number of input tokens.
72    pub input_tokens: u32,
73    /// Number of output tokens.
74    pub output_tokens: u32,
75    /// Expected business value of this request, in microunits.
76    pub business_value_microunits: i64,
77    /// Maximum cost allowed, in microunits.
78    pub budget_limit_microunits: u64,
79    /// Risk level of this request, in basis points (0 = safe, 10,000 = max).
80    pub risk_bps: u16,
81    /// Confidence in the risk estimate, in basis points.
82    pub confidence_bps: u16,
83    /// Minimum acceptable quality, in basis points.
84    pub minimum_quality_bps: u16,
85    /// Maximum acceptable p95 latency (0 = no limit).
86    pub max_p95_latency_ms: u32,
87    /// Required capability bitmask (all bits must match).
88    pub required_capabilities: u64,
89    /// Allowed provider bitmask ([`ALL_PROVIDERS`] = any).
90    pub allowed_provider_mask: u64,
91    /// Required region bitmask (0 = no constraint).
92    pub required_region_mask: u64,
93}
94
95/// Input boundary validation errors.
96#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
97pub enum InputError {
98    #[error("{field} must be <= {max}, got {value}")]
99    OutOfRangeBps {
100        field: &'static str,
101        value: u16,
102        max: u16,
103    },
104}
105
106impl KernelInput {
107    /// Validate basis-point fields before the hot prescribe path.
108    ///
109    /// Call at API boundaries (Python bindings, HTTP gateways, builders) so
110    /// out-of-range values cannot silently truncate via `u16` casts.
111    pub fn validate(&self) -> Result<(), InputError> {
112        if self.risk_bps > MAX_BPS {
113            return Err(InputError::OutOfRangeBps {
114                field: "risk_bps",
115                value: self.risk_bps,
116                max: MAX_BPS,
117            });
118        }
119        if self.confidence_bps > MAX_BPS {
120            return Err(InputError::OutOfRangeBps {
121                field: "confidence_bps",
122                value: self.confidence_bps,
123                max: MAX_BPS,
124            });
125        }
126        if self.minimum_quality_bps > MAX_BPS {
127            return Err(InputError::OutOfRangeBps {
128                field: "minimum_quality_bps",
129                value: self.minimum_quality_bps,
130                max: MAX_BPS,
131            });
132        }
133        Ok(())
134    }
135}
136
137/// The action the kernel decided to take.
138#[repr(u8)]
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub enum KernelAction {
142    /// The requested model was selected (it maximized utility).
143    ExecuteRequested = 1,
144    /// A different model was selected (it had higher utility).
145    Substitute = 2,
146    /// No model passed all constraints with positive utility.
147    Reject = 3,
148}
149
150impl std::fmt::Display for KernelAction {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        match self {
153            Self::ExecuteRequested => write!(f, "execute_requested"),
154            Self::Substitute => write!(f, "substitute"),
155            Self::Reject => write!(f, "reject"),
156        }
157    }
158}
159
160/// Why the kernel made this decision.
161#[repr(u16)]
162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
164pub enum KernelReason {
165    /// The requested model had the highest utility.
166    RequestedModelMaximizesUtility = 1,
167    /// A substitute model had higher utility.
168    AlternativeMaximizesUtility = 2,
169    /// Request risk exceeded the hard limit.
170    RiskHardLimit = 100,
171    /// Request confidence was below the minimum.
172    ConfidenceHardLimit = 101,
173    /// No enabled models in the catalog.
174    NoEnabledModel = 102,
175    /// All models failed the quality floor.
176    QualityConstraint = 103,
177    /// All models exceeded the latency cap.
178    LatencyConstraint = 104,
179    /// No model had the required capabilities.
180    CapabilityConstraint = 105,
181    /// No model matched the allowed provider mask.
182    ProviderConstraint = 106,
183    /// No model matched the required region mask.
184    RegionConstraint = 107,
185    /// All models exceeded the budget limit.
186    BudgetConstraint = 108,
187    /// All eligible models had non-positive utility.
188    NonPositiveUtility = 109,
189    /// Request risk exceeded the model's risk ceiling.
190    RiskCeilingConstraint = 110,
191}
192
193impl std::fmt::Display for KernelReason {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        match self {
196            Self::RequestedModelMaximizesUtility => write!(f, "requested_model_maximizes_utility"),
197            Self::AlternativeMaximizesUtility => write!(f, "alternative_maximizes_utility"),
198            Self::RiskHardLimit => write!(f, "risk_hard_limit"),
199            Self::ConfidenceHardLimit => write!(f, "confidence_hard_limit"),
200            Self::NoEnabledModel => write!(f, "no_enabled_model"),
201            Self::QualityConstraint => write!(f, "quality_constraint"),
202            Self::LatencyConstraint => write!(f, "latency_constraint"),
203            Self::CapabilityConstraint => write!(f, "capability_constraint"),
204            Self::ProviderConstraint => write!(f, "provider_constraint"),
205            Self::RegionConstraint => write!(f, "region_constraint"),
206            Self::BudgetConstraint => write!(f, "budget_constraint"),
207            Self::NonPositiveUtility => write!(f, "non_positive_utility"),
208            Self::RiskCeilingConstraint => write!(f, "risk_ceiling_constraint"),
209        }
210    }
211}
212
213/// The result of evaluating a [`KernelInput`] against a [`PolicySnapshot`].
214#[repr(C)]
215#[derive(Clone, Copy, Debug, Eq, PartialEq)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
218pub struct KernelDecision {
219    /// Echoed from the input.
220    pub request_sequence: u64,
221    /// What to do: execute, substitute, or reject.
222    pub action: KernelAction,
223    /// Why this action was chosen.
224    pub reason: KernelReason,
225    /// The model that was selected (0 if rejected).
226    pub selected_model_id: u32,
227    /// Index of the selected model in the catalog.
228    pub selected_model_index: u16,
229    /// Estimated cost of the selected model, in microunits.
230    pub estimated_cost_microunits: u64,
231    /// Expected utility of the selected model.
232    pub expected_utility_microunits: i64,
233    /// The second-best model (for counterfactual analysis).
234    pub counterfactual_model_id: u32,
235    /// Utility of the counterfactual model.
236    pub counterfactual_utility_microunits: i64,
237    /// How many models were evaluated.
238    pub evaluated_models: u16,
239    /// How many models passed all constraints.
240    pub eligible_models: u16,
241    /// Policy version used for this decision.
242    pub policy_epoch: u64,
243    /// Catalog version used for this decision.
244    pub catalog_epoch: u64,
245}
246
247impl KernelDecision {
248    /// Returns `true` when the decision selected a model that may be executed.
249    #[must_use]
250    pub const fn is_executable(&self) -> bool {
251        matches!(
252            self.action,
253            KernelAction::ExecuteRequested | KernelAction::Substitute
254        )
255    }
256
257    /// Returns `true` when the requested model was selected.
258    #[must_use]
259    pub const fn is_requested_execution(&self) -> bool {
260        matches!(self.action, KernelAction::ExecuteRequested)
261    }
262
263    /// Returns `true` when a different eligible model was selected.
264    #[must_use]
265    pub const fn is_substitution(&self) -> bool {
266        matches!(self.action, KernelAction::Substitute)
267    }
268
269    /// Returns `true` when the request failed closed and no model was selected.
270    #[must_use]
271    pub const fn is_rejected(&self) -> bool {
272        matches!(self.action, KernelAction::Reject)
273    }
274}
275
276/// An immutable snapshot of the decision policy and model catalog.
277///
278/// Create production policies with [`PolicySnapshot::try_new_trusted`] or `PolicyBuilder`.
279/// [`PolicySnapshot::try_new`] remains for legacy replay compatibility, while
280/// [`PolicySnapshot::new_unchecked`] is an explicit test/fixture escape hatch.
281/// then call [`prescribe`](PolicySnapshot::prescribe)
282/// for each request. The snapshot is `Clone` and can be shared across threads via `Arc`.
283#[derive(Clone, Debug)]
284pub struct PolicySnapshot {
285    pub policy_epoch: u64,
286    pub catalog_epoch: u64,
287    pub hard_risk_limit_bps: u16,
288    pub minimum_confidence_bps: u16,
289    pub risk_penalty_multiplier_bps: u16,
290    /// Cost penalty per millisecond of p95 latency, in microunits.
291    ///
292    /// No static upper bound — overflow is dynamically guarded by
293    /// `all_latencies_fit` which falls back to `i128` arithmetic when
294    /// `max_p95_latency_ms * latency_penalty_microunits_per_ms` would
295    /// overflow `u64`.
296    pub latency_penalty_microunits_per_ms: u64,
297    max_quality_bps: u16,
298    max_p95_latency_ms: u32,
299    max_input_cost: u64,
300    max_output_cost: u64,
301    models: Arc<[KernelModel]>,
302}
303
304#[derive(Clone, Copy)]
305struct Candidate {
306    model_id: u32,
307    model_index: u16,
308    quality_bps: u16,
309    cost: u64,
310    utility: i64,
311}
312
313/// Per-constraint rejection counts from a single prescribe evaluation.
314#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
315#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
316pub struct RejectionHistogram {
317    /// Models skipped because `enabled == 0`.
318    pub disabled: u16,
319    /// Models below `minimum_quality_bps`.
320    pub quality: u16,
321    /// Models where request risk exceeds model risk ceiling.
322    pub risk_ceiling: u16,
323    /// Models above latency cap.
324    pub latency: u16,
325    /// Models missing required capabilities.
326    pub capability: u16,
327    /// Models filtered by provider mask or unrepresentable provider id.
328    pub provider: u16,
329    /// Models filtered by region mask.
330    pub region: u16,
331    /// Models above budget limit.
332    pub budget: u16,
333    /// Eligible models with non-positive utility.
334    pub utility: u16,
335}
336
337/// Explainability snapshot for a prescribe call (alloc-free alongside decision).
338#[derive(Clone, Copy, Debug, PartialEq, Eq)]
339#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
340pub struct DecisionTrace {
341    pub rejections: RejectionHistogram,
342    pub evaluated_models: u16,
343    pub eligible_models: u16,
344}
345
346/// Maximum basis-points value accepted by policy validation (100%).
347pub const MAX_BPS: u16 = 10_000;
348
349/// Upper bound for [`PolicySnapshot::risk_penalty_multiplier_bps`] validation.
350pub const MAX_RISK_PENALTY_MULTIPLIER_BPS: u16 = 50_000;
351
352/// Policy catalog validation errors.
353#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
354pub enum PolicyError {
355    #[error("model catalog is empty")]
356    EmptyCatalog,
357    #[error("duplicate model_id {model_id}")]
358    DuplicateModelId { model_id: u32 },
359    #[error("model_id {model_id} has provider_id {provider_id} > MAX_PROVIDER_ID")]
360    InvalidProviderId { model_id: u32, provider_id: u16 },
361    #[error("no enabled models in catalog")]
362    NoEnabledModels,
363    #[error("{field} must be <= {max}, got {value}")]
364    OutOfRangeBps {
365        field: &'static str,
366        value: u16,
367        max: u16,
368    },
369}
370
371/// Additional trust-boundary validation errors for canonical policy construction.
372#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
373pub enum TrustPolicyError {
374    #[error("policy validation failed: {0}")]
375    Policy(#[from] PolicyError),
376    #[error("model catalog has {len} entries; maximum is {max}")]
377    CatalogTooLarge { len: usize, max: usize },
378    #[error("model_id 0 is reserved for decisions with no selected model")]
379    ReservedModelId,
380    #[error("model_id {model_id} has non-canonical enabled flag {value}; expected 0 or 1")]
381    InvalidEnabledFlag { model_id: u32, value: u8 },
382}
383
384type RejectionCounts = RejectionHistogram;
385
386impl PolicySnapshot {
387    /// Creates a policy snapshot **without validation** (escape hatch).
388    ///
389    /// # Production guidance
390    ///
391    /// Use [`try_new_trusted`](Self::try_new_trusted) or `PolicyBuilder` for any policy served
392    /// to real traffic. `new_unchecked`
393    /// is for unit tests, fuzz fixtures, and experiments where you call [`validate`](Self::validate)
394    /// yourself or intentionally construct invalid catalogs.
395    ///
396    /// Serving from an unchecked snapshot without a successful `validate()` is an audit failure.
397    pub fn new_unchecked(
398        policy_epoch: u64,
399        catalog_epoch: u64,
400        hard_risk_limit_bps: u16,
401        minimum_confidence_bps: u16,
402        risk_penalty_multiplier_bps: u16,
403        latency_penalty_microunits_per_ms: u64,
404        models: Vec<KernelModel>,
405    ) -> Self {
406        let max_quality_bps = models
407            .iter()
408            .map(|model| model.quality_bps)
409            .max()
410            .unwrap_or_default();
411        let max_p95_latency_ms = models
412            .iter()
413            .map(|model| model.p95_latency_ms)
414            .max()
415            .unwrap_or_default();
416        let max_input_cost = models
417            .iter()
418            .map(|model| model.input_cost_microunits_per_million_tokens)
419            .max()
420            .unwrap_or_default();
421        let max_output_cost = models
422            .iter()
423            .map(|model| model.output_cost_microunits_per_million_tokens)
424            .max()
425            .unwrap_or_default();
426        Self {
427            policy_epoch,
428            catalog_epoch,
429            hard_risk_limit_bps,
430            minimum_confidence_bps,
431            risk_penalty_multiplier_bps,
432            latency_penalty_microunits_per_ms,
433            max_quality_bps,
434            max_p95_latency_ms,
435            max_input_cost,
436            max_output_cost,
437            models: Arc::from(models),
438        }
439    }
440
441    /// Creates a new policy snapshot from a model catalog (alias for [`new_unchecked`](Self::new_unchecked)).
442    #[deprecated(
443        since = "0.3.9",
444        note = "use PolicySnapshot::try_new for validated snapshots or new_unchecked for tests"
445    )]
446    pub fn new(
447        policy_epoch: u64,
448        catalog_epoch: u64,
449        hard_risk_limit_bps: u16,
450        minimum_confidence_bps: u16,
451        risk_penalty_multiplier_bps: u16,
452        latency_penalty_microunits_per_ms: u64,
453        models: Vec<KernelModel>,
454    ) -> Self {
455        Self::new_unchecked(
456            policy_epoch,
457            catalog_epoch,
458            hard_risk_limit_bps,
459            minimum_confidence_bps,
460            risk_penalty_multiplier_bps,
461            latency_penalty_microunits_per_ms,
462            models,
463        )
464    }
465
466    /// Returns the model catalog.
467    pub fn models(&self) -> &[KernelModel] {
468        &self.models
469    }
470
471    /// Validate catalog and basis-point invariants before serving traffic.
472    pub fn validate(&self) -> Result<(), PolicyError> {
473        if self.hard_risk_limit_bps > MAX_BPS {
474            return Err(PolicyError::OutOfRangeBps {
475                field: "hard_risk_limit_bps",
476                value: self.hard_risk_limit_bps,
477                max: MAX_BPS,
478            });
479        }
480        if self.minimum_confidence_bps > MAX_BPS {
481            return Err(PolicyError::OutOfRangeBps {
482                field: "minimum_confidence_bps",
483                value: self.minimum_confidence_bps,
484                max: MAX_BPS,
485            });
486        }
487        if self.risk_penalty_multiplier_bps > MAX_RISK_PENALTY_MULTIPLIER_BPS {
488            return Err(PolicyError::OutOfRangeBps {
489                field: "risk_penalty_multiplier_bps",
490                value: self.risk_penalty_multiplier_bps,
491                max: MAX_RISK_PENALTY_MULTIPLIER_BPS,
492            });
493        }
494        if self.models.is_empty() {
495            return Err(PolicyError::EmptyCatalog);
496        }
497        let mut seen = std::collections::HashSet::new();
498        let mut any_enabled = false;
499        for model in self.models.iter() {
500            if !seen.insert(model.model_id) {
501                return Err(PolicyError::DuplicateModelId {
502                    model_id: model.model_id,
503                });
504            }
505            if model.provider_id > MAX_PROVIDER_ID {
506                return Err(PolicyError::InvalidProviderId {
507                    model_id: model.model_id,
508                    provider_id: model.provider_id,
509                });
510            }
511            if model.quality_bps > MAX_BPS {
512                return Err(PolicyError::OutOfRangeBps {
513                    field: "model.quality_bps",
514                    value: model.quality_bps,
515                    max: MAX_BPS,
516                });
517            }
518            if model.risk_ceiling_bps > MAX_BPS {
519                return Err(PolicyError::OutOfRangeBps {
520                    field: "model.risk_ceiling_bps",
521                    value: model.risk_ceiling_bps,
522                    max: MAX_BPS,
523                });
524            }
525            if model.enabled != 0 {
526                any_enabled = true;
527            }
528        }
529        if !any_enabled {
530            return Err(PolicyError::NoEnabledModels);
531        }
532        Ok(())
533    }
534
535    /// Build and validate a legacy-compatible snapshot.
536    ///
537    /// This constructor remains available for deterministic replay of older artifacts.
538    /// New production policy boundaries should use [`try_new_trusted`](Self::try_new_trusted).
539    pub fn try_new(
540        policy_epoch: u64,
541        catalog_epoch: u64,
542        hard_risk_limit_bps: u16,
543        minimum_confidence_bps: u16,
544        risk_penalty_multiplier_bps: u16,
545        latency_penalty_microunits_per_ms: u64,
546        models: Vec<KernelModel>,
547    ) -> Result<Self, PolicyError> {
548        let snapshot = Self::new_unchecked(
549            policy_epoch,
550            catalog_epoch,
551            hard_risk_limit_bps,
552            minimum_confidence_bps,
553            risk_penalty_multiplier_bps,
554            latency_penalty_microunits_per_ms,
555            models,
556        );
557        snapshot.validate()?;
558        Ok(snapshot)
559    }
560
561    /// Build a validated, canonically ordered policy for new trust-boundary integrations.
562    ///
563    /// Unlike the legacy [`try_new`](Self::try_new) constructor, this reserves
564    /// model ID zero for rejected decisions and rejects catalogs that cannot be
565    /// represented exactly by the public `u16` decision counters. The legacy
566    /// constructor remains available so v1 artifacts can still be replayed.
567    pub fn try_new_trusted(
568        policy_epoch: u64,
569        catalog_epoch: u64,
570        hard_risk_limit_bps: u16,
571        minimum_confidence_bps: u16,
572        risk_penalty_multiplier_bps: u16,
573        latency_penalty_microunits_per_ms: u64,
574        mut models: Vec<KernelModel>,
575    ) -> Result<Self, TrustPolicyError> {
576        if models.len() > MAX_CATALOG_MODELS {
577            return Err(TrustPolicyError::CatalogTooLarge {
578                len: models.len(),
579                max: MAX_CATALOG_MODELS,
580            });
581        }
582        if models.iter().any(|model| model.model_id == 0) {
583            return Err(TrustPolicyError::ReservedModelId);
584        }
585        if let Some(model) = models.iter().find(|model| model.enabled > 1) {
586            return Err(TrustPolicyError::InvalidEnabledFlag {
587                model_id: model.model_id,
588                value: model.enabled,
589            });
590        }
591        models.sort_by_key(|model| model.model_id);
592        let snapshot = Self::new_unchecked(
593            policy_epoch,
594            catalog_epoch,
595            hard_risk_limit_bps,
596            minimum_confidence_bps,
597            risk_penalty_multiplier_bps,
598            latency_penalty_microunits_per_ms,
599            models,
600        );
601        snapshot.validate()?;
602        Ok(snapshot)
603    }
604
605    /// Evaluate many inputs. Allocates the output vector only.
606    pub fn prescribe_batch(&self, inputs: &[KernelInput]) -> Vec<KernelDecision> {
607        inputs.iter().map(|&input| self.prescribe(input)).collect()
608    }
609
610    /// Validate and evaluate many inputs, failing before producing decisions
611    /// when any request is structurally invalid.
612    pub fn prescribe_batch_checked(
613        &self,
614        inputs: &[KernelInput],
615    ) -> Result<Vec<KernelDecision>, InputError> {
616        for input in inputs {
617            input.validate()?;
618        }
619        Ok(self.prescribe_batch(inputs))
620    }
621
622    /// Evaluate `input` and return decision plus rejection histogram.
623    pub fn prescribe_with_trace(&self, input: KernelInput) -> (KernelDecision, DecisionTrace) {
624        let (decision, rejections) = self.prescribe_inner(input);
625        let trace = DecisionTrace {
626            rejections,
627            evaluated_models: decision.evaluated_models,
628            eligible_models: decision.eligible_models,
629        };
630        (decision, trace)
631    }
632
633    /// Validate `input`, then evaluate it with an explainability trace.
634    pub fn prescribe_with_trace_checked(
635        &self,
636        input: KernelInput,
637    ) -> Result<(KernelDecision, DecisionTrace), InputError> {
638        input.validate()?;
639        Ok(self.prescribe_with_trace(input))
640    }
641
642    /// Evaluate `input` against the policy and return the optimal decision.
643    ///
644    /// The kernel checks 11 constraint gates per candidate, computes utility as
645    /// `quality_adjusted_value - risk_penalty - cost - latency_penalty`,
646    /// and selects the candidate with the highest positive utility.
647    ///
648    /// If no candidate has positive utility, the request is rejected (fail-closed).
649    /// The decision also records the counterfactual (second-best) candidate.
650    ///
651    /// **This function does not allocate.**
652    #[must_use]
653    pub fn prescribe(&self, input: KernelInput) -> KernelDecision {
654        self.prescribe_inner(input).0
655    }
656
657    /// Validate `input`, then evaluate it.
658    ///
659    /// This is the recommended API at untrusted Rust boundaries. The unchecked
660    /// [`prescribe`](Self::prescribe) path remains available for callers that
661    /// have already validated or constructed the input through a safe builder.
662    pub fn prescribe_checked(&self, input: KernelInput) -> Result<KernelDecision, InputError> {
663        input.validate()?;
664        Ok(self.prescribe(input))
665    }
666
667    /// Evaluate utility for a **specific** catalog model if it passes all constraint gates.
668    ///
669    /// Unlike [`prescribe`](Self::prescribe), this does not rank candidates — it only
670    /// answers whether `model_id` is eligible and what its utility would be.
671    #[must_use]
672    pub fn utility_for_model(&self, input: KernelInput, model_id: u32) -> Option<i64> {
673        if input.risk_bps >= self.hard_risk_limit_bps {
674            return None;
675        }
676        if input.confidence_bps < self.minimum_confidence_bps {
677            return None;
678        }
679        let model = self.models.iter().find(|m| m.model_id == model_id)?;
680
681        if model.enabled == 0 {
682            return None;
683        }
684        if model.quality_bps < input.minimum_quality_bps {
685            return None;
686        }
687        if input.max_p95_latency_ms > 0 && model.p95_latency_ms > input.max_p95_latency_ms {
688            return None;
689        }
690        if model.capabilities & input.required_capabilities != input.required_capabilities {
691            return None;
692        }
693        if model.provider_id > MAX_PROVIDER_ID {
694            return None;
695        }
696        if input.allowed_provider_mask != ALL_PROVIDERS
697            && input.allowed_provider_mask & (1_u64 << model.provider_id) == 0
698        {
699            return None;
700        }
701        if input.required_region_mask != 0 && model.region_mask & input.required_region_mask == 0 {
702            return None;
703        }
704        if input.risk_bps > model.risk_ceiling_bps {
705            return None;
706        }
707
708        let all_costs_fit = self.all_costs_fit_u64(input.input_tokens, input.output_tokens);
709        let cost = if all_costs_fit {
710            model_cost_fast(model, input.input_tokens, input.output_tokens)
711        } else {
712            model_cost_reference(model, input.input_tokens, input.output_tokens)
713        };
714        if cost > input.budget_limit_microunits {
715            return None;
716        }
717
718        let value = input.business_value_microunits.max(0) as u64;
719        let confidence_bps = u64::from(input.confidence_bps);
720        let quality_prefix = value.checked_mul(confidence_bps).filter(|prefix| {
721            prefix
722                .checked_mul(u64::from(self.max_quality_bps))
723                .is_some()
724        });
725        let risk_penalty = scaled_term_exact(
726            value,
727            u64::from(input.risk_bps),
728            u64::from(self.risk_penalty_multiplier_bps),
729        );
730        let all_latencies_fit = u64::from(self.max_p95_latency_ms)
731            .checked_mul(self.latency_penalty_microunits_per_ms)
732            .is_some();
733        let quality_adjusted = quality_prefix.map_or_else(
734            || scaled_term_reference(value, confidence_bps, u64::from(model.quality_bps)),
735            |prefix| {
736                i128::from(prefix.wrapping_mul(u64::from(model.quality_bps)) / SCALED_BASIS_POINTS)
737            },
738        );
739        let latency_penalty = if all_latencies_fit {
740            i128::from(
741                u64::from(model.p95_latency_ms)
742                    .wrapping_mul(self.latency_penalty_microunits_per_ms),
743            )
744        } else {
745            i128::from(model.p95_latency_ms) * i128::from(self.latency_penalty_microunits_per_ms)
746        };
747        let utility =
748            clamp_i128_to_i64(quality_adjusted - risk_penalty - i128::from(cost) - latency_penalty);
749        if utility <= 0 {
750            return None;
751        }
752        Some(utility)
753    }
754
755    fn prescribe_inner(&self, input: KernelInput) -> (KernelDecision, RejectionHistogram) {
756        if input.risk_bps >= self.hard_risk_limit_bps {
757            return self.reject(input, KernelReason::RiskHardLimit, 0, 0);
758        }
759        if input.confidence_bps < self.minimum_confidence_bps {
760            return self.reject(input, KernelReason::ConfidenceHardLimit, 0, 0);
761        }
762
763        let mut best: Option<Candidate> = None;
764        let mut second: Option<Candidate> = None;
765        let mut eligible_models = 0_u16;
766        let mut rejected = RejectionCounts::default();
767
768        let value = input.business_value_microunits.max(0) as u64;
769        let confidence_bps = u64::from(input.confidence_bps);
770        let quality_prefix = value.checked_mul(confidence_bps).filter(|prefix| {
771            prefix
772                .checked_mul(u64::from(self.max_quality_bps))
773                .is_some()
774        });
775        let risk_penalty = scaled_term_exact(
776            value,
777            u64::from(input.risk_bps),
778            u64::from(self.risk_penalty_multiplier_bps),
779        );
780        let all_costs_fit = self.all_costs_fit_u64(input.input_tokens, input.output_tokens);
781        let all_latencies_fit = u64::from(self.max_p95_latency_ms)
782            .checked_mul(self.latency_penalty_microunits_per_ms)
783            .is_some();
784
785        let check_provider = input.allowed_provider_mask != ALL_PROVIDERS;
786        let check_region = input.required_region_mask != 0;
787        let check_latency = input.max_p95_latency_ms > 0;
788        let latency_pen_per_ms = self.latency_penalty_microunits_per_ms;
789
790        for (index, model) in self.models.iter().enumerate() {
791            // Fast reject chain — ordered by cheapest check first
792            if model.enabled == 0 {
793                rejected.disabled += 1;
794                continue;
795            }
796            if model.quality_bps < input.minimum_quality_bps {
797                rejected.quality += 1;
798                continue;
799            }
800            if check_latency && model.p95_latency_ms > input.max_p95_latency_ms {
801                rejected.latency += 1;
802                continue;
803            }
804            if model.capabilities & input.required_capabilities != input.required_capabilities {
805                rejected.capability += 1;
806                continue;
807            }
808            // Provider fence: provider_id >= 64 is always unrepresentable in a
809            // 64-bit mask, so reject unconditionally regardless of ALL_PROVIDERS.
810            if model.provider_id > MAX_PROVIDER_ID {
811                rejected.provider += 1;
812                continue;
813            }
814            if check_provider && input.allowed_provider_mask & (1_u64 << model.provider_id) == 0 {
815                rejected.provider += 1;
816                continue;
817            }
818            if check_region && model.region_mask & input.required_region_mask == 0 {
819                rejected.region += 1;
820                continue;
821            }
822            if input.risk_bps > model.risk_ceiling_bps {
823                rejected.risk_ceiling += 1;
824                continue;
825            }
826
827            let cost = if all_costs_fit {
828                model_cost_fast(model, input.input_tokens, input.output_tokens)
829            } else {
830                model_cost_reference(model, input.input_tokens, input.output_tokens)
831            };
832            if cost > input.budget_limit_microunits {
833                rejected.budget += 1;
834                continue;
835            }
836
837            let quality_adjusted = quality_prefix.map_or_else(
838                || scaled_term_reference(value, confidence_bps, u64::from(model.quality_bps)),
839                |prefix| {
840                    // `quality_prefix` is admitted only after proving this product fits.
841                    i128::from(
842                        prefix.wrapping_mul(u64::from(model.quality_bps)) / SCALED_BASIS_POINTS,
843                    )
844                },
845            );
846            let latency_penalty = if all_latencies_fit {
847                i128::from(u64::from(model.p95_latency_ms).wrapping_mul(latency_pen_per_ms))
848            } else {
849                i128::from(model.p95_latency_ms) * i128::from(latency_pen_per_ms)
850            };
851            let utility = clamp_i128_to_i64(
852                quality_adjusted - risk_penalty - i128::from(cost) - latency_penalty,
853            );
854
855            if utility <= 0 {
856                rejected.utility += 1;
857                continue;
858            }
859            eligible_models = eligible_models.saturating_add(1);
860
861            let candidate = Candidate {
862                model_id: model.model_id,
863                model_index: u16::try_from(index).unwrap_or(u16::MAX),
864                quality_bps: model.quality_bps,
865                cost,
866                utility,
867            };
868            match best {
869                None => best = Some(candidate),
870                Some(current) if candidate_better(candidate, current) => {
871                    second = best;
872                    best = Some(candidate);
873                }
874                _ => {
875                    if second.is_none_or(|s| candidate_better(candidate, s)) {
876                        second = Some(candidate);
877                    }
878                }
879            }
880        }
881
882        let evaluated_models = u16::try_from(self.models.len()).unwrap_or(u16::MAX);
883        let Some(best) = best else {
884            let decision = self
885                .reject(
886                    input,
887                    dominant_rejection_reason(&rejected),
888                    evaluated_models,
889                    eligible_models,
890                )
891                .0;
892            return (decision, rejected);
893        };
894        let action = if best.model_id == input.requested_model_id {
895            KernelAction::ExecuteRequested
896        } else {
897            KernelAction::Substitute
898        };
899        (
900            KernelDecision {
901                request_sequence: input.request_sequence,
902                action,
903                reason: if action == KernelAction::ExecuteRequested {
904                    KernelReason::RequestedModelMaximizesUtility
905                } else {
906                    KernelReason::AlternativeMaximizesUtility
907                },
908                selected_model_id: best.model_id,
909                selected_model_index: best.model_index,
910                estimated_cost_microunits: best.cost,
911                expected_utility_microunits: best.utility,
912                counterfactual_model_id: second.map_or(0, |candidate| candidate.model_id),
913                counterfactual_utility_microunits: second.map_or(0, |candidate| candidate.utility),
914                evaluated_models,
915                eligible_models,
916                policy_epoch: self.policy_epoch,
917                catalog_epoch: self.catalog_epoch,
918            },
919            rejected,
920        )
921    }
922
923    #[inline]
924    fn all_costs_fit_u64(&self, input_tokens: u32, output_tokens: u32) -> bool {
925        let input = u64::from(input_tokens)
926            .checked_mul(self.max_input_cost)
927            .and_then(|value| value.checked_add(COST_ROUNDING));
928        let output = u64::from(output_tokens)
929            .checked_mul(self.max_output_cost)
930            .and_then(|value| value.checked_add(COST_ROUNDING));
931        input
932            .zip(output)
933            .is_some_and(|(input, output)| input.checked_add(output).is_some())
934    }
935
936    /// Build a rejection decision. Returns a default (all-zero) histogram because
937    /// hard-limit rejections (risk, confidence) exit before model evaluation — no
938    /// per-model constraint counts are available.
939    fn reject(
940        &self,
941        input: KernelInput,
942        reason: KernelReason,
943        evaluated_models: u16,
944        eligible_models: u16,
945    ) -> (KernelDecision, RejectionHistogram) {
946        (
947            KernelDecision {
948                request_sequence: input.request_sequence,
949                action: KernelAction::Reject,
950                reason,
951                selected_model_id: 0,
952                selected_model_index: u16::MAX,
953                estimated_cost_microunits: 0,
954                expected_utility_microunits: 0,
955                counterfactual_model_id: 0,
956                counterfactual_utility_microunits: 0,
957                evaluated_models,
958                eligible_models,
959                policy_epoch: self.policy_epoch,
960                catalog_epoch: self.catalog_epoch,
961            },
962            RejectionHistogram::default(),
963        )
964    }
965}
966
967#[inline(always)]
968fn model_cost_fast(model: &KernelModel, input_tokens: u32, output_tokens: u32) -> u64 {
969    let input = u64::from(input_tokens)
970        .wrapping_mul(model.input_cost_microunits_per_million_tokens)
971        .wrapping_add(COST_ROUNDING)
972        / COST_SCALE;
973    let output = u64::from(output_tokens)
974        .wrapping_mul(model.output_cost_microunits_per_million_tokens)
975        .wrapping_add(COST_ROUNDING)
976        / COST_SCALE;
977    input.wrapping_add(output)
978}
979
980fn model_cost_reference(model: &KernelModel, input_tokens: u32, output_tokens: u32) -> u64 {
981    let input = u128::from(input_tokens)
982        .saturating_mul(u128::from(model.input_cost_microunits_per_million_tokens))
983        .saturating_add(u128::from(COST_ROUNDING))
984        / u128::from(COST_SCALE);
985    let output = u128::from(output_tokens)
986        .saturating_mul(u128::from(model.output_cost_microunits_per_million_tokens))
987        .saturating_add(u128::from(COST_ROUNDING))
988        / u128::from(COST_SCALE);
989    u64::try_from(input.saturating_add(output)).unwrap_or(u64::MAX)
990}
991
992#[inline]
993fn scaled_term_exact(value: u64, first_bps: u64, second_bps: u64) -> i128 {
994    value
995        .checked_mul(first_bps)
996        .and_then(|value| value.checked_mul(second_bps))
997        .map_or_else(
998            || scaled_term_reference(value, first_bps, second_bps),
999            |numerator| i128::from(numerator / SCALED_BASIS_POINTS),
1000        )
1001}
1002
1003#[inline]
1004fn scaled_term_reference(value: u64, first_bps: u64, second_bps: u64) -> i128 {
1005    i128::from(value) * i128::from(first_bps) * i128::from(second_bps)
1006        / i128::from(SCALED_BASIS_POINTS)
1007}
1008
1009/// Tie-breaking order: utility > lower cost > higher quality > lower model_id.
1010#[inline(always)]
1011fn candidate_better(left: Candidate, right: Candidate) -> bool {
1012    left.utility > right.utility
1013        || (left.utility == right.utility && left.cost < right.cost)
1014        || (left.utility == right.utility
1015            && left.cost == right.cost
1016            && left.quality_bps > right.quality_bps)
1017        || (left.utility == right.utility
1018            && left.cost == right.cost
1019            && left.quality_bps == right.quality_bps
1020            && left.model_id < right.model_id)
1021}
1022
1023fn dominant_rejection_reason(counts: &RejectionCounts) -> KernelReason {
1024    let candidates = [
1025        (counts.capability, KernelReason::CapabilityConstraint),
1026        (counts.region, KernelReason::RegionConstraint),
1027        (counts.provider, KernelReason::ProviderConstraint),
1028        (counts.quality, KernelReason::QualityConstraint),
1029        (counts.risk_ceiling, KernelReason::RiskCeilingConstraint),
1030        (counts.latency, KernelReason::LatencyConstraint),
1031        (counts.budget, KernelReason::BudgetConstraint),
1032        (counts.utility, KernelReason::NonPositiveUtility),
1033        (counts.disabled, KernelReason::NoEnabledModel),
1034    ];
1035    candidates
1036        .into_iter()
1037        .max_by_key(|(count, _)| *count)
1038        .filter(|(count, _)| *count > 0)
1039        .map_or(KernelReason::NoEnabledModel, |(_, reason)| reason)
1040}
1041
1042fn clamp_i128_to_i64(value: i128) -> i64 {
1043    value.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    use std::{hint::black_box, time::Instant};
1049
1050    use proptest::prelude::*;
1051
1052    use super::*;
1053
1054    const TOOLS: u64 = 1 << 0;
1055    const REGION_EU: u64 = 1 << 0;
1056
1057    fn snapshot() -> PolicySnapshot {
1058        PolicySnapshot::new_unchecked(
1059            7,
1060            11,
1061            9_600,
1062            5_500,
1063            10_000,
1064            2,
1065            vec![
1066                KernelModel {
1067                    model_id: 10,
1068                    provider_id: 0,
1069                    quality_bps: 7_500,
1070                    risk_ceiling_bps: 9_500,
1071                    enabled: 1,
1072                    p95_latency_ms: 180,
1073                    capabilities: TOOLS,
1074                    region_mask: REGION_EU,
1075                    input_cost_microunits_per_million_tokens: 150_000,
1076                    output_cost_microunits_per_million_tokens: 600_000,
1077                },
1078                KernelModel {
1079                    model_id: 20,
1080                    provider_id: 1,
1081                    quality_bps: 9_500,
1082                    risk_ceiling_bps: 9_500,
1083                    enabled: 1,
1084                    p95_latency_ms: 450,
1085                    capabilities: TOOLS,
1086                    region_mask: REGION_EU,
1087                    input_cost_microunits_per_million_tokens: 2_500_000,
1088                    output_cost_microunits_per_million_tokens: 10_000_000,
1089                },
1090            ],
1091        )
1092    }
1093
1094    fn input() -> KernelInput {
1095        KernelInput {
1096            request_sequence: 1,
1097            requested_model_id: 20,
1098            input_tokens: 2_000,
1099            output_tokens: 500,
1100            business_value_microunits: 100_000_000,
1101            budget_limit_microunits: 20_000_000,
1102            risk_bps: 1_000,
1103            confidence_bps: 9_000,
1104            minimum_quality_bps: 7_000,
1105            max_p95_latency_ms: 1_000,
1106            required_capabilities: TOOLS,
1107            allowed_provider_mask: ALL_PROVIDERS,
1108            required_region_mask: REGION_EU,
1109        }
1110    }
1111
1112    fn prescribe_reference(snapshot: &PolicySnapshot, input: KernelInput) -> KernelDecision {
1113        if input.risk_bps >= snapshot.hard_risk_limit_bps {
1114            return snapshot.reject(input, KernelReason::RiskHardLimit, 0, 0).0;
1115        }
1116        if input.confidence_bps < snapshot.minimum_confidence_bps {
1117            return snapshot
1118                .reject(input, KernelReason::ConfidenceHardLimit, 0, 0)
1119                .0;
1120        }
1121
1122        let mut best: Option<Candidate> = None;
1123        let mut second: Option<Candidate> = None;
1124        let mut eligible_models = 0_u16;
1125        let mut rejected = RejectionCounts::default();
1126        let value = input.business_value_microunits.max(0) as u64;
1127        let risk_penalty = scaled_term_reference(
1128            value,
1129            u64::from(input.risk_bps),
1130            u64::from(snapshot.risk_penalty_multiplier_bps),
1131        );
1132
1133        for (index, model) in snapshot.models.iter().enumerate() {
1134            if model.enabled == 0 {
1135                rejected.disabled += 1;
1136                continue;
1137            }
1138            if model.quality_bps < input.minimum_quality_bps {
1139                rejected.quality += 1;
1140                continue;
1141            }
1142            if input.max_p95_latency_ms > 0 && model.p95_latency_ms > input.max_p95_latency_ms {
1143                rejected.latency += 1;
1144                continue;
1145            }
1146            if model.capabilities & input.required_capabilities != input.required_capabilities {
1147                rejected.capability += 1;
1148                continue;
1149            }
1150            if model.provider_id > MAX_PROVIDER_ID {
1151                rejected.provider += 1;
1152                continue;
1153            }
1154            if input.allowed_provider_mask != ALL_PROVIDERS
1155                && input.allowed_provider_mask & (1_u64 << model.provider_id) == 0
1156            {
1157                rejected.provider += 1;
1158                continue;
1159            }
1160            if input.required_region_mask != 0
1161                && model.region_mask & input.required_region_mask == 0
1162            {
1163                rejected.region += 1;
1164                continue;
1165            }
1166            if input.risk_bps > model.risk_ceiling_bps {
1167                rejected.risk_ceiling += 1;
1168                continue;
1169            }
1170
1171            let cost = model_cost_reference(model, input.input_tokens, input.output_tokens);
1172            if cost > input.budget_limit_microunits {
1173                rejected.budget += 1;
1174                continue;
1175            }
1176            let quality_adjusted = scaled_term_reference(
1177                value,
1178                u64::from(input.confidence_bps),
1179                u64::from(model.quality_bps),
1180            );
1181            let latency_penalty = i128::from(model.p95_latency_ms)
1182                * i128::from(snapshot.latency_penalty_microunits_per_ms);
1183            let utility = clamp_i128_to_i64(
1184                quality_adjusted - risk_penalty - i128::from(cost) - latency_penalty,
1185            );
1186            if utility <= 0 {
1187                rejected.utility += 1;
1188                continue;
1189            }
1190            eligible_models = eligible_models.saturating_add(1);
1191            let candidate = Candidate {
1192                model_id: model.model_id,
1193                model_index: u16::try_from(index).unwrap_or(u16::MAX),
1194                quality_bps: model.quality_bps,
1195                cost,
1196                utility,
1197            };
1198            if best.is_none_or(|current| candidate_better(candidate, current)) {
1199                second = best;
1200                best = Some(candidate);
1201            } else if second.is_none_or(|current| candidate_better(candidate, current)) {
1202                second = Some(candidate);
1203            }
1204        }
1205
1206        let evaluated_models = u16::try_from(snapshot.models.len()).unwrap_or(u16::MAX);
1207        let Some(best) = best else {
1208            return snapshot
1209                .reject(
1210                    input,
1211                    dominant_rejection_reason(&rejected),
1212                    evaluated_models,
1213                    eligible_models,
1214                )
1215                .0;
1216        };
1217        let action = if best.model_id == input.requested_model_id {
1218            KernelAction::ExecuteRequested
1219        } else {
1220            KernelAction::Substitute
1221        };
1222        KernelDecision {
1223            request_sequence: input.request_sequence,
1224            action,
1225            reason: if action == KernelAction::ExecuteRequested {
1226                KernelReason::RequestedModelMaximizesUtility
1227            } else {
1228                KernelReason::AlternativeMaximizesUtility
1229            },
1230            selected_model_id: best.model_id,
1231            selected_model_index: best.model_index,
1232            estimated_cost_microunits: best.cost,
1233            expected_utility_microunits: best.utility,
1234            counterfactual_model_id: second.map_or(0, |candidate| candidate.model_id),
1235            counterfactual_utility_microunits: second.map_or(0, |candidate| candidate.utility),
1236            evaluated_models,
1237            eligible_models,
1238            policy_epoch: snapshot.policy_epoch,
1239            catalog_epoch: snapshot.catalog_epoch,
1240        }
1241    }
1242
1243    #[test]
1244    fn prescribes_maximum_utility_not_minimum_price() {
1245        let decision = snapshot().prescribe(input());
1246        assert_eq!(decision.action, KernelAction::ExecuteRequested);
1247        assert_eq!(decision.selected_model_id, 20);
1248        assert_eq!(decision.counterfactual_model_id, 10);
1249        assert!(decision.expected_utility_microunits > decision.counterfactual_utility_microunits);
1250    }
1251
1252    #[test]
1253    fn hard_budget_can_prescribe_substitution() {
1254        let mut request = input();
1255        request.budget_limit_microunits = 1_000;
1256        let decision = snapshot().prescribe(request);
1257        assert_eq!(decision.action, KernelAction::Substitute);
1258        assert_eq!(decision.selected_model_id, 10);
1259    }
1260
1261    #[test]
1262    fn decision_action_helpers_match_action() {
1263        let requested = snapshot().prescribe(input());
1264        assert!(requested.is_executable());
1265        assert!(requested.is_requested_execution());
1266        assert!(!requested.is_substitution());
1267        assert!(!requested.is_rejected());
1268
1269        let mut substitute_input = input();
1270        substitute_input.budget_limit_microunits = 1_000;
1271        let substitute = snapshot().prescribe(substitute_input);
1272        assert!(substitute.is_executable());
1273        assert!(!substitute.is_requested_execution());
1274        assert!(substitute.is_substitution());
1275        assert!(!substitute.is_rejected());
1276
1277        let mut rejected_input = input();
1278        rejected_input.risk_bps = 9_900;
1279        let rejected = snapshot().prescribe(rejected_input);
1280        assert!(!rejected.is_executable());
1281        assert!(!rejected.is_requested_execution());
1282        assert!(!rejected.is_substitution());
1283        assert!(rejected.is_rejected());
1284    }
1285
1286    fn base_model(model_id: u32, enabled: u8) -> KernelModel {
1287        KernelModel {
1288            model_id,
1289            provider_id: 0,
1290            quality_bps: 8_000,
1291            risk_ceiling_bps: 9_500,
1292            enabled,
1293            p95_latency_ms: 200,
1294            capabilities: 0,
1295            region_mask: ALL_REGIONS,
1296            input_cost_microunits_per_million_tokens: 100,
1297            output_cost_microunits_per_million_tokens: 400,
1298        }
1299    }
1300
1301    #[test]
1302    fn input_validate_rejects_out_of_range_bps() {
1303        let mut request = input();
1304        request.confidence_bps = 10_001;
1305        assert_eq!(
1306            request.validate(),
1307            Err(InputError::OutOfRangeBps {
1308                field: "confidence_bps",
1309                value: 10_001,
1310                max: MAX_BPS,
1311            })
1312        );
1313    }
1314
1315    #[test]
1316    fn input_validate_accepts_boundary_bps() {
1317        let mut request = input();
1318        request.risk_bps = MAX_BPS;
1319        request.confidence_bps = MAX_BPS;
1320        request.minimum_quality_bps = MAX_BPS;
1321        assert!(request.validate().is_ok());
1322    }
1323
1324    #[test]
1325    fn checked_prescribe_rejects_invalid_input_before_evaluation() {
1326        let snapshot = snapshot();
1327        let mut request = input();
1328        request.confidence_bps = MAX_BPS + 1;
1329        assert!(matches!(
1330            snapshot.prescribe_checked(request),
1331            Err(InputError::OutOfRangeBps {
1332                field: "confidence_bps",
1333                ..
1334            })
1335        ));
1336        assert!(snapshot.prescribe_with_trace_checked(request).is_err());
1337        assert!(snapshot
1338            .prescribe_batch_checked(&[input(), request])
1339            .is_err());
1340    }
1341
1342    #[test]
1343    fn policy_error_empty_catalog() {
1344        let snap = PolicySnapshot::new_unchecked(1, 1, 9_600, 5_500, 3_500, 0, vec![]);
1345        assert_eq!(snap.validate(), Err(PolicyError::EmptyCatalog));
1346        assert!(matches!(
1347            PolicySnapshot::try_new(1, 1, 9_600, 5_500, 3_500, 0, vec![]),
1348            Err(PolicyError::EmptyCatalog)
1349        ));
1350    }
1351
1352    #[test]
1353    fn policy_error_duplicate_model_id() {
1354        let snap = PolicySnapshot::new_unchecked(
1355            1,
1356            1,
1357            9_600,
1358            5_500,
1359            3_500,
1360            0,
1361            vec![base_model(1, 1), base_model(1, 1)],
1362        );
1363        assert_eq!(
1364            snap.validate(),
1365            Err(PolicyError::DuplicateModelId { model_id: 1 })
1366        );
1367    }
1368
1369    #[test]
1370    fn model_id_zero_is_reserved_for_rejection() {
1371        assert!(matches!(
1372            PolicySnapshot::try_new_trusted(1, 1, 9_600, 5_500, 3_500, 0, vec![base_model(0, 1)]),
1373            Err(TrustPolicyError::ReservedModelId)
1374        ));
1375    }
1376
1377    #[test]
1378    fn catalog_larger_than_decision_counters_is_rejected() {
1379        let models = (1..=u32::from(u16::MAX) + 1)
1380            .map(|model_id| base_model(model_id, 1))
1381            .collect();
1382        assert!(matches!(
1383            PolicySnapshot::try_new_trusted(1, 1, 9_600, 5_500, 3_500, 0, models),
1384            Err(TrustPolicyError::CatalogTooLarge { .. })
1385        ));
1386    }
1387
1388    #[test]
1389    fn policy_error_invalid_provider_id() {
1390        let mut model = base_model(1, 1);
1391        model.provider_id = MAX_PROVIDER_ID + 1;
1392        let snap = PolicySnapshot::new_unchecked(1, 1, 9_600, 5_500, 3_500, 0, vec![model]);
1393        assert_eq!(
1394            snap.validate(),
1395            Err(PolicyError::InvalidProviderId {
1396                model_id: 1,
1397                provider_id: MAX_PROVIDER_ID + 1,
1398            })
1399        );
1400    }
1401
1402    #[test]
1403    fn policy_error_no_enabled_models() {
1404        let snap = PolicySnapshot::new_unchecked(
1405            1,
1406            1,
1407            9_600,
1408            5_500,
1409            3_500,
1410            0,
1411            vec![base_model(1, 0), base_model(2, 0)],
1412        );
1413        assert_eq!(snap.validate(), Err(PolicyError::NoEnabledModels));
1414    }
1415
1416    #[test]
1417    fn policy_error_out_of_range_bps() {
1418        let models = vec![base_model(1, 1)];
1419        assert!(matches!(
1420            PolicySnapshot::try_new(1, 1, 10_001, 5_500, 3_500, 0, models.clone()),
1421            Err(PolicyError::OutOfRangeBps { .. })
1422        ));
1423        assert!(matches!(
1424            PolicySnapshot::try_new(1, 1, 9_600, 10_001, 3_500, 0, models.clone()),
1425            Err(PolicyError::OutOfRangeBps { .. })
1426        ));
1427        assert!(matches!(
1428            PolicySnapshot::try_new(1, 1, 9_600, 5_500, 50_001, 0, models.clone()),
1429            Err(PolicyError::OutOfRangeBps { .. })
1430        ));
1431        let mut bad_quality = base_model(2, 1);
1432        bad_quality.quality_bps = 10_001;
1433        assert!(matches!(
1434            PolicySnapshot::try_new(1, 1, 9_600, 5_500, 3_500, 0, vec![bad_quality]),
1435            Err(PolicyError::OutOfRangeBps { .. })
1436        ));
1437    }
1438
1439    #[test]
1440    fn utility_for_model_matches_eligible_catalog_entry() {
1441        let snap = snapshot();
1442        let input = input();
1443        let utility = snap.utility_for_model(input, 20);
1444        assert!(utility.is_some());
1445        assert_eq!(
1446            utility,
1447            Some(snap.prescribe(input).expected_utility_microunits)
1448        );
1449    }
1450
1451    #[test]
1452    fn utility_for_model_none_for_missing_id() {
1453        let snap = snapshot();
1454        assert!(snap.utility_for_model(input(), 999).is_none());
1455    }
1456
1457    #[test]
1458    fn prescribe_batch_matches_individual() {
1459        let snap = snapshot();
1460        let inputs = [
1461            input(),
1462            KernelInput {
1463                request_sequence: 2,
1464                requested_model_id: 10,
1465                input_tokens: 500,
1466                output_tokens: 100,
1467                business_value_microunits: 50_000_000,
1468                budget_limit_microunits: 5_000_000,
1469                risk_bps: 500,
1470                confidence_bps: 9_500,
1471                minimum_quality_bps: 7_000,
1472                max_p95_latency_ms: 500,
1473                required_capabilities: TOOLS,
1474                allowed_provider_mask: ALL_PROVIDERS,
1475                required_region_mask: REGION_EU,
1476            },
1477        ];
1478        let batch = snap.prescribe_batch(&inputs);
1479        assert_eq!(batch.len(), inputs.len());
1480        for (i, &inp) in inputs.iter().enumerate() {
1481            assert_eq!(batch[i], snap.prescribe(inp));
1482        }
1483    }
1484
1485    #[test]
1486    fn hard_constraints_fail_closed() {
1487        let mut request = input();
1488        request.risk_bps = 9_900;
1489        let decision = snapshot().prescribe(request);
1490        assert_eq!(decision.action, KernelAction::Reject);
1491        assert_eq!(decision.reason, KernelReason::RiskHardLimit);
1492
1493        request.risk_bps = 1_000;
1494        request.required_capabilities = 1 << 9;
1495        let decision = snapshot().prescribe(request);
1496        assert_eq!(decision.action, KernelAction::Reject);
1497        assert_eq!(decision.reason, KernelReason::CapabilityConstraint);
1498    }
1499
1500    #[test]
1501    fn extreme_inputs_saturate_without_panicking() {
1502        let mut request = input();
1503        request.input_tokens = u32::MAX;
1504        request.output_tokens = u32::MAX;
1505        request.business_value_microunits = i64::MAX;
1506        request.budget_limit_microunits = u64::MAX;
1507        let decision = snapshot().prescribe(request);
1508        assert_eq!(decision.request_sequence, request.request_sequence);
1509    }
1510
1511    #[test]
1512    fn exact_fast_path_preserves_single_rounding_step() {
1513        assert_eq!(scaled_term_reference(2, 5_001, 9_999), 1);
1514        assert_eq!(scaled_term_exact(2, 5_001, 9_999), 1);
1515    }
1516
1517    proptest! {
1518        #[test]
1519        fn optimized_scaled_term_matches_i128_reference(
1520            value in any::<u64>(),
1521            first_bps in any::<u16>(),
1522            second_bps in any::<u16>(),
1523        ) {
1524            prop_assert_eq!(
1525                scaled_term_exact(value, u64::from(first_bps), u64::from(second_bps)),
1526                scaled_term_reference(value, u64::from(first_bps), u64::from(second_bps)),
1527            );
1528        }
1529
1530        #[test]
1531        fn optimized_cost_matches_u128_reference_when_guard_admits(
1532            input_tokens in any::<u32>(),
1533            output_tokens in any::<u32>(),
1534            input_price in any::<u64>(),
1535            output_price in any::<u64>(),
1536        ) {
1537            let model = KernelModel {
1538                model_id: 1,
1539                provider_id: 0,
1540                quality_bps: 10_000,
1541                risk_ceiling_bps: u16::MAX,
1542                enabled: 1,
1543                p95_latency_ms: 1,
1544                capabilities: 0,
1545                region_mask: ALL_REGIONS,
1546                input_cost_microunits_per_million_tokens: input_price,
1547                output_cost_microunits_per_million_tokens: output_price,
1548            };
1549            let snapshot = PolicySnapshot::new_unchecked(1, 1, u16::MAX, 0, 0, 0, vec![model]);
1550            if snapshot.all_costs_fit_u64(input_tokens, output_tokens) {
1551                prop_assert_eq!(
1552                    model_cost_fast(&model, input_tokens, output_tokens),
1553                    model_cost_reference(&model, input_tokens, output_tokens),
1554                );
1555            }
1556        }
1557
1558        #[test]
1559        fn optimized_kernel_matches_reference_decision(
1560            input_tokens in any::<u32>(),
1561            output_tokens in any::<u32>(),
1562            value in any::<i64>(),
1563            budget in any::<u64>(),
1564            risk in any::<u16>(),
1565            confidence in any::<u16>(),
1566            minimum_quality in any::<u16>(),
1567            maximum_latency in any::<u32>(),
1568            provider_mask in any::<u64>(),
1569            region_mask in any::<u64>(),
1570        ) {
1571            let mut request = input();
1572            request.input_tokens = input_tokens;
1573            request.output_tokens = output_tokens;
1574            request.business_value_microunits = value;
1575            request.budget_limit_microunits = budget;
1576            request.risk_bps = risk;
1577            request.confidence_bps = confidence;
1578            request.minimum_quality_bps = minimum_quality;
1579            request.max_p95_latency_ms = maximum_latency;
1580            request.allowed_provider_mask = provider_mask;
1581            request.required_region_mask = region_mask;
1582            let snapshot = snapshot();
1583            prop_assert_eq!(snapshot.prescribe(request), prescribe_reference(&snapshot, request));
1584        }
1585
1586        #[test]
1587        fn arbitrary_inputs_never_bypass_provider_fence(
1588            input_tokens in any::<u32>(),
1589            output_tokens in any::<u32>(),
1590            value in any::<i64>(),
1591            budget in any::<u64>(),
1592            risk in any::<u16>(),
1593            confidence in any::<u16>(),
1594        ) {
1595            let mut request = input();
1596            request.input_tokens = input_tokens;
1597            request.output_tokens = output_tokens;
1598            request.business_value_microunits = value;
1599            request.budget_limit_microunits = budget;
1600            request.risk_bps = risk;
1601            request.confidence_bps = confidence;
1602            request.allowed_provider_mask = 0;
1603            let decision = snapshot().prescribe(request);
1604            prop_assert_eq!(decision.action, KernelAction::Reject);
1605        }
1606    }
1607
1608    #[test]
1609    fn provider_id_above_64_rejected_even_with_all_providers() {
1610        let models = vec![KernelModel {
1611            model_id: 1,
1612            provider_id: 65,
1613            quality_bps: 9500,
1614            risk_ceiling_bps: 10000,
1615            enabled: 1,
1616            p95_latency_ms: 500,
1617            capabilities: 0,
1618            region_mask: ALL_REGIONS,
1619            input_cost_microunits_per_million_tokens: 100,
1620            output_cost_microunits_per_million_tokens: 400,
1621        }];
1622        let snapshot = PolicySnapshot::new_unchecked(1, 1, 9600, 5500, 3500, 0, models);
1623        let mut request = input();
1624        request.allowed_provider_mask = ALL_PROVIDERS;
1625        let decision = snapshot.prescribe(request);
1626        assert_eq!(
1627            decision.action,
1628            KernelAction::Reject,
1629            "provider_id >= 64 must be rejected even when mask is ALL_PROVIDERS"
1630        );
1631    }
1632
1633    #[test]
1634    fn provider_id_below_64_accepted_with_all_providers() {
1635        let mut request = input();
1636        request.allowed_provider_mask = ALL_PROVIDERS;
1637        let decision = snapshot().prescribe(request);
1638        assert_ne!(
1639            decision.action,
1640            KernelAction::Reject,
1641            "provider_id < 64 with ALL_PROVIDERS should not be rejected by provider fence"
1642        );
1643    }
1644
1645    #[test]
1646    #[ignore = "release-only kernel guard"]
1647    fn prescriptive_kernel_latency_guard() {
1648        let snapshot = snapshot();
1649        let base = input();
1650        let iterations = 1_000_000_u64;
1651        let started = Instant::now();
1652        for sequence in 0..iterations {
1653            let mut request = base;
1654            request.request_sequence = sequence;
1655            request.input_tokens = 1_000 + (sequence % 1_024) as u32;
1656            black_box(snapshot.prescribe(black_box(request)));
1657        }
1658        let average_ns = started.elapsed().as_nanos() / u128::from(iterations);
1659        assert!(
1660            average_ns < 2_000,
1661            "prescriptive kernel exceeded 2us average guard: {average_ns}ns"
1662        );
1663    }
1664}