Skip to main content

converge_core/
kernel_boundary.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! # Kernel Boundary Types
5//!
6//! These types define the **constitutional boundary** between reasoning kernels
7//! (converge-llm) and the Converge platform. They encode core axioms:
8//!
9//! - **Proposed vs Fact**: Kernels emit `KernelProposal`, not `Fact`
10//! - **Replayable vs Audit-only**: `LocalReplayTrace` vs `RemoteReplayTrace`
11//! - **Explicit Authority**: All proposals have provenance via `ReplayTrace`
12//!
13//! ## Axiom Compliance
14//!
15//! | Axiom | Enforcement |
16//! |-------|-------------|
17//! | Agents Suggest, Engines Decide | `KernelProposal` cannot become `Fact` without validation |
18//! | Transparent Determinism | `ReplayTrace` in every proposal |
19//! | Human Authority First-Class | `requires_human` flag on proposals |
20//!
21//! ## Usage
22//!
23//! These types are re-exported by capability kernels (e.g., converge-llm)
24//! but defined here to ensure a single source of truth across all kernels.
25
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28
29// ============================================================================
30// Kernel Input Types: The Platform-to-Kernel Contract
31// ============================================================================
32
33/// What the kernel should reason about.
34///
35/// This is the **intent contract** between the platform and any reasoning kernel.
36/// It defines the task, success criteria, and resource budgets.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct KernelIntent {
39    /// The task to perform (e.g., "analyze_metrics", "generate_plan")
40    pub task: String,
41    /// Success criteria for the task
42    pub criteria: Vec<String>,
43    /// Maximum tokens budget for the entire kernel run
44    pub max_tokens: usize,
45}
46
47impl KernelIntent {
48    /// Create a new kernel intent with a task description.
49    #[must_use]
50    pub fn new(task: impl Into<String>) -> Self {
51        Self {
52            task: task.into(),
53            criteria: Vec::new(),
54            max_tokens: 1024,
55        }
56    }
57
58    /// Add a success criterion.
59    #[must_use]
60    pub fn with_criteria(mut self, criteria: impl Into<String>) -> Self {
61        self.criteria.push(criteria.into());
62        self
63    }
64
65    /// Set maximum tokens budget.
66    #[must_use]
67    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
68        self.max_tokens = max_tokens;
69        self
70    }
71}
72
73/// The context provided to the kernel (from converge-core's Context).
74///
75/// This is a **read-only view** of the platform's context, projected
76/// for kernel consumption. Kernels cannot mutate this directly.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct KernelContext {
79    /// Structured state data (from Seeds, Signals, etc.)
80    pub state: HashMap<String, serde_json::Value>,
81    /// Relevant facts from context (read-only view)
82    pub facts: Vec<ContextFact>,
83    /// Tenant/session identifier for recall scoping
84    pub tenant_id: Option<String>,
85}
86
87impl KernelContext {
88    /// Create an empty kernel context.
89    #[must_use]
90    pub fn new() -> Self {
91        Self {
92            state: HashMap::new(),
93            facts: Vec::new(),
94            tenant_id: None,
95        }
96    }
97
98    /// Add state data.
99    #[must_use]
100    pub fn with_state(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
101        self.state.insert(key.into(), value);
102        self
103    }
104
105    /// Add a fact from the platform context.
106    #[must_use]
107    pub fn with_fact(
108        mut self,
109        key: impl Into<String>,
110        id: impl Into<String>,
111        content: impl Into<String>,
112    ) -> Self {
113        self.facts.push(ContextFact {
114            key: key.into(),
115            id: id.into(),
116            content: content.into(),
117        });
118        self
119    }
120
121    /// Set tenant identifier for recall scoping.
122    #[must_use]
123    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
124        self.tenant_id = Some(tenant_id.into());
125        self
126    }
127}
128
129impl Default for KernelContext {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135/// A fact from converge-core's context (read-only).
136///
137/// This is a projection of platform facts for kernel consumption.
138/// The kernel cannot create or modify facts directly.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ContextFact {
141    /// The context key this fact belongs to
142    pub key: String,
143    /// Unique identifier for this fact
144    pub id: String,
145    /// The fact content
146    pub content: String,
147}
148
149/// Policy controlling kernel behavior.
150///
151/// This is the **policy contract** from the platform/runtime to the kernel.
152/// It controls adapter selection, recall behavior, determinism, and human gates.
153///
154/// # Axiom: Explicit Authority
155///
156/// Adapter selection comes from `KernelPolicy`, not emergent kernel behavior.
157/// This ensures the platform maintains control over which capabilities are used.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct KernelPolicy {
160    /// Which adapter to use (explicit authority from outside)
161    pub adapter_id: Option<String>,
162    /// Whether recall is enabled for this run
163    pub recall_enabled: bool,
164    /// Maximum recall candidates to consider
165    pub recall_max_candidates: usize,
166    /// Minimum relevance score for recall results
167    pub recall_min_score: f32,
168    /// Seed for deterministic execution (None = random)
169    pub seed: Option<u64>,
170    /// Whether proposals from this run require human approval
171    pub requires_human: bool,
172    /// Truth targets that must pass for auto-promotion
173    pub required_truths: Vec<String>,
174}
175
176impl KernelPolicy {
177    /// Create a new default policy.
178    #[must_use]
179    pub fn new() -> Self {
180        Self {
181            adapter_id: None,
182            recall_enabled: false,
183            recall_max_candidates: 5,
184            recall_min_score: 0.7,
185            seed: None,
186            requires_human: false,
187            required_truths: Vec::new(),
188        }
189    }
190
191    /// Create a deterministic policy with a fixed seed.
192    #[must_use]
193    pub fn deterministic(seed: u64) -> Self {
194        Self {
195            seed: Some(seed),
196            ..Self::new()
197        }
198    }
199
200    /// Set the adapter to use.
201    #[must_use]
202    pub fn with_adapter(mut self, adapter_id: impl Into<String>) -> Self {
203        self.adapter_id = Some(adapter_id.into());
204        self
205    }
206
207    /// Enable or disable recall.
208    #[must_use]
209    pub fn with_recall(mut self, enabled: bool) -> Self {
210        self.recall_enabled = enabled;
211        self
212    }
213
214    /// Mark proposals as requiring human approval.
215    #[must_use]
216    pub fn with_human_required(mut self) -> Self {
217        self.requires_human = true;
218        self
219    }
220
221    /// Add a required truth for auto-promotion.
222    #[must_use]
223    pub fn with_required_truth(mut self, truth: impl Into<String>) -> Self {
224        self.required_truths.push(truth.into());
225        self
226    }
227}
228
229impl Default for KernelPolicy {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235// ============================================================================
236// Routing Policy: Backend Selection Vocabulary
237// ============================================================================
238
239/// Risk tier for routing decisions.
240///
241/// This enum is part of the platform's vocabulary for backend selection.
242/// Policies can restrict which backends are allowed for each risk tier.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
244pub enum RiskTier {
245    Low,
246    Medium,
247    High,
248    Critical,
249}
250
251/// Data classification for routing decisions.
252///
253/// This enum controls which backends can handle data based on sensitivity.
254/// Policies can restrict remote backends for confidential/restricted data.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
256pub enum DataClassification {
257    Public,
258    Internal,
259    Confidential,
260    Restricted,
261}
262
263/// Policy for routing requests to backends.
264///
265/// Routing should be **policy-based**, not ad-hoc. This type encodes
266/// the rules for selecting backends based on:
267/// - Truth preferences (which truths prefer which backends)
268/// - Risk tier (critical/high-risk operations may require local)
269/// - Data classification (sensitive data may require local)
270///
271/// # Axiom: Explicit Authority
272///
273/// Backend selection is never implicit. Policies must explicitly allow
274/// remote backends, and default-deny is the recommended stance.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct RoutingPolicy {
277    /// Truth target → preferred backend
278    pub truth_preferences: HashMap<String, String>,
279    /// Risk tier → allowed backends
280    pub risk_tier_backends: HashMap<RiskTier, Vec<String>>,
281    /// Data classification → allowed backends
282    pub data_classification_backends: HashMap<DataClassification, Vec<String>>,
283    /// Default backend if no rule matches
284    pub default_backend: String,
285}
286
287impl Default for RoutingPolicy {
288    fn default() -> Self {
289        Self {
290            truth_preferences: HashMap::new(),
291            risk_tier_backends: HashMap::new(),
292            data_classification_backends: HashMap::new(),
293            default_backend: "local".to_string(),
294        }
295    }
296}
297
298impl RoutingPolicy {
299    /// Create a policy that denies remote backends by default.
300    ///
301    /// Remote backends must be explicitly allowed via risk tier or data classification.
302    /// This is the **recommended default** for security-conscious deployments.
303    #[must_use]
304    pub fn default_deny_remote() -> Self {
305        let mut policy = Self::default();
306        // Only allow local for high-risk and restricted data by default
307        policy
308            .risk_tier_backends
309            .insert(RiskTier::Critical, vec!["local".to_string()]);
310        policy
311            .risk_tier_backends
312            .insert(RiskTier::High, vec!["local".to_string()]);
313        policy
314            .data_classification_backends
315            .insert(DataClassification::Restricted, vec!["local".to_string()]);
316        policy
317            .data_classification_backends
318            .insert(DataClassification::Confidential, vec!["local".to_string()]);
319        policy
320    }
321
322    /// Check if a backend is allowed for the given context.
323    #[must_use]
324    pub fn is_backend_allowed(
325        &self,
326        backend_name: &str,
327        risk_tier: RiskTier,
328        data_classification: DataClassification,
329    ) -> bool {
330        // Check if explicitly denied by risk tier
331        if let Some(allowed) = self.risk_tier_backends.get(&risk_tier) {
332            if !allowed.contains(&backend_name.to_string()) && !allowed.is_empty() {
333                return false;
334            }
335        }
336
337        // Check if explicitly denied by data classification
338        if let Some(allowed) = self.data_classification_backends.get(&data_classification) {
339            if !allowed.contains(&backend_name.to_string()) && !allowed.is_empty() {
340                return false;
341            }
342        }
343
344        true
345    }
346
347    /// Select a backend for the given request context.
348    #[must_use]
349    pub fn select_backend(
350        &self,
351        truth_ids: &[String],
352        risk_tier: RiskTier,
353        data_classification: DataClassification,
354    ) -> &str {
355        // Check truth preferences first
356        for truth_id in truth_ids {
357            if let Some(backend) = self.truth_preferences.get(truth_id) {
358                return backend;
359            }
360        }
361
362        // Check risk tier
363        if let Some(backends) = self.risk_tier_backends.get(&risk_tier) {
364            if let Some(backend) = backends.first() {
365                return backend;
366            }
367        }
368
369        // Check data classification
370        if let Some(backends) = self.data_classification_backends.get(&data_classification) {
371            if let Some(backend) = backends.first() {
372                return backend;
373            }
374        }
375
376        // Default
377        &self.default_backend
378    }
379}
380
381// ============================================================================
382// Decision Step: Kernel Reasoning Phases
383// ============================================================================
384
385/// A step in the multi-phase reasoning process.
386///
387/// Kernels (like converge-llm) execute reasoning in distinct phases.
388/// This enum represents those phases and is part of the kernel boundary
389/// vocabulary for tracing, recall scoping, and contract validation.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391pub enum DecisionStep {
392    /// First step: derive conclusions from state
393    Reasoning,
394    /// Second step: score/evaluate options
395    Evaluation,
396    /// Third step: produce action plan
397    Planning,
398}
399
400impl DecisionStep {
401    /// Get the expected contract type for this step.
402    #[must_use]
403    pub fn expected_contract(&self) -> &'static str {
404        match self {
405            Self::Reasoning => "Reasoning",
406            Self::Evaluation => "Evaluation",
407            Self::Planning => "Planning",
408        }
409    }
410
411    /// Get the step name as a string.
412    #[must_use]
413    pub fn as_str(&self) -> &'static str {
414        match self {
415            Self::Reasoning => "reasoning",
416            Self::Evaluation => "evaluation",
417            Self::Planning => "planning",
418        }
419    }
420}
421
422impl Default for DecisionStep {
423    fn default() -> Self {
424        Self::Reasoning
425    }
426}
427
428// ============================================================================
429// ReplayTrace: Two Concrete Shapes
430// ============================================================================
431
432/// Trace link for audit and (possibly) replay.
433///
434/// The shape depends on the backend type. This is the **constitutional type**
435/// that prevents "ReplayTrace + hope" semantics.
436#[derive(Debug, Clone, Serialize, Deserialize)]
437#[serde(tag = "type")]
438pub enum ReplayTrace {
439    /// Local backend: replay-eligible (deterministic)
440    Local(LocalReplayTrace),
441    /// Remote backend: audit-eligible only (bounded stochasticity)
442    Remote(RemoteReplayTrace),
443}
444
445impl ReplayTrace {
446    /// Check if this trace is replay-eligible (only local).
447    #[must_use]
448    pub fn is_replay_eligible(&self) -> bool {
449        matches!(self, ReplayTrace::Local(_))
450    }
451
452    /// Get the replayability level.
453    #[must_use]
454    pub fn replayability(&self) -> Replayability {
455        match self {
456            ReplayTrace::Local(_) => Replayability::Deterministic,
457            ReplayTrace::Remote(r) => r.replayability,
458        }
459    }
460}
461
462/// Replayability level of the trace.
463///
464/// This enum enforces explicit acknowledgment of replay guarantees.
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
466pub enum Replayability {
467    /// Can be replayed with identical output (local inference)
468    Deterministic,
469    /// Best effort, may vary slightly (temp=0 remote)
470    BestEffort,
471    /// Cannot be replayed (external factors, safety layers)
472    None,
473}
474
475impl Default for Replayability {
476    fn default() -> Self {
477        Self::None
478    }
479}
480
481/// Reason why proposal replayability was downgraded.
482///
483/// This is a **stable contract surface** - serialization shape must not change
484/// without careful migration planning. Used for audit trails showing which
485/// component caused a replayability downgrade.
486///
487/// # Axiom: System Tells the Truth About Itself
488///
489/// If a kernel proposal includes stochastic components, the system must
490/// explicitly document why replayability was downgraded, not silently degrade.
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
492pub enum ReplayabilityDowngradeReason {
493    /// Recall embedder is not bit-exact deterministic
494    RecallEmbedderNotDeterministic,
495    /// Corpus content hash missing (cannot verify exact corpus state)
496    RecallCorpusNotContentAddressed,
497    /// Remote backend was used (cannot guarantee exact replay)
498    RemoteBackendUsed,
499    /// No seed was provided (inference is stochastic)
500    NoSeedProvided,
501    /// Multiple components caused downgrade
502    MultipleReasons,
503}
504
505/// Local trace link — replay-eligible.
506///
507/// Contains all information needed to reproduce the exact output.
508/// Only local inference can provide this level of determinism.
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct LocalReplayTrace {
511    /// Hash of base model weights
512    pub base_model_hash: String,
513    /// Adapter ID + hash (if used)
514    pub adapter: Option<AdapterTrace>,
515    /// Tokenizer hash
516    pub tokenizer_hash: String,
517    /// Random seed used
518    pub seed: u64,
519    /// Sampler parameters
520    pub sampler: SamplerParams,
521    /// Prompt version
522    pub prompt_version: String,
523    /// Recall trace (if used)
524    pub recall: Option<RecallTrace>,
525    /// Whether weights were mutated (merge)
526    pub weights_mutated: bool,
527    /// Execution environment
528    pub execution_env: ExecutionEnv,
529}
530
531/// Adapter trace for local runs.
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct AdapterTrace {
534    pub adapter_id: String,
535    pub adapter_hash: String,
536    pub merged: bool,
537}
538
539/// Sampler parameters for reproducibility.
540#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct SamplerParams {
542    pub temperature: f32,
543    pub top_p: f32,
544    pub top_k: Option<usize>,
545}
546
547impl Default for SamplerParams {
548    fn default() -> Self {
549        Self {
550            temperature: 0.0,
551            top_p: 1.0,
552            top_k: None,
553        }
554    }
555}
556
557/// Recall trace for local runs.
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct RecallTrace {
560    pub corpus_fingerprint: String,
561    pub candidate_ids: Vec<String>,
562    pub candidate_scores: Vec<f32>,
563    pub injected_count: usize,
564}
565
566/// Execution environment info.
567#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct ExecutionEnv {
569    pub device: String,
570    pub backend: String,
571    pub precision: String,
572}
573
574impl Default for ExecutionEnv {
575    fn default() -> Self {
576        Self {
577            device: "cpu".to_string(),
578            backend: "ndarray".to_string(),
579            precision: "f32".to_string(),
580        }
581    }
582}
583
584/// Remote trace link — audit-eligible only.
585///
586/// Contains enough info to audit but NOT replay deterministically.
587/// This explicitly acknowledges the bounded stochasticity of remote providers.
588#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct RemoteReplayTrace {
590    /// Provider name (e.g., "anthropic", "openai")
591    pub provider_name: String,
592    /// Model ID as returned by provider
593    pub provider_model_id: String,
594    /// Hash of canonicalized request
595    pub request_fingerprint: String,
596    /// Hash of response payload
597    pub response_fingerprint: String,
598    /// Temperature used
599    pub temperature: f32,
600    /// Top-p used
601    pub top_p: f32,
602    /// Max tokens requested
603    pub max_tokens: usize,
604    /// Provider-specific metadata (e.g., system_fingerprint)
605    pub provider_metadata: HashMap<String, String>,
606    /// Whether this was retried
607    pub retried: bool,
608    /// Retry reasons (if retried)
609    pub retry_reasons: Vec<String>,
610    /// Explicit replayability flag — prevents "ReplayTrace + hope" semantics
611    pub replayability: Replayability,
612}
613
614// ============================================================================
615// Proposal Types: The Kernel Output Boundary
616// ============================================================================
617
618/// The kind of proposal a kernel is making.
619///
620/// This taxonomy helps the engine understand what kind of validation
621/// and promotion logic to apply.
622#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
623pub enum ProposalKind {
624    /// Claims or assertions derived from reasoning
625    Claims,
626    /// An action plan with ordered steps
627    Plan,
628    /// A classification or categorization
629    Classification,
630    /// An evaluation with scores and justification
631    Evaluation,
632    /// A draft document or text artifact
633    DraftDocument,
634    /// Raw reasoning output (when no specific kind applies)
635    Reasoning,
636}
637
638impl Default for ProposalKind {
639    fn default() -> Self {
640        Self::Reasoning
641    }
642}
643
644/// Kind of proposed content (backend-level).
645#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
646pub enum ContentKind {
647    Claim,
648    Plan,
649    Classification,
650    Evaluation,
651    Draft,
652    Reasoning,
653}
654
655impl Default for ContentKind {
656    fn default() -> Self {
657        Self::Reasoning
658    }
659}
660
661impl From<ContentKind> for ProposalKind {
662    fn from(kind: ContentKind) -> Self {
663        match kind {
664            ContentKind::Claim => ProposalKind::Claims,
665            ContentKind::Plan => ProposalKind::Plan,
666            ContentKind::Classification => ProposalKind::Classification,
667            ContentKind::Evaluation => ProposalKind::Evaluation,
668            ContentKind::Draft => ProposalKind::DraftDocument,
669            ContentKind::Reasoning => ProposalKind::Reasoning,
670        }
671    }
672}
673
674/// A proposed piece of content (not yet a Fact).
675///
676/// This is the backend-level proposal type. It gets wrapped in
677/// `KernelProposal` at the kernel boundary.
678#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct ProposedContent {
680    /// Unique identifier
681    pub id: String,
682    /// The content type
683    pub kind: ContentKind,
684    /// The actual content
685    pub content: String,
686    /// Structured content (if applicable)
687    pub structured: Option<serde_json::Value>,
688    /// Confidence score (0.0 - 1.0)
689    pub confidence: Option<f32>,
690    /// Whether this requires human approval
691    pub requires_human: bool,
692}
693
694impl ProposedContent {
695    /// Create a new proposed content with minimal fields.
696    #[must_use]
697    pub fn new(id: impl Into<String>, kind: ContentKind, content: impl Into<String>) -> Self {
698        Self {
699            id: id.into(),
700            kind,
701            content: content.into(),
702            structured: None,
703            confidence: None,
704            requires_human: false,
705        }
706    }
707
708    /// Mark this proposal as requiring human approval.
709    #[must_use]
710    pub fn with_human_required(mut self) -> Self {
711        self.requires_human = true;
712        self
713    }
714
715    /// Add confidence score.
716    #[must_use]
717    pub fn with_confidence(mut self, confidence: f32) -> Self {
718        self.confidence = Some(confidence);
719        self
720    }
721}
722
723/// Contract validation result for a proposal.
724#[derive(Debug, Clone, Serialize, Deserialize)]
725pub struct ContractResult {
726    /// Name of the contract or truth
727    pub name: String,
728    /// Whether it passed
729    pub passed: bool,
730    /// Failure reason if not passed
731    pub failure_reason: Option<String>,
732}
733
734impl ContractResult {
735    /// Create a passing result.
736    #[must_use]
737    pub fn passed(name: impl Into<String>) -> Self {
738        Self {
739            name: name.into(),
740            passed: true,
741            failure_reason: None,
742        }
743    }
744
745    /// Create a failing result.
746    #[must_use]
747    pub fn failed(name: impl Into<String>, reason: impl Into<String>) -> Self {
748        Self {
749            name: name.into(),
750            passed: false,
751            failure_reason: Some(reason.into()),
752        }
753    }
754}
755
756/// A proposal from the reasoning kernel.
757///
758/// This is the **only** output type that crosses the kernel boundary.
759/// It must be validated and promoted by converge-core before becoming a Fact.
760///
761/// # Axiom: "Agents Suggest, Engines Decide"
762///
763/// Kernels (including LLM kernels) emit `KernelProposal`, not `Fact`.
764/// The engine validates proposals against contracts and truth requirements
765/// before promoting them to facts.
766#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct KernelProposal {
768    /// Unique identifier for this proposal
769    pub id: String,
770    /// What kind of proposal this is
771    pub kind: ProposalKind,
772    /// The actual content/payload
773    pub payload: String,
774    /// Structured payload (if applicable)
775    pub structured_payload: Option<serde_json::Value>,
776    /// Link to the generation trace (for audit/replay)
777    pub trace_link: ReplayTrace,
778    /// Contract/truth validation results
779    pub contract_results: Vec<ContractResult>,
780    /// Whether this proposal requires human approval
781    pub requires_human: bool,
782    /// Confidence score (0.0 - 1.0) if available
783    pub confidence: Option<f32>,
784}
785
786impl KernelProposal {
787    /// Check if all contracts passed.
788    #[must_use]
789    pub fn all_contracts_passed(&self) -> bool {
790        self.contract_results.iter().all(|r| r.passed)
791    }
792
793    /// Get failed contract names.
794    #[must_use]
795    pub fn failed_contracts(&self) -> Vec<&str> {
796        self.contract_results
797            .iter()
798            .filter(|r| !r.passed)
799            .map(|r| r.name.as_str())
800            .collect()
801    }
802
803    /// Check if this proposal is eligible for automatic promotion.
804    ///
805    /// A proposal can be auto-promoted if:
806    /// - All contracts passed
807    /// - Human approval is not required
808    #[must_use]
809    pub fn is_auto_promotable(&self) -> bool {
810        self.all_contracts_passed() && !self.requires_human
811    }
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817
818    #[test]
819    fn trace_link_replayability() {
820        let local = ReplayTrace::Local(LocalReplayTrace {
821            base_model_hash: "abc123".to_string(),
822            adapter: None,
823            tokenizer_hash: "tok123".to_string(),
824            seed: 42,
825            sampler: SamplerParams::default(),
826            prompt_version: "v1".to_string(),
827            recall: None,
828            weights_mutated: false,
829            execution_env: ExecutionEnv::default(),
830        });
831
832        let remote = ReplayTrace::Remote(RemoteReplayTrace {
833            provider_name: "anthropic".to_string(),
834            provider_model_id: "claude-3-opus".to_string(),
835            request_fingerprint: "req123".to_string(),
836            response_fingerprint: "resp456".to_string(),
837            temperature: 0.0,
838            top_p: 1.0,
839            max_tokens: 1024,
840            provider_metadata: HashMap::new(),
841            retried: false,
842            retry_reasons: vec![],
843            replayability: Replayability::BestEffort,
844        });
845
846        assert!(local.is_replay_eligible());
847        assert!(!remote.is_replay_eligible());
848
849        assert_eq!(local.replayability(), Replayability::Deterministic);
850        assert_eq!(remote.replayability(), Replayability::BestEffort);
851    }
852
853    #[test]
854    fn proposal_kind_conversion() {
855        assert_eq!(ProposalKind::from(ContentKind::Claim), ProposalKind::Claims);
856        assert_eq!(ProposalKind::from(ContentKind::Plan), ProposalKind::Plan);
857        assert_eq!(
858            ProposalKind::from(ContentKind::Reasoning),
859            ProposalKind::Reasoning
860        );
861    }
862
863    #[test]
864    fn contract_result_helpers() {
865        let passed = ContractResult::passed("grounded-answering");
866        assert!(passed.passed);
867        assert!(passed.failure_reason.is_none());
868
869        let failed = ContractResult::failed("reasoning", "missing CONCLUSION");
870        assert!(!failed.passed);
871        assert_eq!(failed.failure_reason.as_deref(), Some("missing CONCLUSION"));
872    }
873
874    #[test]
875    fn kernel_proposal_auto_promotable() {
876        let local_trace = ReplayTrace::Local(LocalReplayTrace {
877            base_model_hash: "hash".to_string(),
878            adapter: None,
879            tokenizer_hash: "tok".to_string(),
880            seed: 1,
881            sampler: SamplerParams::default(),
882            prompt_version: "v1".to_string(),
883            recall: None,
884            weights_mutated: false,
885            execution_env: ExecutionEnv::default(),
886        });
887
888        // All passed, no human required
889        let promotable = KernelProposal {
890            id: "p1".to_string(),
891            kind: ProposalKind::Claims,
892            payload: "claim".to_string(),
893            structured_payload: None,
894            trace_link: local_trace.clone(),
895            contract_results: vec![ContractResult::passed("c1")],
896            requires_human: false,
897            confidence: Some(0.9),
898        };
899        assert!(promotable.is_auto_promotable());
900
901        // Human required
902        let needs_human = KernelProposal {
903            id: "p2".to_string(),
904            kind: ProposalKind::Claims,
905            payload: "claim".to_string(),
906            structured_payload: None,
907            trace_link: local_trace.clone(),
908            contract_results: vec![ContractResult::passed("c1")],
909            requires_human: true,
910            confidence: Some(0.9),
911        };
912        assert!(!needs_human.is_auto_promotable());
913
914        // Contract failed
915        let failed_contract = KernelProposal {
916            id: "p3".to_string(),
917            kind: ProposalKind::Claims,
918            payload: "claim".to_string(),
919            structured_payload: None,
920            trace_link: local_trace,
921            contract_results: vec![ContractResult::failed("c1", "reason")],
922            requires_human: false,
923            confidence: Some(0.9),
924        };
925        assert!(!failed_contract.is_auto_promotable());
926    }
927
928    // ========================================================================
929    // Kernel Input Types Tests
930    // ========================================================================
931
932    #[test]
933    fn kernel_intent_builder() {
934        let intent = KernelIntent::new("analyze_metrics")
935            .with_criteria("find anomalies")
936            .with_criteria("suggest fixes")
937            .with_max_tokens(512);
938
939        assert_eq!(intent.task, "analyze_metrics");
940        assert_eq!(intent.criteria.len(), 2);
941        assert_eq!(intent.criteria[0], "find anomalies");
942        assert_eq!(intent.criteria[1], "suggest fixes");
943        assert_eq!(intent.max_tokens, 512);
944    }
945
946    #[test]
947    fn kernel_context_builder() {
948        let context = KernelContext::new()
949            .with_state("metric", serde_json::json!(0.5))
950            .with_fact("Seeds", "seed-1", "Some seed fact")
951            .with_tenant("tenant-123");
952
953        assert!(context.state.contains_key("metric"));
954        assert_eq!(context.facts.len(), 1);
955        assert_eq!(context.facts[0].key, "Seeds");
956        assert_eq!(context.facts[0].id, "seed-1");
957        assert_eq!(context.tenant_id, Some("tenant-123".to_string()));
958    }
959
960    #[test]
961    fn kernel_context_default() {
962        let context = KernelContext::default();
963        assert!(context.state.is_empty());
964        assert!(context.facts.is_empty());
965        assert!(context.tenant_id.is_none());
966    }
967
968    #[test]
969    fn kernel_policy_default() {
970        let policy = KernelPolicy::default();
971        assert!(policy.adapter_id.is_none());
972        assert!(!policy.recall_enabled);
973        assert_eq!(policy.recall_max_candidates, 5);
974        assert!((policy.recall_min_score - 0.7).abs() < f32::EPSILON);
975        assert!(policy.seed.is_none());
976        assert!(!policy.requires_human);
977        assert!(policy.required_truths.is_empty());
978    }
979
980    #[test]
981    fn kernel_policy_deterministic() {
982        let policy = KernelPolicy::deterministic(42)
983            .with_adapter("llm/grounded@1.0.0")
984            .with_recall(true)
985            .with_human_required()
986            .with_required_truth("grounded-answering");
987
988        assert_eq!(policy.seed, Some(42));
989        assert_eq!(policy.adapter_id, Some("llm/grounded@1.0.0".to_string()));
990        assert!(policy.recall_enabled);
991        assert!(policy.requires_human);
992        assert_eq!(policy.required_truths, vec!["grounded-answering"]);
993    }
994}