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            return self.reject(
885                input,
886                dominant_rejection_reason(&rejected),
887                evaluated_models,
888                eligible_models,
889            );
890        };
891        let action = if best.model_id == input.requested_model_id {
892            KernelAction::ExecuteRequested
893        } else {
894            KernelAction::Substitute
895        };
896        (
897            KernelDecision {
898                request_sequence: input.request_sequence,
899                action,
900                reason: if action == KernelAction::ExecuteRequested {
901                    KernelReason::RequestedModelMaximizesUtility
902                } else {
903                    KernelReason::AlternativeMaximizesUtility
904                },
905                selected_model_id: best.model_id,
906                selected_model_index: best.model_index,
907                estimated_cost_microunits: best.cost,
908                expected_utility_microunits: best.utility,
909                counterfactual_model_id: second.map_or(0, |candidate| candidate.model_id),
910                counterfactual_utility_microunits: second.map_or(0, |candidate| candidate.utility),
911                evaluated_models,
912                eligible_models,
913                policy_epoch: self.policy_epoch,
914                catalog_epoch: self.catalog_epoch,
915            },
916            rejected,
917        )
918    }
919
920    #[inline]
921    fn all_costs_fit_u64(&self, input_tokens: u32, output_tokens: u32) -> bool {
922        let input = u64::from(input_tokens)
923            .checked_mul(self.max_input_cost)
924            .and_then(|value| value.checked_add(COST_ROUNDING));
925        let output = u64::from(output_tokens)
926            .checked_mul(self.max_output_cost)
927            .and_then(|value| value.checked_add(COST_ROUNDING));
928        input
929            .zip(output)
930            .is_some_and(|(input, output)| input.checked_add(output).is_some())
931    }
932
933    /// Build a rejection decision. Returns a default (all-zero) histogram because
934    /// hard-limit rejections (risk, confidence) exit before model evaluation — no
935    /// per-model constraint counts are available.
936    fn reject(
937        &self,
938        input: KernelInput,
939        reason: KernelReason,
940        evaluated_models: u16,
941        eligible_models: u16,
942    ) -> (KernelDecision, RejectionHistogram) {
943        (
944            KernelDecision {
945                request_sequence: input.request_sequence,
946                action: KernelAction::Reject,
947                reason,
948                selected_model_id: 0,
949                selected_model_index: u16::MAX,
950                estimated_cost_microunits: 0,
951                expected_utility_microunits: 0,
952                counterfactual_model_id: 0,
953                counterfactual_utility_microunits: 0,
954                evaluated_models,
955                eligible_models,
956                policy_epoch: self.policy_epoch,
957                catalog_epoch: self.catalog_epoch,
958            },
959            RejectionHistogram::default(),
960        )
961    }
962}
963
964#[inline(always)]
965fn model_cost_fast(model: &KernelModel, input_tokens: u32, output_tokens: u32) -> u64 {
966    let input = u64::from(input_tokens)
967        .wrapping_mul(model.input_cost_microunits_per_million_tokens)
968        .wrapping_add(COST_ROUNDING)
969        / COST_SCALE;
970    let output = u64::from(output_tokens)
971        .wrapping_mul(model.output_cost_microunits_per_million_tokens)
972        .wrapping_add(COST_ROUNDING)
973        / COST_SCALE;
974    input.wrapping_add(output)
975}
976
977fn model_cost_reference(model: &KernelModel, input_tokens: u32, output_tokens: u32) -> u64 {
978    let input = u128::from(input_tokens)
979        .saturating_mul(u128::from(model.input_cost_microunits_per_million_tokens))
980        .saturating_add(u128::from(COST_ROUNDING))
981        / u128::from(COST_SCALE);
982    let output = u128::from(output_tokens)
983        .saturating_mul(u128::from(model.output_cost_microunits_per_million_tokens))
984        .saturating_add(u128::from(COST_ROUNDING))
985        / u128::from(COST_SCALE);
986    u64::try_from(input.saturating_add(output)).unwrap_or(u64::MAX)
987}
988
989#[inline]
990fn scaled_term_exact(value: u64, first_bps: u64, second_bps: u64) -> i128 {
991    value
992        .checked_mul(first_bps)
993        .and_then(|value| value.checked_mul(second_bps))
994        .map_or_else(
995            || scaled_term_reference(value, first_bps, second_bps),
996            |numerator| i128::from(numerator / SCALED_BASIS_POINTS),
997        )
998}
999
1000#[inline]
1001fn scaled_term_reference(value: u64, first_bps: u64, second_bps: u64) -> i128 {
1002    i128::from(value) * i128::from(first_bps) * i128::from(second_bps)
1003        / i128::from(SCALED_BASIS_POINTS)
1004}
1005
1006/// Tie-breaking order: utility > lower cost > higher quality > lower model_id.
1007#[inline(always)]
1008fn candidate_better(left: Candidate, right: Candidate) -> bool {
1009    left.utility > right.utility
1010        || (left.utility == right.utility && left.cost < right.cost)
1011        || (left.utility == right.utility
1012            && left.cost == right.cost
1013            && left.quality_bps > right.quality_bps)
1014        || (left.utility == right.utility
1015            && left.cost == right.cost
1016            && left.quality_bps == right.quality_bps
1017            && left.model_id < right.model_id)
1018}
1019
1020fn dominant_rejection_reason(counts: &RejectionCounts) -> KernelReason {
1021    let candidates = [
1022        (counts.capability, KernelReason::CapabilityConstraint),
1023        (counts.region, KernelReason::RegionConstraint),
1024        (counts.provider, KernelReason::ProviderConstraint),
1025        (counts.quality, KernelReason::QualityConstraint),
1026        (counts.risk_ceiling, KernelReason::RiskCeilingConstraint),
1027        (counts.latency, KernelReason::LatencyConstraint),
1028        (counts.budget, KernelReason::BudgetConstraint),
1029        (counts.utility, KernelReason::NonPositiveUtility),
1030        (counts.disabled, KernelReason::NoEnabledModel),
1031    ];
1032    candidates
1033        .into_iter()
1034        .max_by_key(|(count, _)| *count)
1035        .filter(|(count, _)| *count > 0)
1036        .map_or(KernelReason::NoEnabledModel, |(_, reason)| reason)
1037}
1038
1039fn clamp_i128_to_i64(value: i128) -> i64 {
1040    value.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045    use std::{hint::black_box, time::Instant};
1046
1047    use proptest::prelude::*;
1048
1049    use super::*;
1050
1051    const TOOLS: u64 = 1 << 0;
1052    const REGION_EU: u64 = 1 << 0;
1053
1054    fn snapshot() -> PolicySnapshot {
1055        PolicySnapshot::new_unchecked(
1056            7,
1057            11,
1058            9_600,
1059            5_500,
1060            10_000,
1061            2,
1062            vec![
1063                KernelModel {
1064                    model_id: 10,
1065                    provider_id: 0,
1066                    quality_bps: 7_500,
1067                    risk_ceiling_bps: 9_500,
1068                    enabled: 1,
1069                    p95_latency_ms: 180,
1070                    capabilities: TOOLS,
1071                    region_mask: REGION_EU,
1072                    input_cost_microunits_per_million_tokens: 150_000,
1073                    output_cost_microunits_per_million_tokens: 600_000,
1074                },
1075                KernelModel {
1076                    model_id: 20,
1077                    provider_id: 1,
1078                    quality_bps: 9_500,
1079                    risk_ceiling_bps: 9_500,
1080                    enabled: 1,
1081                    p95_latency_ms: 450,
1082                    capabilities: TOOLS,
1083                    region_mask: REGION_EU,
1084                    input_cost_microunits_per_million_tokens: 2_500_000,
1085                    output_cost_microunits_per_million_tokens: 10_000_000,
1086                },
1087            ],
1088        )
1089    }
1090
1091    fn input() -> KernelInput {
1092        KernelInput {
1093            request_sequence: 1,
1094            requested_model_id: 20,
1095            input_tokens: 2_000,
1096            output_tokens: 500,
1097            business_value_microunits: 100_000_000,
1098            budget_limit_microunits: 20_000_000,
1099            risk_bps: 1_000,
1100            confidence_bps: 9_000,
1101            minimum_quality_bps: 7_000,
1102            max_p95_latency_ms: 1_000,
1103            required_capabilities: TOOLS,
1104            allowed_provider_mask: ALL_PROVIDERS,
1105            required_region_mask: REGION_EU,
1106        }
1107    }
1108
1109    fn prescribe_reference(snapshot: &PolicySnapshot, input: KernelInput) -> KernelDecision {
1110        if input.risk_bps >= snapshot.hard_risk_limit_bps {
1111            return snapshot.reject(input, KernelReason::RiskHardLimit, 0, 0).0;
1112        }
1113        if input.confidence_bps < snapshot.minimum_confidence_bps {
1114            return snapshot
1115                .reject(input, KernelReason::ConfidenceHardLimit, 0, 0)
1116                .0;
1117        }
1118
1119        let mut best: Option<Candidate> = None;
1120        let mut second: Option<Candidate> = None;
1121        let mut eligible_models = 0_u16;
1122        let mut rejected = RejectionCounts::default();
1123        let value = input.business_value_microunits.max(0) as u64;
1124        let risk_penalty = scaled_term_reference(
1125            value,
1126            u64::from(input.risk_bps),
1127            u64::from(snapshot.risk_penalty_multiplier_bps),
1128        );
1129
1130        for (index, model) in snapshot.models.iter().enumerate() {
1131            if model.enabled == 0 {
1132                rejected.disabled += 1;
1133                continue;
1134            }
1135            if model.quality_bps < input.minimum_quality_bps {
1136                rejected.quality += 1;
1137                continue;
1138            }
1139            if input.max_p95_latency_ms > 0 && model.p95_latency_ms > input.max_p95_latency_ms {
1140                rejected.latency += 1;
1141                continue;
1142            }
1143            if model.capabilities & input.required_capabilities != input.required_capabilities {
1144                rejected.capability += 1;
1145                continue;
1146            }
1147            if model.provider_id > MAX_PROVIDER_ID {
1148                rejected.provider += 1;
1149                continue;
1150            }
1151            if input.allowed_provider_mask != ALL_PROVIDERS
1152                && input.allowed_provider_mask & (1_u64 << model.provider_id) == 0
1153            {
1154                rejected.provider += 1;
1155                continue;
1156            }
1157            if input.required_region_mask != 0
1158                && model.region_mask & input.required_region_mask == 0
1159            {
1160                rejected.region += 1;
1161                continue;
1162            }
1163            if input.risk_bps > model.risk_ceiling_bps {
1164                rejected.risk_ceiling += 1;
1165                continue;
1166            }
1167
1168            let cost = model_cost_reference(model, input.input_tokens, input.output_tokens);
1169            if cost > input.budget_limit_microunits {
1170                rejected.budget += 1;
1171                continue;
1172            }
1173            let quality_adjusted = scaled_term_reference(
1174                value,
1175                u64::from(input.confidence_bps),
1176                u64::from(model.quality_bps),
1177            );
1178            let latency_penalty = i128::from(model.p95_latency_ms)
1179                * i128::from(snapshot.latency_penalty_microunits_per_ms);
1180            let utility = clamp_i128_to_i64(
1181                quality_adjusted - risk_penalty - i128::from(cost) - latency_penalty,
1182            );
1183            if utility <= 0 {
1184                rejected.utility += 1;
1185                continue;
1186            }
1187            eligible_models = eligible_models.saturating_add(1);
1188            let candidate = Candidate {
1189                model_id: model.model_id,
1190                model_index: u16::try_from(index).unwrap_or(u16::MAX),
1191                quality_bps: model.quality_bps,
1192                cost,
1193                utility,
1194            };
1195            if best.is_none_or(|current| candidate_better(candidate, current)) {
1196                second = best;
1197                best = Some(candidate);
1198            } else if second.is_none_or(|current| candidate_better(candidate, current)) {
1199                second = Some(candidate);
1200            }
1201        }
1202
1203        let evaluated_models = u16::try_from(snapshot.models.len()).unwrap_or(u16::MAX);
1204        let Some(best) = best else {
1205            return snapshot
1206                .reject(
1207                    input,
1208                    dominant_rejection_reason(&rejected),
1209                    evaluated_models,
1210                    eligible_models,
1211                )
1212                .0;
1213        };
1214        let action = if best.model_id == input.requested_model_id {
1215            KernelAction::ExecuteRequested
1216        } else {
1217            KernelAction::Substitute
1218        };
1219        KernelDecision {
1220            request_sequence: input.request_sequence,
1221            action,
1222            reason: if action == KernelAction::ExecuteRequested {
1223                KernelReason::RequestedModelMaximizesUtility
1224            } else {
1225                KernelReason::AlternativeMaximizesUtility
1226            },
1227            selected_model_id: best.model_id,
1228            selected_model_index: best.model_index,
1229            estimated_cost_microunits: best.cost,
1230            expected_utility_microunits: best.utility,
1231            counterfactual_model_id: second.map_or(0, |candidate| candidate.model_id),
1232            counterfactual_utility_microunits: second.map_or(0, |candidate| candidate.utility),
1233            evaluated_models,
1234            eligible_models,
1235            policy_epoch: snapshot.policy_epoch,
1236            catalog_epoch: snapshot.catalog_epoch,
1237        }
1238    }
1239
1240    #[test]
1241    fn prescribes_maximum_utility_not_minimum_price() {
1242        let decision = snapshot().prescribe(input());
1243        assert_eq!(decision.action, KernelAction::ExecuteRequested);
1244        assert_eq!(decision.selected_model_id, 20);
1245        assert_eq!(decision.counterfactual_model_id, 10);
1246        assert!(decision.expected_utility_microunits > decision.counterfactual_utility_microunits);
1247    }
1248
1249    #[test]
1250    fn hard_budget_can_prescribe_substitution() {
1251        let mut request = input();
1252        request.budget_limit_microunits = 1_000;
1253        let decision = snapshot().prescribe(request);
1254        assert_eq!(decision.action, KernelAction::Substitute);
1255        assert_eq!(decision.selected_model_id, 10);
1256    }
1257
1258    #[test]
1259    fn decision_action_helpers_match_action() {
1260        let requested = snapshot().prescribe(input());
1261        assert!(requested.is_executable());
1262        assert!(requested.is_requested_execution());
1263        assert!(!requested.is_substitution());
1264        assert!(!requested.is_rejected());
1265
1266        let mut substitute_input = input();
1267        substitute_input.budget_limit_microunits = 1_000;
1268        let substitute = snapshot().prescribe(substitute_input);
1269        assert!(substitute.is_executable());
1270        assert!(!substitute.is_requested_execution());
1271        assert!(substitute.is_substitution());
1272        assert!(!substitute.is_rejected());
1273
1274        let mut rejected_input = input();
1275        rejected_input.risk_bps = 9_900;
1276        let rejected = snapshot().prescribe(rejected_input);
1277        assert!(!rejected.is_executable());
1278        assert!(!rejected.is_requested_execution());
1279        assert!(!rejected.is_substitution());
1280        assert!(rejected.is_rejected());
1281    }
1282
1283    fn base_model(model_id: u32, enabled: u8) -> KernelModel {
1284        KernelModel {
1285            model_id,
1286            provider_id: 0,
1287            quality_bps: 8_000,
1288            risk_ceiling_bps: 9_500,
1289            enabled,
1290            p95_latency_ms: 200,
1291            capabilities: 0,
1292            region_mask: ALL_REGIONS,
1293            input_cost_microunits_per_million_tokens: 100,
1294            output_cost_microunits_per_million_tokens: 400,
1295        }
1296    }
1297
1298    #[test]
1299    fn input_validate_rejects_out_of_range_bps() {
1300        let mut request = input();
1301        request.confidence_bps = 10_001;
1302        assert_eq!(
1303            request.validate(),
1304            Err(InputError::OutOfRangeBps {
1305                field: "confidence_bps",
1306                value: 10_001,
1307                max: MAX_BPS,
1308            })
1309        );
1310    }
1311
1312    #[test]
1313    fn input_validate_accepts_boundary_bps() {
1314        let mut request = input();
1315        request.risk_bps = MAX_BPS;
1316        request.confidence_bps = MAX_BPS;
1317        request.minimum_quality_bps = MAX_BPS;
1318        assert!(request.validate().is_ok());
1319    }
1320
1321    #[test]
1322    fn checked_prescribe_rejects_invalid_input_before_evaluation() {
1323        let snapshot = snapshot();
1324        let mut request = input();
1325        request.confidence_bps = MAX_BPS + 1;
1326        assert!(matches!(
1327            snapshot.prescribe_checked(request),
1328            Err(InputError::OutOfRangeBps {
1329                field: "confidence_bps",
1330                ..
1331            })
1332        ));
1333        assert!(snapshot.prescribe_with_trace_checked(request).is_err());
1334        assert!(snapshot
1335            .prescribe_batch_checked(&[input(), request])
1336            .is_err());
1337    }
1338
1339    #[test]
1340    fn policy_error_empty_catalog() {
1341        let snap = PolicySnapshot::new_unchecked(1, 1, 9_600, 5_500, 3_500, 0, vec![]);
1342        assert_eq!(snap.validate(), Err(PolicyError::EmptyCatalog));
1343        assert!(matches!(
1344            PolicySnapshot::try_new(1, 1, 9_600, 5_500, 3_500, 0, vec![]),
1345            Err(PolicyError::EmptyCatalog)
1346        ));
1347    }
1348
1349    #[test]
1350    fn policy_error_duplicate_model_id() {
1351        let snap = PolicySnapshot::new_unchecked(
1352            1,
1353            1,
1354            9_600,
1355            5_500,
1356            3_500,
1357            0,
1358            vec![base_model(1, 1), base_model(1, 1)],
1359        );
1360        assert_eq!(
1361            snap.validate(),
1362            Err(PolicyError::DuplicateModelId { model_id: 1 })
1363        );
1364    }
1365
1366    #[test]
1367    fn model_id_zero_is_reserved_for_rejection() {
1368        assert!(matches!(
1369            PolicySnapshot::try_new_trusted(1, 1, 9_600, 5_500, 3_500, 0, vec![base_model(0, 1)]),
1370            Err(TrustPolicyError::ReservedModelId)
1371        ));
1372    }
1373
1374    #[test]
1375    fn catalog_larger_than_decision_counters_is_rejected() {
1376        let models = (1..=u32::from(u16::MAX) + 1)
1377            .map(|model_id| base_model(model_id, 1))
1378            .collect();
1379        assert!(matches!(
1380            PolicySnapshot::try_new_trusted(1, 1, 9_600, 5_500, 3_500, 0, models),
1381            Err(TrustPolicyError::CatalogTooLarge { .. })
1382        ));
1383    }
1384
1385    #[test]
1386    fn policy_error_invalid_provider_id() {
1387        let mut model = base_model(1, 1);
1388        model.provider_id = MAX_PROVIDER_ID + 1;
1389        let snap = PolicySnapshot::new_unchecked(1, 1, 9_600, 5_500, 3_500, 0, vec![model]);
1390        assert_eq!(
1391            snap.validate(),
1392            Err(PolicyError::InvalidProviderId {
1393                model_id: 1,
1394                provider_id: MAX_PROVIDER_ID + 1,
1395            })
1396        );
1397    }
1398
1399    #[test]
1400    fn policy_error_no_enabled_models() {
1401        let snap = PolicySnapshot::new_unchecked(
1402            1,
1403            1,
1404            9_600,
1405            5_500,
1406            3_500,
1407            0,
1408            vec![base_model(1, 0), base_model(2, 0)],
1409        );
1410        assert_eq!(snap.validate(), Err(PolicyError::NoEnabledModels));
1411    }
1412
1413    #[test]
1414    fn policy_error_out_of_range_bps() {
1415        let models = vec![base_model(1, 1)];
1416        assert!(matches!(
1417            PolicySnapshot::try_new(1, 1, 10_001, 5_500, 3_500, 0, models.clone()),
1418            Err(PolicyError::OutOfRangeBps { .. })
1419        ));
1420        assert!(matches!(
1421            PolicySnapshot::try_new(1, 1, 9_600, 10_001, 3_500, 0, models.clone()),
1422            Err(PolicyError::OutOfRangeBps { .. })
1423        ));
1424        assert!(matches!(
1425            PolicySnapshot::try_new(1, 1, 9_600, 5_500, 50_001, 0, models.clone()),
1426            Err(PolicyError::OutOfRangeBps { .. })
1427        ));
1428        let mut bad_quality = base_model(2, 1);
1429        bad_quality.quality_bps = 10_001;
1430        assert!(matches!(
1431            PolicySnapshot::try_new(1, 1, 9_600, 5_500, 3_500, 0, vec![bad_quality]),
1432            Err(PolicyError::OutOfRangeBps { .. })
1433        ));
1434    }
1435
1436    #[test]
1437    fn utility_for_model_matches_eligible_catalog_entry() {
1438        let snap = snapshot();
1439        let input = input();
1440        let utility = snap.utility_for_model(input, 20);
1441        assert!(utility.is_some());
1442        assert_eq!(
1443            utility,
1444            Some(snap.prescribe(input).expected_utility_microunits)
1445        );
1446    }
1447
1448    #[test]
1449    fn utility_for_model_none_for_missing_id() {
1450        let snap = snapshot();
1451        assert!(snap.utility_for_model(input(), 999).is_none());
1452    }
1453
1454    #[test]
1455    fn prescribe_batch_matches_individual() {
1456        let snap = snapshot();
1457        let inputs = [
1458            input(),
1459            KernelInput {
1460                request_sequence: 2,
1461                requested_model_id: 10,
1462                input_tokens: 500,
1463                output_tokens: 100,
1464                business_value_microunits: 50_000_000,
1465                budget_limit_microunits: 5_000_000,
1466                risk_bps: 500,
1467                confidence_bps: 9_500,
1468                minimum_quality_bps: 7_000,
1469                max_p95_latency_ms: 500,
1470                required_capabilities: TOOLS,
1471                allowed_provider_mask: ALL_PROVIDERS,
1472                required_region_mask: REGION_EU,
1473            },
1474        ];
1475        let batch = snap.prescribe_batch(&inputs);
1476        assert_eq!(batch.len(), inputs.len());
1477        for (i, &inp) in inputs.iter().enumerate() {
1478            assert_eq!(batch[i], snap.prescribe(inp));
1479        }
1480    }
1481
1482    #[test]
1483    fn hard_constraints_fail_closed() {
1484        let mut request = input();
1485        request.risk_bps = 9_900;
1486        let decision = snapshot().prescribe(request);
1487        assert_eq!(decision.action, KernelAction::Reject);
1488        assert_eq!(decision.reason, KernelReason::RiskHardLimit);
1489
1490        request.risk_bps = 1_000;
1491        request.required_capabilities = 1 << 9;
1492        let decision = snapshot().prescribe(request);
1493        assert_eq!(decision.action, KernelAction::Reject);
1494        assert_eq!(decision.reason, KernelReason::CapabilityConstraint);
1495    }
1496
1497    #[test]
1498    fn extreme_inputs_saturate_without_panicking() {
1499        let mut request = input();
1500        request.input_tokens = u32::MAX;
1501        request.output_tokens = u32::MAX;
1502        request.business_value_microunits = i64::MAX;
1503        request.budget_limit_microunits = u64::MAX;
1504        let decision = snapshot().prescribe(request);
1505        assert_eq!(decision.request_sequence, request.request_sequence);
1506    }
1507
1508    #[test]
1509    fn exact_fast_path_preserves_single_rounding_step() {
1510        assert_eq!(scaled_term_reference(2, 5_001, 9_999), 1);
1511        assert_eq!(scaled_term_exact(2, 5_001, 9_999), 1);
1512    }
1513
1514    proptest! {
1515        #[test]
1516        fn optimized_scaled_term_matches_i128_reference(
1517            value in any::<u64>(),
1518            first_bps in any::<u16>(),
1519            second_bps in any::<u16>(),
1520        ) {
1521            prop_assert_eq!(
1522                scaled_term_exact(value, u64::from(first_bps), u64::from(second_bps)),
1523                scaled_term_reference(value, u64::from(first_bps), u64::from(second_bps)),
1524            );
1525        }
1526
1527        #[test]
1528        fn optimized_cost_matches_u128_reference_when_guard_admits(
1529            input_tokens in any::<u32>(),
1530            output_tokens in any::<u32>(),
1531            input_price in any::<u64>(),
1532            output_price in any::<u64>(),
1533        ) {
1534            let model = KernelModel {
1535                model_id: 1,
1536                provider_id: 0,
1537                quality_bps: 10_000,
1538                risk_ceiling_bps: u16::MAX,
1539                enabled: 1,
1540                p95_latency_ms: 1,
1541                capabilities: 0,
1542                region_mask: ALL_REGIONS,
1543                input_cost_microunits_per_million_tokens: input_price,
1544                output_cost_microunits_per_million_tokens: output_price,
1545            };
1546            let snapshot = PolicySnapshot::new_unchecked(1, 1, u16::MAX, 0, 0, 0, vec![model]);
1547            if snapshot.all_costs_fit_u64(input_tokens, output_tokens) {
1548                prop_assert_eq!(
1549                    model_cost_fast(&model, input_tokens, output_tokens),
1550                    model_cost_reference(&model, input_tokens, output_tokens),
1551                );
1552            }
1553        }
1554
1555        #[test]
1556        fn optimized_kernel_matches_reference_decision(
1557            input_tokens in any::<u32>(),
1558            output_tokens in any::<u32>(),
1559            value in any::<i64>(),
1560            budget in any::<u64>(),
1561            risk in any::<u16>(),
1562            confidence in any::<u16>(),
1563            minimum_quality in any::<u16>(),
1564            maximum_latency in any::<u32>(),
1565            provider_mask in any::<u64>(),
1566            region_mask in any::<u64>(),
1567        ) {
1568            let mut request = input();
1569            request.input_tokens = input_tokens;
1570            request.output_tokens = output_tokens;
1571            request.business_value_microunits = value;
1572            request.budget_limit_microunits = budget;
1573            request.risk_bps = risk;
1574            request.confidence_bps = confidence;
1575            request.minimum_quality_bps = minimum_quality;
1576            request.max_p95_latency_ms = maximum_latency;
1577            request.allowed_provider_mask = provider_mask;
1578            request.required_region_mask = region_mask;
1579            let snapshot = snapshot();
1580            prop_assert_eq!(snapshot.prescribe(request), prescribe_reference(&snapshot, request));
1581        }
1582
1583        #[test]
1584        fn arbitrary_inputs_never_bypass_provider_fence(
1585            input_tokens in any::<u32>(),
1586            output_tokens in any::<u32>(),
1587            value in any::<i64>(),
1588            budget in any::<u64>(),
1589            risk in any::<u16>(),
1590            confidence in any::<u16>(),
1591        ) {
1592            let mut request = input();
1593            request.input_tokens = input_tokens;
1594            request.output_tokens = output_tokens;
1595            request.business_value_microunits = value;
1596            request.budget_limit_microunits = budget;
1597            request.risk_bps = risk;
1598            request.confidence_bps = confidence;
1599            request.allowed_provider_mask = 0;
1600            let decision = snapshot().prescribe(request);
1601            prop_assert_eq!(decision.action, KernelAction::Reject);
1602        }
1603    }
1604
1605    #[test]
1606    fn provider_id_above_64_rejected_even_with_all_providers() {
1607        let models = vec![KernelModel {
1608            model_id: 1,
1609            provider_id: 65,
1610            quality_bps: 9500,
1611            risk_ceiling_bps: 10000,
1612            enabled: 1,
1613            p95_latency_ms: 500,
1614            capabilities: 0,
1615            region_mask: ALL_REGIONS,
1616            input_cost_microunits_per_million_tokens: 100,
1617            output_cost_microunits_per_million_tokens: 400,
1618        }];
1619        let snapshot = PolicySnapshot::new_unchecked(1, 1, 9600, 5500, 3500, 0, models);
1620        let mut request = input();
1621        request.allowed_provider_mask = ALL_PROVIDERS;
1622        let decision = snapshot.prescribe(request);
1623        assert_eq!(
1624            decision.action,
1625            KernelAction::Reject,
1626            "provider_id >= 64 must be rejected even when mask is ALL_PROVIDERS"
1627        );
1628    }
1629
1630    #[test]
1631    fn provider_id_below_64_accepted_with_all_providers() {
1632        let mut request = input();
1633        request.allowed_provider_mask = ALL_PROVIDERS;
1634        let decision = snapshot().prescribe(request);
1635        assert_ne!(
1636            decision.action,
1637            KernelAction::Reject,
1638            "provider_id < 64 with ALL_PROVIDERS should not be rejected by provider fence"
1639        );
1640    }
1641
1642    #[test]
1643    #[ignore = "release-only kernel guard"]
1644    fn prescriptive_kernel_latency_guard() {
1645        let snapshot = snapshot();
1646        let base = input();
1647        let iterations = 1_000_000_u64;
1648        let started = Instant::now();
1649        for sequence in 0..iterations {
1650            let mut request = base;
1651            request.request_sequence = sequence;
1652            request.input_tokens = 1_000 + (sequence % 1_024) as u32;
1653            black_box(snapshot.prescribe(black_box(request)));
1654        }
1655        let average_ns = started.elapsed().as_nanos() / u128::from(iterations);
1656        assert!(
1657            average_ns < 2_000,
1658            "prescriptive kernel exceeded 2us average guard: {average_ns}ns"
1659        );
1660    }
1661}