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