converge_core/
kernel_boundary.rs

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