Skip to main content

oris_evokernel/
core.rs

1//! EvoKernel orchestration: mutation capture, validation, capsule construction, and replay-first reuse.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::{Arc, Mutex};
8
9use async_trait::async_trait;
10use chrono::{DateTime, Duration, Utc};
11use oris_agent_contract::{
12    infer_mutation_needed_failure_reason_code, infer_replay_fallback_reason_code,
13    normalize_mutation_needed_failure_contract, normalize_replay_fallback_contract, AgentRole,
14    BoundedTaskClass, CoordinationMessage, CoordinationPlan, CoordinationPrimitive,
15    CoordinationResult, CoordinationTask, ExecutionFeedback, MutationNeededFailureContract,
16    MutationNeededFailureReasonCode, MutationProposal as AgentMutationProposal, ReplayFeedback,
17    ReplayPlannerDirective, SupervisedDevloopOutcome, SupervisedDevloopRequest,
18    SupervisedDevloopStatus,
19};
20use oris_economics::{EconomicsSignal, EvuLedger, StakePolicy};
21use oris_evolution::{
22    compute_artifact_hash, decayed_replay_confidence, next_id, stable_hash_json, AssetState,
23    BlastRadius, CandidateSource, Capsule, CapsuleId, EnvFingerprint, EvolutionError,
24    EvolutionEvent, EvolutionProjection, EvolutionStore, Gene, GeneCandidate, MutationId,
25    PreparedMutation, ReplayRoiEvidence, ReplayRoiReasonCode, Selector, SelectorInput,
26    StoreBackedSelector, StoredEvolutionEvent, ValidationSnapshot, MIN_REPLAY_CONFIDENCE,
27};
28use oris_evolution_network::{EvolutionEnvelope, NetworkAsset, SyncAudit};
29use oris_governor::{DefaultGovernor, Governor, GovernorDecision, GovernorInput};
30use oris_kernel::{Kernel, KernelState, RunId};
31use oris_sandbox::{
32    compute_blast_radius, execute_allowed_command, Sandbox, SandboxPolicy, SandboxReceipt,
33};
34use oris_spec::CompiledMutationPlan;
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37use thiserror::Error;
38
39pub use oris_evolution::{
40    default_store_root, ArtifactEncoding, AssetState as EvoAssetState,
41    BlastRadius as EvoBlastRadius, CandidateSource as EvoCandidateSource,
42    EnvFingerprint as EvoEnvFingerprint, EvolutionStore as EvoEvolutionStore, JsonlEvolutionStore,
43    MutationArtifact, MutationIntent, MutationTarget, Outcome, RiskLevel,
44    SelectorInput as EvoSelectorInput, TransitionEvidence, TransitionReasonCode,
45    TransitionReasonCode as EvoTransitionReasonCode,
46};
47pub use oris_evolution_network::{
48    FetchQuery, FetchResponse, MessageType, PublishRequest, RevokeNotice,
49};
50pub use oris_governor::{CoolingWindow, GovernorConfig, RevocationReason};
51pub use oris_sandbox::{LocalProcessSandbox, SandboxPolicy as EvoSandboxPolicy};
52pub use oris_spec::{SpecCompileError, SpecCompiler, SpecDocument};
53
54#[derive(Clone, Debug, Serialize, Deserialize)]
55pub struct ValidationPlan {
56    pub profile: String,
57    pub stages: Vec<ValidationStage>,
58}
59
60impl ValidationPlan {
61    pub fn oris_default() -> Self {
62        Self {
63            profile: "oris-default".into(),
64            stages: vec![
65                ValidationStage::Command {
66                    program: "cargo".into(),
67                    args: vec!["fmt".into(), "--all".into(), "--check".into()],
68                    timeout_ms: 60_000,
69                },
70                ValidationStage::Command {
71                    program: "cargo".into(),
72                    args: vec!["check".into(), "--workspace".into()],
73                    timeout_ms: 180_000,
74                },
75                ValidationStage::Command {
76                    program: "cargo".into(),
77                    args: vec![
78                        "test".into(),
79                        "-p".into(),
80                        "oris-kernel".into(),
81                        "-p".into(),
82                        "oris-evolution".into(),
83                        "-p".into(),
84                        "oris-sandbox".into(),
85                        "-p".into(),
86                        "oris-evokernel".into(),
87                        "--lib".into(),
88                    ],
89                    timeout_ms: 300_000,
90                },
91                ValidationStage::Command {
92                    program: "cargo".into(),
93                    args: vec![
94                        "test".into(),
95                        "-p".into(),
96                        "oris-runtime".into(),
97                        "--lib".into(),
98                    ],
99                    timeout_ms: 300_000,
100                },
101            ],
102        }
103    }
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize)]
107pub enum ValidationStage {
108    Command {
109        program: String,
110        args: Vec<String>,
111        timeout_ms: u64,
112    },
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize)]
116pub struct ValidationStageReport {
117    pub stage: String,
118    pub success: bool,
119    pub exit_code: Option<i32>,
120    pub duration_ms: u64,
121    pub stdout: String,
122    pub stderr: String,
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct ValidationReport {
127    pub success: bool,
128    pub duration_ms: u64,
129    pub stages: Vec<ValidationStageReport>,
130    pub logs: String,
131}
132
133#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
134pub struct SignalExtractionInput {
135    pub patch_diff: String,
136    pub intent: String,
137    pub expected_effect: String,
138    pub declared_signals: Vec<String>,
139    pub changed_files: Vec<String>,
140    pub validation_success: bool,
141    pub validation_logs: String,
142    pub stage_outputs: Vec<String>,
143}
144
145#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
146pub struct SignalExtractionOutput {
147    pub values: Vec<String>,
148    pub hash: String,
149}
150
151#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
152pub struct SeedTemplate {
153    pub id: String,
154    pub intent: String,
155    pub signals: Vec<String>,
156    pub diff_payload: String,
157    pub validation_profile: String,
158}
159
160#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
161pub struct BootstrapReport {
162    pub seeded: bool,
163    pub genes_added: usize,
164    pub capsules_added: usize,
165}
166
167const REPORTED_EXPERIENCE_RETENTION_LIMIT: usize = 3;
168const SHADOW_PROMOTION_MIN_REPLAY_ATTEMPTS: u64 = 2;
169const SHADOW_PROMOTION_MIN_SUCCESS_RATE: f32 = 0.70;
170const SHADOW_PROMOTION_MIN_ENV_MATCH: f32 = 0.75;
171const SHADOW_PROMOTION_MIN_DECAYED_CONFIDENCE: f32 = MIN_REPLAY_CONFIDENCE;
172const REPLAY_REASONING_TOKEN_FLOOR: u64 = 192;
173const REPLAY_REASONING_TOKEN_SIGNAL_WEIGHT: u64 = 24;
174const COLD_START_LOOKUP_PENALTY: f32 = 0.05;
175const MUTATION_NEEDED_MAX_DIFF_BYTES: usize = 128 * 1024;
176const MUTATION_NEEDED_MAX_CHANGED_LINES: usize = 600;
177const MUTATION_NEEDED_MAX_SANDBOX_DURATION_MS: u64 = 120_000;
178const MUTATION_NEEDED_MAX_VALIDATION_BUDGET_MS: u64 = 900_000;
179pub const REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS: [&str; 2] =
180    ["task_class", "source_sender_id"];
181
182#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
183pub struct RepairQualityGateReport {
184    pub root_cause: bool,
185    pub fix: bool,
186    pub verification: bool,
187    pub rollback: bool,
188    pub incident_anchor: bool,
189    pub structure_score: usize,
190    pub has_actionable_command: bool,
191}
192
193impl RepairQualityGateReport {
194    pub fn passes(&self) -> bool {
195        self.incident_anchor
196            && self.structure_score >= 3
197            && (self.has_actionable_command || self.verification)
198    }
199
200    pub fn failed_checks(&self) -> Vec<String> {
201        let mut failed = Vec::new();
202        if !self.incident_anchor {
203            failed.push("包含unknown command故障上下文".to_string());
204        }
205        if self.structure_score < 3 {
206            failed.push("结构化修复信息至少满足3项(根因/修复/验证/回滚)".to_string());
207        }
208        if !(self.has_actionable_command || self.verification) {
209            failed.push("包含可执行验证命令或验证计划".to_string());
210        }
211        failed
212    }
213}
214
215pub fn evaluate_repair_quality_gate(plan: &str) -> RepairQualityGateReport {
216    fn contains_any(haystack: &str, needles: &[&str]) -> bool {
217        needles.iter().any(|needle| haystack.contains(needle))
218    }
219
220    let lower = plan.to_ascii_lowercase();
221    let root_cause = contains_any(
222        plan,
223        &["根因", "原因分析", "问题定位", "原因定位", "根本原因"],
224    ) || contains_any(
225        &lower,
226        &[
227            "root cause",
228            "cause analysis",
229            "problem diagnosis",
230            "diagnosis",
231        ],
232    );
233    let fix = contains_any(
234        plan,
235        &["修复步骤", "修复方案", "处理步骤", "修复建议", "整改方案"],
236    ) || contains_any(
237        &lower,
238        &[
239            "fix",
240            "remediation",
241            "mitigation",
242            "resolution",
243            "repair steps",
244        ],
245    );
246    let verification = contains_any(
247        plan,
248        &["验证命令", "验证步骤", "回归测试", "验证方式", "验收步骤"],
249    ) || contains_any(
250        &lower,
251        &[
252            "verification",
253            "validate",
254            "regression test",
255            "smoke test",
256            "test command",
257        ],
258    );
259    let rollback = contains_any(plan, &["回滚方案", "回滚步骤", "恢复方案", "撤销方案"])
260        || contains_any(&lower, &["rollback", "revert", "fallback plan", "undo"]);
261    let incident_anchor = contains_any(
262        &lower,
263        &[
264            "unknown command",
265            "process",
266            "proccess",
267            "command not found",
268        ],
269    ) || contains_any(plan, &["命令不存在", "命令未找到", "未知命令"]);
270    let structure_score = [root_cause, fix, verification, rollback]
271        .into_iter()
272        .filter(|ok| *ok)
273        .count();
274    let has_actionable_command = contains_any(
275        &lower,
276        &[
277            "cargo ", "git ", "python ", "pip ", "npm ", "pnpm ", "yarn ", "bash ", "make ",
278        ],
279    );
280
281    RepairQualityGateReport {
282        root_cause,
283        fix,
284        verification,
285        rollback,
286        incident_anchor,
287        structure_score,
288        has_actionable_command,
289    }
290}
291
292impl ValidationReport {
293    pub fn to_snapshot(&self, profile: &str) -> ValidationSnapshot {
294        ValidationSnapshot {
295            success: self.success,
296            profile: profile.to_string(),
297            duration_ms: self.duration_ms,
298            summary: if self.success {
299                "validation passed".into()
300            } else {
301                "validation failed".into()
302            },
303        }
304    }
305}
306
307pub fn extract_deterministic_signals(input: &SignalExtractionInput) -> SignalExtractionOutput {
308    let mut signals = BTreeSet::new();
309
310    for declared in &input.declared_signals {
311        if let Some(phrase) = normalize_signal_phrase(declared) {
312            signals.insert(phrase);
313        }
314        extend_signal_tokens(&mut signals, declared);
315    }
316
317    for text in [
318        input.patch_diff.as_str(),
319        input.intent.as_str(),
320        input.expected_effect.as_str(),
321        input.validation_logs.as_str(),
322    ] {
323        extend_signal_tokens(&mut signals, text);
324    }
325
326    for changed_file in &input.changed_files {
327        extend_signal_tokens(&mut signals, changed_file);
328    }
329
330    for stage_output in &input.stage_outputs {
331        extend_signal_tokens(&mut signals, stage_output);
332    }
333
334    signals.insert(if input.validation_success {
335        "validation passed".into()
336    } else {
337        "validation failed".into()
338    });
339
340    let values = signals.into_iter().take(32).collect::<Vec<_>>();
341    let hash =
342        stable_hash_json(&values).unwrap_or_else(|_| compute_artifact_hash(&values.join("\n")));
343    SignalExtractionOutput { values, hash }
344}
345
346#[derive(Debug, Error)]
347pub enum ValidationError {
348    #[error("validation execution failed: {0}")]
349    Execution(String),
350}
351
352#[async_trait]
353pub trait Validator: Send + Sync {
354    async fn run(
355        &self,
356        receipt: &SandboxReceipt,
357        plan: &ValidationPlan,
358    ) -> Result<ValidationReport, ValidationError>;
359}
360
361pub struct CommandValidator {
362    policy: SandboxPolicy,
363}
364
365impl CommandValidator {
366    pub fn new(policy: SandboxPolicy) -> Self {
367        Self { policy }
368    }
369}
370
371#[async_trait]
372impl Validator for CommandValidator {
373    async fn run(
374        &self,
375        receipt: &SandboxReceipt,
376        plan: &ValidationPlan,
377    ) -> Result<ValidationReport, ValidationError> {
378        let started = std::time::Instant::now();
379        let mut stages = Vec::new();
380        let mut success = true;
381        let mut logs = String::new();
382
383        for stage in &plan.stages {
384            match stage {
385                ValidationStage::Command {
386                    program,
387                    args,
388                    timeout_ms,
389                } => {
390                    let result = execute_allowed_command(
391                        &self.policy,
392                        &receipt.workdir,
393                        program,
394                        args,
395                        *timeout_ms,
396                    )
397                    .await;
398                    let report = match result {
399                        Ok(output) => ValidationStageReport {
400                            stage: format!("{program} {}", args.join(" ")),
401                            success: output.success,
402                            exit_code: output.exit_code,
403                            duration_ms: output.duration_ms,
404                            stdout: output.stdout,
405                            stderr: output.stderr,
406                        },
407                        Err(err) => ValidationStageReport {
408                            stage: format!("{program} {}", args.join(" ")),
409                            success: false,
410                            exit_code: None,
411                            duration_ms: 0,
412                            stdout: String::new(),
413                            stderr: err.to_string(),
414                        },
415                    };
416                    if !report.success {
417                        success = false;
418                    }
419                    if !report.stdout.is_empty() {
420                        logs.push_str(&report.stdout);
421                        logs.push('\n');
422                    }
423                    if !report.stderr.is_empty() {
424                        logs.push_str(&report.stderr);
425                        logs.push('\n');
426                    }
427                    stages.push(report);
428                    if !success {
429                        break;
430                    }
431                }
432            }
433        }
434
435        Ok(ValidationReport {
436            success,
437            duration_ms: started.elapsed().as_millis() as u64,
438            stages,
439            logs,
440        })
441    }
442}
443
444#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
445pub struct ReplayDetectEvidence {
446    pub task_class_id: String,
447    pub task_label: String,
448    pub matched_signals: Vec<String>,
449    pub mismatch_reasons: Vec<String>,
450}
451
452#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
453pub struct ReplayCandidateEvidence {
454    pub rank: usize,
455    pub gene_id: String,
456    pub capsule_id: Option<String>,
457    pub match_quality: f32,
458    pub confidence: Option<f32>,
459    pub environment_match_factor: Option<f32>,
460    pub cold_start_penalty: f32,
461    pub final_score: f32,
462}
463
464#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
465pub struct ReplaySelectEvidence {
466    pub exact_match_lookup: bool,
467    pub selected_gene_id: Option<String>,
468    pub selected_capsule_id: Option<String>,
469    pub candidates: Vec<ReplayCandidateEvidence>,
470}
471
472#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
473pub struct ReplayDecision {
474    pub used_capsule: bool,
475    pub capsule_id: Option<CapsuleId>,
476    pub fallback_to_planner: bool,
477    pub reason: String,
478    pub detect_evidence: ReplayDetectEvidence,
479    pub select_evidence: ReplaySelectEvidence,
480    pub economics_evidence: ReplayRoiEvidence,
481}
482
483#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
484pub struct ReplayTaskClassMetrics {
485    pub task_class_id: String,
486    pub task_label: String,
487    pub replay_success_total: u64,
488    pub replay_failure_total: u64,
489    pub reasoning_steps_avoided_total: u64,
490    pub reasoning_avoided_tokens_total: u64,
491    pub replay_fallback_cost_total: u64,
492    pub replay_roi: f64,
493}
494
495#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
496pub struct ReplaySourceRoiMetrics {
497    pub source_sender_id: String,
498    pub replay_success_total: u64,
499    pub replay_failure_total: u64,
500    pub reasoning_avoided_tokens_total: u64,
501    pub replay_fallback_cost_total: u64,
502    pub replay_roi: f64,
503}
504
505#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
506pub struct ReplayRoiWindowSummary {
507    pub generated_at: String,
508    pub window_seconds: u64,
509    pub replay_attempts_total: u64,
510    pub replay_success_total: u64,
511    pub replay_failure_total: u64,
512    pub reasoning_avoided_tokens_total: u64,
513    pub replay_fallback_cost_total: u64,
514    pub replay_roi: f64,
515    pub replay_task_classes: Vec<ReplayTaskClassMetrics>,
516    pub replay_sources: Vec<ReplaySourceRoiMetrics>,
517}
518
519#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
520pub struct ReplayRoiReleaseGateThresholds {
521    pub min_replay_attempts: u64,
522    pub min_replay_hit_rate: f64,
523    pub max_false_replay_rate: f64,
524    pub min_reasoning_avoided_tokens: u64,
525    pub min_replay_roi: f64,
526    pub require_replay_safety: bool,
527}
528
529impl Default for ReplayRoiReleaseGateThresholds {
530    fn default() -> Self {
531        Self {
532            min_replay_attempts: 3,
533            min_replay_hit_rate: 0.60,
534            max_false_replay_rate: 0.25,
535            min_reasoning_avoided_tokens: REPLAY_REASONING_TOKEN_FLOOR,
536            min_replay_roi: 0.05,
537            require_replay_safety: true,
538        }
539    }
540}
541
542#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
543#[serde(rename_all = "snake_case")]
544pub enum ReplayRoiReleaseGateAction {
545    BlockRelease,
546}
547
548#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
549pub struct ReplayRoiReleaseGateFailClosedPolicy {
550    pub on_threshold_violation: ReplayRoiReleaseGateAction,
551    pub on_missing_metrics: ReplayRoiReleaseGateAction,
552    pub on_invalid_metrics: ReplayRoiReleaseGateAction,
553}
554
555impl Default for ReplayRoiReleaseGateFailClosedPolicy {
556    fn default() -> Self {
557        Self {
558            on_threshold_violation: ReplayRoiReleaseGateAction::BlockRelease,
559            on_missing_metrics: ReplayRoiReleaseGateAction::BlockRelease,
560            on_invalid_metrics: ReplayRoiReleaseGateAction::BlockRelease,
561        }
562    }
563}
564
565#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
566pub struct ReplayRoiReleaseGateSafetySignal {
567    pub fail_closed_default: bool,
568    pub rollback_ready: bool,
569    pub audit_trail_complete: bool,
570    pub has_replay_activity: bool,
571}
572
573#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
574pub struct ReplayRoiReleaseGateInputContract {
575    pub generated_at: String,
576    pub window_seconds: u64,
577    pub aggregation_dimensions: Vec<String>,
578    pub replay_attempts_total: u64,
579    pub replay_success_total: u64,
580    pub replay_failure_total: u64,
581    pub replay_hit_rate: f64,
582    pub false_replay_rate: f64,
583    pub reasoning_avoided_tokens: u64,
584    pub replay_fallback_cost_total: u64,
585    pub replay_roi: f64,
586    pub replay_safety: bool,
587    pub replay_safety_signal: ReplayRoiReleaseGateSafetySignal,
588    pub thresholds: ReplayRoiReleaseGateThresholds,
589    pub fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy,
590}
591
592#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
593#[serde(rename_all = "snake_case")]
594pub enum ReplayRoiReleaseGateStatus {
595    Pass,
596    FailClosed,
597    Indeterminate,
598}
599
600#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
601pub struct ReplayRoiReleaseGateOutputContract {
602    pub status: ReplayRoiReleaseGateStatus,
603    pub failed_checks: Vec<String>,
604    pub evidence_refs: Vec<String>,
605    pub summary: String,
606}
607
608#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
609pub struct ReplayRoiReleaseGateContract {
610    pub input: ReplayRoiReleaseGateInputContract,
611    pub output: ReplayRoiReleaseGateOutputContract,
612}
613
614#[derive(Clone, Copy, Debug, Eq, PartialEq)]
615enum CoordinationTaskState {
616    Ready,
617    Waiting,
618    BlockedByFailure,
619    PermanentlyBlocked,
620}
621
622#[derive(Clone, Debug, Default)]
623pub struct MultiAgentCoordinator;
624
625impl MultiAgentCoordinator {
626    pub fn new() -> Self {
627        Self
628    }
629
630    pub fn coordinate(&self, plan: CoordinationPlan) -> CoordinationResult {
631        let primitive = plan.primitive.clone();
632        let root_goal = plan.root_goal.clone();
633        let timeout_ms = plan.timeout_ms;
634        let max_retries = plan.max_retries;
635        let mut tasks = BTreeMap::new();
636        for task in plan.tasks {
637            tasks.entry(task.id.clone()).or_insert(task);
638        }
639
640        let mut pending = tasks.keys().cloned().collect::<BTreeSet<_>>();
641        let mut completed = BTreeSet::new();
642        let mut failed = BTreeSet::new();
643        let mut completed_order = Vec::new();
644        let mut failed_order = Vec::new();
645        let mut skipped = BTreeSet::new();
646        let mut attempts = BTreeMap::new();
647        let mut messages = Vec::new();
648
649        loop {
650            if matches!(primitive, CoordinationPrimitive::Conditional) {
651                self.apply_conditional_skips(
652                    &tasks,
653                    &mut pending,
654                    &completed,
655                    &failed,
656                    &mut skipped,
657                    &mut messages,
658                );
659            }
660
661            let mut ready = self.ready_task_ids(&tasks, &pending, &completed, &failed, &skipped);
662            if ready.is_empty() {
663                break;
664            }
665            if matches!(primitive, CoordinationPrimitive::Sequential) {
666                ready.truncate(1);
667            }
668
669            for task_id in ready {
670                let Some(task) = tasks.get(&task_id) else {
671                    continue;
672                };
673                if !pending.contains(&task_id) {
674                    continue;
675                }
676                self.record_handoff_messages(task, &tasks, &completed, &failed, &mut messages);
677
678                let prior_failures = attempts.get(&task_id).copied().unwrap_or(0);
679                if Self::simulate_task_failure(task, prior_failures) {
680                    let failure_count = prior_failures + 1;
681                    attempts.insert(task_id.clone(), failure_count);
682                    let will_retry = failure_count <= max_retries;
683                    messages.push(CoordinationMessage {
684                        from_role: task.role.clone(),
685                        to_role: task.role.clone(),
686                        task_id: task_id.clone(),
687                        content: if will_retry {
688                            format!("task {task_id} failed on attempt {failure_count} and will retry")
689                        } else {
690                            format!(
691                                "task {task_id} failed on attempt {failure_count} and exhausted retries"
692                            )
693                        },
694                    });
695                    if !will_retry {
696                        pending.remove(&task_id);
697                        if failed.insert(task_id.clone()) {
698                            failed_order.push(task_id);
699                        }
700                    }
701                    continue;
702                }
703
704                pending.remove(&task_id);
705                if completed.insert(task_id.clone()) {
706                    completed_order.push(task_id);
707                }
708            }
709        }
710
711        let blocked_ids = pending.into_iter().collect::<Vec<_>>();
712        for task_id in blocked_ids {
713            let Some(task) = tasks.get(&task_id) else {
714                continue;
715            };
716            let state = self.classify_task(task, &tasks, &completed, &failed, &skipped);
717            let content = match state {
718                CoordinationTaskState::BlockedByFailure => {
719                    format!("task {task_id} blocked by failed dependencies")
720                }
721                CoordinationTaskState::PermanentlyBlocked => {
722                    format!("task {task_id} has invalid coordination prerequisites")
723                }
724                CoordinationTaskState::Waiting => {
725                    format!("task {task_id} has unresolved dependencies")
726                }
727                CoordinationTaskState::Ready => {
728                    format!("task {task_id} was left pending unexpectedly")
729                }
730            };
731            messages.push(CoordinationMessage {
732                from_role: task.role.clone(),
733                to_role: task.role.clone(),
734                task_id: task_id.clone(),
735                content,
736            });
737            if failed.insert(task_id.clone()) {
738                failed_order.push(task_id);
739            }
740        }
741
742        CoordinationResult {
743            completed_tasks: completed_order,
744            failed_tasks: failed_order,
745            messages,
746            summary: format!(
747                "goal '{}' completed {} tasks, failed {}, skipped {} using {:?} coordination (timeout={}ms, max_retries={})",
748                root_goal,
749                completed.len(),
750                failed.len(),
751                skipped.len(),
752                primitive,
753                timeout_ms,
754                max_retries
755            ),
756        }
757    }
758
759    fn ready_task_ids(
760        &self,
761        tasks: &BTreeMap<String, CoordinationTask>,
762        pending: &BTreeSet<String>,
763        completed: &BTreeSet<String>,
764        failed: &BTreeSet<String>,
765        skipped: &BTreeSet<String>,
766    ) -> Vec<String> {
767        pending
768            .iter()
769            .filter_map(|task_id| {
770                let task = tasks.get(task_id)?;
771                (self.classify_task(task, tasks, completed, failed, skipped)
772                    == CoordinationTaskState::Ready)
773                    .then(|| task_id.clone())
774            })
775            .collect()
776    }
777
778    fn apply_conditional_skips(
779        &self,
780        tasks: &BTreeMap<String, CoordinationTask>,
781        pending: &mut BTreeSet<String>,
782        completed: &BTreeSet<String>,
783        failed: &BTreeSet<String>,
784        skipped: &mut BTreeSet<String>,
785        messages: &mut Vec<CoordinationMessage>,
786    ) {
787        let skip_ids = pending
788            .iter()
789            .filter_map(|task_id| {
790                let task = tasks.get(task_id)?;
791                (self.classify_task(task, tasks, completed, failed, skipped)
792                    == CoordinationTaskState::BlockedByFailure)
793                    .then(|| task_id.clone())
794            })
795            .collect::<Vec<_>>();
796
797        for task_id in skip_ids {
798            let Some(task) = tasks.get(&task_id) else {
799                continue;
800            };
801            pending.remove(&task_id);
802            skipped.insert(task_id.clone());
803            messages.push(CoordinationMessage {
804                from_role: task.role.clone(),
805                to_role: task.role.clone(),
806                task_id: task_id.clone(),
807                content: format!("task {task_id} skipped due to failed dependency chain"),
808            });
809        }
810    }
811
812    fn classify_task(
813        &self,
814        task: &CoordinationTask,
815        tasks: &BTreeMap<String, CoordinationTask>,
816        completed: &BTreeSet<String>,
817        failed: &BTreeSet<String>,
818        skipped: &BTreeSet<String>,
819    ) -> CoordinationTaskState {
820        match task.role {
821            AgentRole::Planner | AgentRole::Coder => {
822                let mut waiting = false;
823                for dependency_id in &task.depends_on {
824                    if !tasks.contains_key(dependency_id) {
825                        return CoordinationTaskState::PermanentlyBlocked;
826                    }
827                    if skipped.contains(dependency_id) || failed.contains(dependency_id) {
828                        return CoordinationTaskState::BlockedByFailure;
829                    }
830                    if !completed.contains(dependency_id) {
831                        waiting = true;
832                    }
833                }
834                if waiting {
835                    CoordinationTaskState::Waiting
836                } else {
837                    CoordinationTaskState::Ready
838                }
839            }
840            AgentRole::Repair => {
841                let mut waiting = false;
842                let mut has_coder_dependency = false;
843                let mut has_failed_coder = false;
844                for dependency_id in &task.depends_on {
845                    let Some(dependency) = tasks.get(dependency_id) else {
846                        return CoordinationTaskState::PermanentlyBlocked;
847                    };
848                    let is_coder = matches!(dependency.role, AgentRole::Coder);
849                    if is_coder {
850                        has_coder_dependency = true;
851                    }
852                    if skipped.contains(dependency_id) {
853                        return CoordinationTaskState::BlockedByFailure;
854                    }
855                    if failed.contains(dependency_id) {
856                        if is_coder {
857                            has_failed_coder = true;
858                        } else {
859                            return CoordinationTaskState::BlockedByFailure;
860                        }
861                        continue;
862                    }
863                    if !completed.contains(dependency_id) {
864                        waiting = true;
865                    }
866                }
867                if !has_coder_dependency {
868                    CoordinationTaskState::PermanentlyBlocked
869                } else if waiting {
870                    CoordinationTaskState::Waiting
871                } else if has_failed_coder {
872                    CoordinationTaskState::Ready
873                } else {
874                    CoordinationTaskState::PermanentlyBlocked
875                }
876            }
877            AgentRole::Optimizer => {
878                let mut waiting = false;
879                let mut has_impl_dependency = false;
880                let mut has_completed_impl = false;
881                let mut has_failed_impl = false;
882                for dependency_id in &task.depends_on {
883                    let Some(dependency) = tasks.get(dependency_id) else {
884                        return CoordinationTaskState::PermanentlyBlocked;
885                    };
886                    let is_impl = matches!(dependency.role, AgentRole::Coder | AgentRole::Repair);
887                    if is_impl {
888                        has_impl_dependency = true;
889                    }
890                    if skipped.contains(dependency_id) || failed.contains(dependency_id) {
891                        if is_impl {
892                            has_failed_impl = true;
893                            continue;
894                        }
895                        return CoordinationTaskState::BlockedByFailure;
896                    }
897                    if completed.contains(dependency_id) {
898                        if is_impl {
899                            has_completed_impl = true;
900                        }
901                        continue;
902                    }
903                    waiting = true;
904                }
905                if !has_impl_dependency {
906                    CoordinationTaskState::PermanentlyBlocked
907                } else if waiting {
908                    CoordinationTaskState::Waiting
909                } else if has_completed_impl {
910                    CoordinationTaskState::Ready
911                } else if has_failed_impl {
912                    CoordinationTaskState::BlockedByFailure
913                } else {
914                    CoordinationTaskState::PermanentlyBlocked
915                }
916            }
917        }
918    }
919
920    fn record_handoff_messages(
921        &self,
922        task: &CoordinationTask,
923        tasks: &BTreeMap<String, CoordinationTask>,
924        completed: &BTreeSet<String>,
925        failed: &BTreeSet<String>,
926        messages: &mut Vec<CoordinationMessage>,
927    ) {
928        let mut dependency_ids = task.depends_on.clone();
929        dependency_ids.sort();
930        dependency_ids.dedup();
931
932        for dependency_id in dependency_ids {
933            let Some(dependency) = tasks.get(&dependency_id) else {
934                continue;
935            };
936            if completed.contains(&dependency_id) {
937                messages.push(CoordinationMessage {
938                    from_role: dependency.role.clone(),
939                    to_role: task.role.clone(),
940                    task_id: task.id.clone(),
941                    content: format!("handoff from {dependency_id} to {}", task.id),
942                });
943            } else if failed.contains(&dependency_id) {
944                messages.push(CoordinationMessage {
945                    from_role: dependency.role.clone(),
946                    to_role: task.role.clone(),
947                    task_id: task.id.clone(),
948                    content: format!("failed dependency {dependency_id} routed to {}", task.id),
949                });
950            }
951        }
952    }
953
954    fn simulate_task_failure(task: &CoordinationTask, prior_failures: u32) -> bool {
955        let normalized = task.description.to_ascii_lowercase();
956        normalized.contains("force-fail")
957            || (normalized.contains("fail-once") && prior_failures == 0)
958    }
959}
960
961#[derive(Debug, Error)]
962pub enum ReplayError {
963    #[error("store error: {0}")]
964    Store(String),
965    #[error("sandbox error: {0}")]
966    Sandbox(String),
967    #[error("validation error: {0}")]
968    Validation(String),
969}
970
971#[async_trait]
972pub trait ReplayExecutor: Send + Sync {
973    async fn try_replay(
974        &self,
975        input: &SelectorInput,
976        policy: &SandboxPolicy,
977        validation: &ValidationPlan,
978    ) -> Result<ReplayDecision, ReplayError>;
979
980    async fn try_replay_for_run(
981        &self,
982        run_id: &RunId,
983        input: &SelectorInput,
984        policy: &SandboxPolicy,
985        validation: &ValidationPlan,
986    ) -> Result<ReplayDecision, ReplayError> {
987        let _ = run_id;
988        self.try_replay(input, policy, validation).await
989    }
990}
991
992pub struct StoreReplayExecutor {
993    pub sandbox: Arc<dyn Sandbox>,
994    pub validator: Arc<dyn Validator>,
995    pub store: Arc<dyn EvolutionStore>,
996    pub selector: Arc<dyn Selector>,
997    pub governor: Arc<dyn Governor>,
998    pub economics: Option<Arc<Mutex<EvuLedger>>>,
999    pub remote_publishers: Option<Arc<Mutex<BTreeMap<String, String>>>>,
1000    pub stake_policy: StakePolicy,
1001}
1002
1003struct ReplayCandidates {
1004    candidates: Vec<GeneCandidate>,
1005    exact_match: bool,
1006}
1007
1008#[async_trait]
1009impl ReplayExecutor for StoreReplayExecutor {
1010    async fn try_replay(
1011        &self,
1012        input: &SelectorInput,
1013        policy: &SandboxPolicy,
1014        validation: &ValidationPlan,
1015    ) -> Result<ReplayDecision, ReplayError> {
1016        self.try_replay_inner(None, input, policy, validation).await
1017    }
1018
1019    async fn try_replay_for_run(
1020        &self,
1021        run_id: &RunId,
1022        input: &SelectorInput,
1023        policy: &SandboxPolicy,
1024        validation: &ValidationPlan,
1025    ) -> Result<ReplayDecision, ReplayError> {
1026        self.try_replay_inner(Some(run_id), input, policy, validation)
1027            .await
1028    }
1029}
1030
1031impl StoreReplayExecutor {
1032    fn collect_replay_candidates(&self, input: &SelectorInput) -> ReplayCandidates {
1033        self.apply_confidence_revalidation();
1034        let mut selector_input = input.clone();
1035        if self.economics.is_some() && self.remote_publishers.is_some() {
1036            selector_input.limit = selector_input.limit.max(4);
1037        }
1038        let mut candidates = self.selector.select(&selector_input);
1039        self.rerank_with_reputation_bias(&mut candidates);
1040        let mut exact_match = false;
1041        if candidates.is_empty() {
1042            let mut exact_candidates = exact_match_candidates(self.store.as_ref(), input);
1043            self.rerank_with_reputation_bias(&mut exact_candidates);
1044            if !exact_candidates.is_empty() {
1045                candidates = exact_candidates;
1046                exact_match = true;
1047            }
1048        }
1049        if candidates.is_empty() {
1050            let mut remote_candidates =
1051                quarantined_remote_exact_match_candidates(self.store.as_ref(), input);
1052            self.rerank_with_reputation_bias(&mut remote_candidates);
1053            if !remote_candidates.is_empty() {
1054                candidates = remote_candidates;
1055                exact_match = true;
1056            }
1057        }
1058        candidates.truncate(input.limit.max(1));
1059        ReplayCandidates {
1060            candidates,
1061            exact_match,
1062        }
1063    }
1064
1065    fn build_select_evidence(
1066        &self,
1067        input: &SelectorInput,
1068        candidates: &[GeneCandidate],
1069        exact_match: bool,
1070    ) -> ReplaySelectEvidence {
1071        let cold_start_penalty = if exact_match {
1072            COLD_START_LOOKUP_PENALTY
1073        } else {
1074            0.0
1075        };
1076        let candidate_rows = candidates
1077            .iter()
1078            .enumerate()
1079            .map(|(idx, candidate)| {
1080                let top_capsule = candidate.capsules.first();
1081                let environment_match_factor = top_capsule
1082                    .map(|capsule| replay_environment_match_factor(&input.env, &capsule.env));
1083                let final_score = candidate.score * (1.0 - cold_start_penalty);
1084                ReplayCandidateEvidence {
1085                    rank: idx + 1,
1086                    gene_id: candidate.gene.id.clone(),
1087                    capsule_id: top_capsule.map(|capsule| capsule.id.clone()),
1088                    match_quality: candidate.score,
1089                    confidence: top_capsule.map(|capsule| capsule.confidence),
1090                    environment_match_factor,
1091                    cold_start_penalty,
1092                    final_score,
1093                }
1094            })
1095            .collect::<Vec<_>>();
1096
1097        ReplaySelectEvidence {
1098            exact_match_lookup: exact_match,
1099            selected_gene_id: candidate_rows
1100                .first()
1101                .map(|candidate| candidate.gene_id.clone()),
1102            selected_capsule_id: candidate_rows
1103                .first()
1104                .and_then(|candidate| candidate.capsule_id.clone()),
1105            candidates: candidate_rows,
1106        }
1107    }
1108
1109    fn apply_confidence_revalidation(&self) {
1110        let Ok(projection) = projection_snapshot(self.store.as_ref()) else {
1111            return;
1112        };
1113        for target in stale_replay_revalidation_targets(&projection, Utc::now()) {
1114            let reason = format!(
1115                "confidence decayed to {:.3}; revalidation required before replay",
1116                target.decayed_confidence
1117            );
1118            let confidence_decay_ratio = if target.peak_confidence > 0.0 {
1119                (target.decayed_confidence / target.peak_confidence).clamp(0.0, 1.0)
1120            } else {
1121                0.0
1122            };
1123            if self
1124                .store
1125                .append_event(EvolutionEvent::PromotionEvaluated {
1126                    gene_id: target.gene_id.clone(),
1127                    state: AssetState::Quarantined,
1128                    reason: reason.clone(),
1129                    reason_code: TransitionReasonCode::RevalidationConfidenceDecay,
1130                    evidence: Some(TransitionEvidence {
1131                        replay_attempts: None,
1132                        replay_successes: None,
1133                        replay_success_rate: None,
1134                        environment_match_factor: None,
1135                        decayed_confidence: Some(target.decayed_confidence),
1136                        confidence_decay_ratio: Some(confidence_decay_ratio),
1137                        summary: Some(format!(
1138                            "phase=confidence_revalidation; decayed_confidence={:.3}; confidence_decay_ratio={:.3}",
1139                            target.decayed_confidence, confidence_decay_ratio
1140                        )),
1141                    }),
1142                })
1143                .is_err()
1144            {
1145                continue;
1146            }
1147            for capsule_id in target.capsule_ids {
1148                if self
1149                    .store
1150                    .append_event(EvolutionEvent::CapsuleQuarantined { capsule_id })
1151                    .is_err()
1152                {
1153                    break;
1154                }
1155            }
1156        }
1157    }
1158
1159    fn build_replay_economics_evidence(
1160        &self,
1161        input: &SelectorInput,
1162        candidate: Option<&GeneCandidate>,
1163        source_sender_id: Option<&str>,
1164        success: bool,
1165        reason_code: ReplayRoiReasonCode,
1166        reason: &str,
1167    ) -> ReplayRoiEvidence {
1168        let (task_class_id, task_label) =
1169            replay_descriptor_from_candidate_or_input(candidate, input);
1170        let signal_source = candidate
1171            .map(|best| best.gene.signals.as_slice())
1172            .unwrap_or(input.signals.as_slice());
1173        let baseline_tokens = estimated_reasoning_tokens(signal_source);
1174        let reasoning_avoided_tokens = if success { baseline_tokens } else { 0 };
1175        let replay_fallback_cost = if success { 0 } else { baseline_tokens };
1176        let asset_origin =
1177            candidate.and_then(|best| strategy_metadata_value(&best.gene.strategy, "asset_origin"));
1178        let mut context_dimensions = vec![
1179            format!(
1180                "outcome={}",
1181                if success {
1182                    "replay_hit"
1183                } else {
1184                    "planner_fallback"
1185                }
1186            ),
1187            format!("reason={reason}"),
1188            format!("task_class_id={task_class_id}"),
1189            format!("task_label={task_label}"),
1190        ];
1191        if let Some(asset_origin) = asset_origin.as_deref() {
1192            context_dimensions.push(format!("asset_origin={asset_origin}"));
1193        }
1194        if let Some(source_sender_id) = source_sender_id {
1195            context_dimensions.push(format!("source_sender_id={source_sender_id}"));
1196        }
1197        ReplayRoiEvidence {
1198            success,
1199            reason_code,
1200            task_class_id,
1201            task_label,
1202            reasoning_avoided_tokens,
1203            replay_fallback_cost,
1204            replay_roi: compute_replay_roi(reasoning_avoided_tokens, replay_fallback_cost),
1205            asset_origin,
1206            source_sender_id: source_sender_id.map(ToOwned::to_owned),
1207            context_dimensions,
1208        }
1209    }
1210
1211    fn record_replay_economics(
1212        &self,
1213        replay_run_id: Option<&RunId>,
1214        candidate: Option<&GeneCandidate>,
1215        capsule_id: Option<&str>,
1216        evidence: ReplayRoiEvidence,
1217    ) -> Result<(), ReplayError> {
1218        self.store
1219            .append_event(EvolutionEvent::ReplayEconomicsRecorded {
1220                gene_id: candidate.map(|best| best.gene.id.clone()),
1221                capsule_id: capsule_id.map(ToOwned::to_owned),
1222                replay_run_id: replay_run_id.cloned(),
1223                evidence,
1224            })
1225            .map_err(|err| ReplayError::Store(err.to_string()))?;
1226        Ok(())
1227    }
1228
1229    async fn try_replay_inner(
1230        &self,
1231        replay_run_id: Option<&RunId>,
1232        input: &SelectorInput,
1233        policy: &SandboxPolicy,
1234        validation: &ValidationPlan,
1235    ) -> Result<ReplayDecision, ReplayError> {
1236        let ReplayCandidates {
1237            candidates,
1238            exact_match,
1239        } = self.collect_replay_candidates(input);
1240        let mut detect_evidence = replay_detect_evidence_from_input(input);
1241        let select_evidence = self.build_select_evidence(input, &candidates, exact_match);
1242        let Some(best) = candidates.into_iter().next() else {
1243            detect_evidence
1244                .mismatch_reasons
1245                .push("no_candidate_after_select".to_string());
1246            let economics_evidence = self.build_replay_economics_evidence(
1247                input,
1248                None,
1249                None,
1250                false,
1251                ReplayRoiReasonCode::ReplayMissNoMatchingGene,
1252                "no matching gene",
1253            );
1254            self.record_replay_economics(replay_run_id, None, None, economics_evidence.clone())?;
1255            return Ok(ReplayDecision {
1256                used_capsule: false,
1257                capsule_id: None,
1258                fallback_to_planner: true,
1259                reason: "no matching gene".into(),
1260                detect_evidence,
1261                select_evidence,
1262                economics_evidence,
1263            });
1264        };
1265        let (detected_task_class_id, detected_task_label) =
1266            replay_descriptor_from_candidate_or_input(Some(&best), input);
1267        detect_evidence.task_class_id = detected_task_class_id;
1268        detect_evidence.task_label = detected_task_label;
1269        detect_evidence.matched_signals =
1270            matched_replay_signals(&input.signals, &best.gene.signals);
1271        if !exact_match && best.score < 0.82 {
1272            detect_evidence
1273                .mismatch_reasons
1274                .push("score_below_threshold".to_string());
1275            let reason = format!("best gene score {:.3} below replay threshold", best.score);
1276            let economics_evidence = self.build_replay_economics_evidence(
1277                input,
1278                Some(&best),
1279                None,
1280                false,
1281                ReplayRoiReasonCode::ReplayMissScoreBelowThreshold,
1282                &reason,
1283            );
1284            self.record_replay_economics(
1285                replay_run_id,
1286                Some(&best),
1287                None,
1288                economics_evidence.clone(),
1289            )?;
1290            return Ok(ReplayDecision {
1291                used_capsule: false,
1292                capsule_id: None,
1293                fallback_to_planner: true,
1294                reason,
1295                detect_evidence,
1296                select_evidence,
1297                economics_evidence,
1298            });
1299        }
1300
1301        let Some(capsule) = best.capsules.first().cloned() else {
1302            detect_evidence
1303                .mismatch_reasons
1304                .push("candidate_has_no_capsule".to_string());
1305            let economics_evidence = self.build_replay_economics_evidence(
1306                input,
1307                Some(&best),
1308                None,
1309                false,
1310                ReplayRoiReasonCode::ReplayMissCandidateHasNoCapsule,
1311                "candidate gene has no capsule",
1312            );
1313            self.record_replay_economics(
1314                replay_run_id,
1315                Some(&best),
1316                None,
1317                economics_evidence.clone(),
1318            )?;
1319            return Ok(ReplayDecision {
1320                used_capsule: false,
1321                capsule_id: None,
1322                fallback_to_planner: true,
1323                reason: "candidate gene has no capsule".into(),
1324                detect_evidence,
1325                select_evidence,
1326                economics_evidence,
1327            });
1328        };
1329        let remote_publisher = self.publisher_for_capsule(&capsule.id);
1330
1331        let Some(mutation) = find_declared_mutation(self.store.as_ref(), &capsule.mutation_id)
1332            .map_err(|err| ReplayError::Store(err.to_string()))?
1333        else {
1334            detect_evidence
1335                .mismatch_reasons
1336                .push("mutation_payload_missing".to_string());
1337            let economics_evidence = self.build_replay_economics_evidence(
1338                input,
1339                Some(&best),
1340                remote_publisher.as_deref(),
1341                false,
1342                ReplayRoiReasonCode::ReplayMissMutationPayloadMissing,
1343                "mutation payload missing from store",
1344            );
1345            self.record_replay_economics(
1346                replay_run_id,
1347                Some(&best),
1348                Some(&capsule.id),
1349                economics_evidence.clone(),
1350            )?;
1351            return Ok(ReplayDecision {
1352                used_capsule: false,
1353                capsule_id: None,
1354                fallback_to_planner: true,
1355                reason: "mutation payload missing from store".into(),
1356                detect_evidence,
1357                select_evidence,
1358                economics_evidence,
1359            });
1360        };
1361
1362        let receipt = match self.sandbox.apply(&mutation, policy).await {
1363            Ok(receipt) => receipt,
1364            Err(err) => {
1365                self.record_reuse_settlement(remote_publisher.as_deref(), false);
1366                let reason = format!("replay patch apply failed: {err}");
1367                let economics_evidence = self.build_replay_economics_evidence(
1368                    input,
1369                    Some(&best),
1370                    remote_publisher.as_deref(),
1371                    false,
1372                    ReplayRoiReasonCode::ReplayMissPatchApplyFailed,
1373                    &reason,
1374                );
1375                self.record_replay_economics(
1376                    replay_run_id,
1377                    Some(&best),
1378                    Some(&capsule.id),
1379                    economics_evidence.clone(),
1380                )?;
1381                detect_evidence
1382                    .mismatch_reasons
1383                    .push("patch_apply_failed".to_string());
1384                return Ok(ReplayDecision {
1385                    used_capsule: false,
1386                    capsule_id: Some(capsule.id.clone()),
1387                    fallback_to_planner: true,
1388                    reason,
1389                    detect_evidence,
1390                    select_evidence,
1391                    economics_evidence,
1392                });
1393            }
1394        };
1395
1396        let report = self
1397            .validator
1398            .run(&receipt, validation)
1399            .await
1400            .map_err(|err| ReplayError::Validation(err.to_string()))?;
1401        if !report.success {
1402            self.record_replay_validation_failure(&best, &capsule, validation, &report)?;
1403            self.record_reuse_settlement(remote_publisher.as_deref(), false);
1404            let economics_evidence = self.build_replay_economics_evidence(
1405                input,
1406                Some(&best),
1407                remote_publisher.as_deref(),
1408                false,
1409                ReplayRoiReasonCode::ReplayMissValidationFailed,
1410                "replay validation failed",
1411            );
1412            self.record_replay_economics(
1413                replay_run_id,
1414                Some(&best),
1415                Some(&capsule.id),
1416                economics_evidence.clone(),
1417            )?;
1418            detect_evidence
1419                .mismatch_reasons
1420                .push("validation_failed".to_string());
1421            return Ok(ReplayDecision {
1422                used_capsule: false,
1423                capsule_id: Some(capsule.id.clone()),
1424                fallback_to_planner: true,
1425                reason: "replay validation failed".into(),
1426                detect_evidence,
1427                select_evidence,
1428                economics_evidence,
1429            });
1430        }
1431
1432        let requires_shadow_progression = remote_publisher.is_some()
1433            && matches!(
1434                capsule.state,
1435                AssetState::Quarantined | AssetState::ShadowValidated
1436            );
1437        if requires_shadow_progression {
1438            self.store
1439                .append_event(EvolutionEvent::ValidationPassed {
1440                    mutation_id: capsule.mutation_id.clone(),
1441                    report: report.to_snapshot(&validation.profile),
1442                    gene_id: Some(best.gene.id.clone()),
1443                })
1444                .map_err(|err| ReplayError::Store(err.to_string()))?;
1445            let evidence = self.shadow_transition_evidence(&best.gene.id, &capsule, &input.env)?;
1446            let (target_state, reason_code, reason, promote_now, phase) =
1447                if matches!(best.gene.state, AssetState::Quarantined) {
1448                    (
1449                        AssetState::ShadowValidated,
1450                        TransitionReasonCode::PromotionShadowValidationPassed,
1451                        "remote asset passed first local replay and entered shadow validation"
1452                            .into(),
1453                        false,
1454                        "quarantine_to_shadow",
1455                    )
1456                } else if shadow_promotion_gate_passed(&evidence) {
1457                    (
1458                        AssetState::Promoted,
1459                        TransitionReasonCode::PromotionRemoteReplayValidated,
1460                        "shadow validation thresholds satisfied; remote asset promoted".into(),
1461                        true,
1462                        "shadow_to_promoted",
1463                    )
1464                } else {
1465                    (
1466                        AssetState::ShadowValidated,
1467                        TransitionReasonCode::ShadowCollectingReplayEvidence,
1468                        "shadow validation collecting additional replay evidence".into(),
1469                        false,
1470                        "shadow_hold",
1471                    )
1472                };
1473            self.store
1474                .append_event(EvolutionEvent::PromotionEvaluated {
1475                    gene_id: best.gene.id.clone(),
1476                    state: target_state.clone(),
1477                    reason,
1478                    reason_code,
1479                    evidence: Some(evidence.to_transition_evidence(shadow_evidence_summary(
1480                        &evidence,
1481                        promote_now,
1482                        phase,
1483                    ))),
1484                })
1485                .map_err(|err| ReplayError::Store(err.to_string()))?;
1486            if promote_now {
1487                self.store
1488                    .append_event(EvolutionEvent::GenePromoted {
1489                        gene_id: best.gene.id.clone(),
1490                    })
1491                    .map_err(|err| ReplayError::Store(err.to_string()))?;
1492            }
1493            self.store
1494                .append_event(EvolutionEvent::CapsuleReleased {
1495                    capsule_id: capsule.id.clone(),
1496                    state: target_state,
1497                })
1498                .map_err(|err| ReplayError::Store(err.to_string()))?;
1499        }
1500
1501        self.store
1502            .append_event(EvolutionEvent::CapsuleReused {
1503                capsule_id: capsule.id.clone(),
1504                gene_id: capsule.gene_id.clone(),
1505                run_id: capsule.run_id.clone(),
1506                replay_run_id: replay_run_id.cloned(),
1507            })
1508            .map_err(|err| ReplayError::Store(err.to_string()))?;
1509        self.record_reuse_settlement(remote_publisher.as_deref(), true);
1510        let reason = if exact_match {
1511            "replayed via cold-start lookup".to_string()
1512        } else {
1513            "replayed via selector".to_string()
1514        };
1515        let economics_evidence = self.build_replay_economics_evidence(
1516            input,
1517            Some(&best),
1518            remote_publisher.as_deref(),
1519            true,
1520            ReplayRoiReasonCode::ReplayHit,
1521            &reason,
1522        );
1523        self.record_replay_economics(
1524            replay_run_id,
1525            Some(&best),
1526            Some(&capsule.id),
1527            economics_evidence.clone(),
1528        )?;
1529
1530        Ok(ReplayDecision {
1531            used_capsule: true,
1532            capsule_id: Some(capsule.id),
1533            fallback_to_planner: false,
1534            reason,
1535            detect_evidence,
1536            select_evidence,
1537            economics_evidence,
1538        })
1539    }
1540
1541    fn rerank_with_reputation_bias(&self, candidates: &mut [GeneCandidate]) {
1542        let Some(ledger) = self.economics.as_ref() else {
1543            return;
1544        };
1545        let reputation_bias = ledger
1546            .lock()
1547            .ok()
1548            .map(|locked| locked.selector_reputation_bias())
1549            .unwrap_or_default();
1550        if reputation_bias.is_empty() {
1551            return;
1552        }
1553        let required_assets = candidates
1554            .iter()
1555            .filter_map(|candidate| {
1556                candidate
1557                    .capsules
1558                    .first()
1559                    .map(|capsule| capsule.id.as_str())
1560            })
1561            .collect::<Vec<_>>();
1562        let publisher_map = self.remote_publishers_snapshot(&required_assets);
1563        if publisher_map.is_empty() {
1564            return;
1565        }
1566        candidates.sort_by(|left, right| {
1567            effective_candidate_score(right, &publisher_map, &reputation_bias)
1568                .partial_cmp(&effective_candidate_score(
1569                    left,
1570                    &publisher_map,
1571                    &reputation_bias,
1572                ))
1573                .unwrap_or(std::cmp::Ordering::Equal)
1574                .then_with(|| left.gene.id.cmp(&right.gene.id))
1575        });
1576    }
1577
1578    fn publisher_for_capsule(&self, capsule_id: &str) -> Option<String> {
1579        self.remote_publishers_snapshot(&[capsule_id])
1580            .get(capsule_id)
1581            .cloned()
1582    }
1583
1584    fn remote_publishers_snapshot(&self, required_assets: &[&str]) -> BTreeMap<String, String> {
1585        let cached = self
1586            .remote_publishers
1587            .as_ref()
1588            .and_then(|remote_publishers| {
1589                remote_publishers.lock().ok().map(|locked| locked.clone())
1590            })
1591            .unwrap_or_default();
1592        if !cached.is_empty()
1593            && required_assets
1594                .iter()
1595                .all(|asset_id| cached.contains_key(*asset_id))
1596        {
1597            return cached;
1598        }
1599
1600        let persisted = remote_publishers_by_asset_from_store(self.store.as_ref());
1601        if persisted.is_empty() {
1602            return cached;
1603        }
1604
1605        let mut merged = cached;
1606        for (asset_id, sender_id) in persisted {
1607            merged.entry(asset_id).or_insert(sender_id);
1608        }
1609
1610        if let Some(remote_publishers) = self.remote_publishers.as_ref() {
1611            if let Ok(mut locked) = remote_publishers.lock() {
1612                for (asset_id, sender_id) in &merged {
1613                    locked.entry(asset_id.clone()).or_insert(sender_id.clone());
1614                }
1615            }
1616        }
1617
1618        merged
1619    }
1620
1621    fn record_reuse_settlement(&self, publisher_id: Option<&str>, success: bool) {
1622        let Some(publisher_id) = publisher_id else {
1623            return;
1624        };
1625        let Some(ledger) = self.economics.as_ref() else {
1626            return;
1627        };
1628        if let Ok(mut locked) = ledger.lock() {
1629            locked.settle_remote_reuse(publisher_id, success, &self.stake_policy);
1630        }
1631    }
1632
1633    fn record_replay_validation_failure(
1634        &self,
1635        best: &GeneCandidate,
1636        capsule: &Capsule,
1637        validation: &ValidationPlan,
1638        report: &ValidationReport,
1639    ) -> Result<(), ReplayError> {
1640        let projection = projection_snapshot(self.store.as_ref())
1641            .map_err(|err| ReplayError::Store(err.to_string()))?;
1642        let confidence_context = Self::confidence_context(&projection, &best.gene.id);
1643
1644        self.store
1645            .append_event(EvolutionEvent::ValidationFailed {
1646                mutation_id: capsule.mutation_id.clone(),
1647                report: report.to_snapshot(&validation.profile),
1648                gene_id: Some(best.gene.id.clone()),
1649            })
1650            .map_err(|err| ReplayError::Store(err.to_string()))?;
1651
1652        let replay_failures = self.replay_failure_count(&best.gene.id)?;
1653        let governor_decision = self.governor.evaluate(GovernorInput {
1654            candidate_source: if self.publisher_for_capsule(&capsule.id).is_some() {
1655                CandidateSource::Remote
1656            } else {
1657                CandidateSource::Local
1658            },
1659            success_count: 0,
1660            blast_radius: BlastRadius {
1661                files_changed: capsule.outcome.changed_files.len(),
1662                lines_changed: capsule.outcome.lines_changed,
1663            },
1664            replay_failures,
1665            recent_mutation_ages_secs: Vec::new(),
1666            current_confidence: confidence_context.current_confidence,
1667            historical_peak_confidence: confidence_context.historical_peak_confidence,
1668            confidence_last_updated_secs: confidence_context.confidence_last_updated_secs,
1669        });
1670
1671        if matches!(governor_decision.target_state, AssetState::Revoked) {
1672            self.store
1673                .append_event(EvolutionEvent::PromotionEvaluated {
1674                    gene_id: best.gene.id.clone(),
1675                    state: AssetState::Revoked,
1676                    reason: governor_decision.reason.clone(),
1677                    reason_code: governor_decision.reason_code.clone(),
1678                    evidence: Some(confidence_context.to_transition_evidence(
1679                        "replay_failure_revocation",
1680                        Some(replay_failures),
1681                        None,
1682                        None,
1683                        None,
1684                        Some(format!("replay_failures={replay_failures}")),
1685                    )),
1686                })
1687                .map_err(|err| ReplayError::Store(err.to_string()))?;
1688            self.store
1689                .append_event(EvolutionEvent::GeneRevoked {
1690                    gene_id: best.gene.id.clone(),
1691                    reason: governor_decision.reason,
1692                })
1693                .map_err(|err| ReplayError::Store(err.to_string()))?;
1694            for related in &best.capsules {
1695                self.store
1696                    .append_event(EvolutionEvent::CapsuleQuarantined {
1697                        capsule_id: related.id.clone(),
1698                    })
1699                    .map_err(|err| ReplayError::Store(err.to_string()))?;
1700            }
1701        }
1702
1703        Ok(())
1704    }
1705
1706    fn confidence_context(
1707        projection: &EvolutionProjection,
1708        gene_id: &str,
1709    ) -> ConfidenceTransitionContext {
1710        let peak_confidence = projection
1711            .capsules
1712            .iter()
1713            .filter(|capsule| capsule.gene_id == gene_id)
1714            .map(|capsule| capsule.confidence)
1715            .fold(0.0_f32, f32::max);
1716        let age_secs = projection
1717            .last_updated_at
1718            .get(gene_id)
1719            .and_then(|timestamp| Self::seconds_since_timestamp(timestamp, Utc::now()));
1720        ConfidenceTransitionContext {
1721            current_confidence: peak_confidence,
1722            historical_peak_confidence: peak_confidence,
1723            confidence_last_updated_secs: age_secs,
1724        }
1725    }
1726
1727    fn seconds_since_timestamp(timestamp: &str, now: DateTime<Utc>) -> Option<u64> {
1728        let parsed = DateTime::parse_from_rfc3339(timestamp)
1729            .ok()?
1730            .with_timezone(&Utc);
1731        let elapsed = now.signed_duration_since(parsed);
1732        if elapsed < Duration::zero() {
1733            Some(0)
1734        } else {
1735            u64::try_from(elapsed.num_seconds()).ok()
1736        }
1737    }
1738
1739    fn replay_failure_count(&self, gene_id: &str) -> Result<u64, ReplayError> {
1740        Ok(self
1741            .store
1742            .scan(1)
1743            .map_err(|err| ReplayError::Store(err.to_string()))?
1744            .into_iter()
1745            .filter(|stored| {
1746                matches!(
1747                    &stored.event,
1748                    EvolutionEvent::ValidationFailed {
1749                        gene_id: Some(current_gene_id),
1750                        ..
1751                    } if current_gene_id == gene_id
1752                )
1753            })
1754            .count() as u64)
1755    }
1756
1757    fn shadow_transition_evidence(
1758        &self,
1759        gene_id: &str,
1760        capsule: &Capsule,
1761        input_env: &EnvFingerprint,
1762    ) -> Result<ShadowTransitionEvidence, ReplayError> {
1763        let events = self
1764            .store
1765            .scan(1)
1766            .map_err(|err| ReplayError::Store(err.to_string()))?;
1767        let (replay_attempts, replay_successes) = events.iter().fold(
1768            (0_u64, 0_u64),
1769            |(attempts, successes), stored| match &stored.event {
1770                EvolutionEvent::ValidationPassed {
1771                    gene_id: Some(current_gene_id),
1772                    ..
1773                } if current_gene_id == gene_id => (attempts + 1, successes + 1),
1774                EvolutionEvent::ValidationFailed {
1775                    gene_id: Some(current_gene_id),
1776                    ..
1777                } if current_gene_id == gene_id => (attempts + 1, successes),
1778                _ => (attempts, successes),
1779            },
1780        );
1781        let replay_success_rate = safe_ratio(replay_successes, replay_attempts) as f32;
1782        let environment_match_factor = replay_environment_match_factor(input_env, &capsule.env);
1783        let projection = projection_snapshot(self.store.as_ref())
1784            .map_err(|err| ReplayError::Store(err.to_string()))?;
1785        let age_secs = projection
1786            .last_updated_at
1787            .get(gene_id)
1788            .and_then(|timestamp| Self::seconds_since_timestamp(timestamp, Utc::now()));
1789        let decayed_confidence = decayed_replay_confidence(capsule.confidence, age_secs);
1790        let confidence_decay_ratio = if capsule.confidence > 0.0 {
1791            (decayed_confidence / capsule.confidence).clamp(0.0, 1.0)
1792        } else {
1793            0.0
1794        };
1795
1796        Ok(ShadowTransitionEvidence {
1797            replay_attempts,
1798            replay_successes,
1799            replay_success_rate,
1800            environment_match_factor,
1801            decayed_confidence,
1802            confidence_decay_ratio,
1803        })
1804    }
1805}
1806
1807#[derive(Clone, Debug)]
1808struct ShadowTransitionEvidence {
1809    replay_attempts: u64,
1810    replay_successes: u64,
1811    replay_success_rate: f32,
1812    environment_match_factor: f32,
1813    decayed_confidence: f32,
1814    confidence_decay_ratio: f32,
1815}
1816
1817impl ShadowTransitionEvidence {
1818    fn to_transition_evidence(&self, summary: String) -> TransitionEvidence {
1819        TransitionEvidence {
1820            replay_attempts: Some(self.replay_attempts),
1821            replay_successes: Some(self.replay_successes),
1822            replay_success_rate: Some(self.replay_success_rate),
1823            environment_match_factor: Some(self.environment_match_factor),
1824            decayed_confidence: Some(self.decayed_confidence),
1825            confidence_decay_ratio: Some(self.confidence_decay_ratio),
1826            summary: Some(summary),
1827        }
1828    }
1829}
1830
1831#[derive(Clone, Copy, Debug, Default)]
1832struct ConfidenceTransitionContext {
1833    current_confidence: f32,
1834    historical_peak_confidence: f32,
1835    confidence_last_updated_secs: Option<u64>,
1836}
1837
1838impl ConfidenceTransitionContext {
1839    fn decayed_confidence(self) -> f32 {
1840        decayed_replay_confidence(self.current_confidence, self.confidence_last_updated_secs)
1841    }
1842
1843    fn confidence_decay_ratio(self) -> Option<f32> {
1844        if self.historical_peak_confidence > 0.0 {
1845            Some((self.decayed_confidence() / self.historical_peak_confidence).clamp(0.0, 1.0))
1846        } else {
1847            None
1848        }
1849    }
1850
1851    fn to_transition_evidence(
1852        self,
1853        phase: &str,
1854        replay_attempts: Option<u64>,
1855        replay_successes: Option<u64>,
1856        replay_success_rate: Option<f32>,
1857        environment_match_factor: Option<f32>,
1858        extra_summary: Option<String>,
1859    ) -> TransitionEvidence {
1860        let decayed_confidence = self.decayed_confidence();
1861        let confidence_decay_ratio = self.confidence_decay_ratio();
1862        let age_secs = self
1863            .confidence_last_updated_secs
1864            .map(|age| age.to_string())
1865            .unwrap_or_else(|| "unknown".into());
1866        let mut summary = format!(
1867            "phase={phase}; current_confidence={:.3}; decayed_confidence={:.3}; historical_peak_confidence={:.3}; confidence_last_updated_secs={age_secs}",
1868            self.current_confidence, decayed_confidence, self.historical_peak_confidence
1869        );
1870        if let Some(ratio) = confidence_decay_ratio {
1871            summary.push_str(&format!("; confidence_decay_ratio={ratio:.3}"));
1872        }
1873        if let Some(extra_summary) = extra_summary {
1874            summary.push_str("; ");
1875            summary.push_str(&extra_summary);
1876        }
1877
1878        TransitionEvidence {
1879            replay_attempts,
1880            replay_successes,
1881            replay_success_rate,
1882            environment_match_factor,
1883            decayed_confidence: Some(decayed_confidence),
1884            confidence_decay_ratio,
1885            summary: Some(summary),
1886        }
1887    }
1888}
1889
1890fn shadow_promotion_gate_passed(evidence: &ShadowTransitionEvidence) -> bool {
1891    evidence.replay_attempts >= SHADOW_PROMOTION_MIN_REPLAY_ATTEMPTS
1892        && evidence.replay_success_rate >= SHADOW_PROMOTION_MIN_SUCCESS_RATE
1893        && evidence.environment_match_factor >= SHADOW_PROMOTION_MIN_ENV_MATCH
1894        && evidence.decayed_confidence >= SHADOW_PROMOTION_MIN_DECAYED_CONFIDENCE
1895}
1896
1897fn shadow_evidence_summary(
1898    evidence: &ShadowTransitionEvidence,
1899    promoted: bool,
1900    phase: &str,
1901) -> String {
1902    format!(
1903        "phase={phase}; replay_attempts={}; replay_successes={}; replay_success_rate={:.3}; environment_match_factor={:.3}; decayed_confidence={:.3}; confidence_decay_ratio={:.3}; promote={promoted}",
1904        evidence.replay_attempts,
1905        evidence.replay_successes,
1906        evidence.replay_success_rate,
1907        evidence.environment_match_factor,
1908        evidence.decayed_confidence,
1909        evidence.confidence_decay_ratio,
1910    )
1911}
1912
1913fn confidence_transition_evidence_for_governor(
1914    confidence_context: ConfidenceTransitionContext,
1915    governor_decision: &GovernorDecision,
1916    success_count: u64,
1917) -> Option<TransitionEvidence> {
1918    match governor_decision.reason_code {
1919        TransitionReasonCode::DowngradeConfidenceRegression => {
1920            Some(confidence_context.to_transition_evidence(
1921                "confidence_regression",
1922                None,
1923                Some(success_count),
1924                None,
1925                None,
1926                Some(format!("target_state={:?}", governor_decision.target_state)),
1927            ))
1928        }
1929        _ => None,
1930    }
1931}
1932
1933#[derive(Clone, Debug, PartialEq)]
1934struct ConfidenceRevalidationTarget {
1935    gene_id: String,
1936    capsule_ids: Vec<String>,
1937    peak_confidence: f32,
1938    decayed_confidence: f32,
1939}
1940
1941fn stale_replay_revalidation_targets(
1942    projection: &EvolutionProjection,
1943    now: DateTime<Utc>,
1944) -> Vec<ConfidenceRevalidationTarget> {
1945    projection
1946        .genes
1947        .iter()
1948        .filter(|gene| gene.state == AssetState::Promoted)
1949        .filter_map(|gene| {
1950            let promoted_capsules = projection
1951                .capsules
1952                .iter()
1953                .filter(|capsule| {
1954                    capsule.gene_id == gene.id && capsule.state == AssetState::Promoted
1955                })
1956                .collect::<Vec<_>>();
1957            if promoted_capsules.is_empty() {
1958                return None;
1959            }
1960            let age_secs = projection
1961                .last_updated_at
1962                .get(&gene.id)
1963                .and_then(|timestamp| seconds_since_timestamp_for_confidence(timestamp, now));
1964            let decayed_confidence = promoted_capsules
1965                .iter()
1966                .map(|capsule| decayed_replay_confidence(capsule.confidence, age_secs))
1967                .fold(0.0_f32, f32::max);
1968            if decayed_confidence >= MIN_REPLAY_CONFIDENCE {
1969                return None;
1970            }
1971            let peak_confidence = promoted_capsules
1972                .iter()
1973                .map(|capsule| capsule.confidence)
1974                .fold(0.0_f32, f32::max);
1975            Some(ConfidenceRevalidationTarget {
1976                gene_id: gene.id.clone(),
1977                capsule_ids: promoted_capsules
1978                    .into_iter()
1979                    .map(|capsule| capsule.id.clone())
1980                    .collect(),
1981                peak_confidence,
1982                decayed_confidence,
1983            })
1984        })
1985        .collect()
1986}
1987
1988fn seconds_since_timestamp_for_confidence(timestamp: &str, now: DateTime<Utc>) -> Option<u64> {
1989    let parsed = DateTime::parse_from_rfc3339(timestamp)
1990        .ok()?
1991        .with_timezone(&Utc);
1992    let elapsed = now.signed_duration_since(parsed);
1993    if elapsed < Duration::zero() {
1994        Some(0)
1995    } else {
1996        u64::try_from(elapsed.num_seconds()).ok()
1997    }
1998}
1999
2000#[derive(Debug, Error)]
2001pub enum EvoKernelError {
2002    #[error("sandbox error: {0}")]
2003    Sandbox(String),
2004    #[error("validation error: {0}")]
2005    Validation(String),
2006    #[error("validation failed")]
2007    ValidationFailed(ValidationReport),
2008    #[error("store error: {0}")]
2009    Store(String),
2010}
2011
2012#[derive(Clone, Debug)]
2013pub struct CaptureOutcome {
2014    pub capsule: Capsule,
2015    pub gene: Gene,
2016    pub governor_decision: GovernorDecision,
2017}
2018
2019#[derive(Clone, Debug, Serialize, Deserialize)]
2020pub struct ImportOutcome {
2021    pub imported_asset_ids: Vec<String>,
2022    pub accepted: bool,
2023    #[serde(default, skip_serializing_if = "Option::is_none")]
2024    pub next_cursor: Option<String>,
2025    #[serde(default, skip_serializing_if = "Option::is_none")]
2026    pub resume_token: Option<String>,
2027    #[serde(default)]
2028    pub sync_audit: SyncAudit,
2029}
2030
2031#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
2032pub struct EvolutionMetricsSnapshot {
2033    pub replay_attempts_total: u64,
2034    pub replay_success_total: u64,
2035    pub replay_success_rate: f64,
2036    pub confidence_revalidations_total: u64,
2037    pub replay_reasoning_avoided_total: u64,
2038    pub reasoning_avoided_tokens_total: u64,
2039    pub replay_fallback_cost_total: u64,
2040    pub replay_roi: f64,
2041    pub replay_task_classes: Vec<ReplayTaskClassMetrics>,
2042    pub replay_sources: Vec<ReplaySourceRoiMetrics>,
2043    pub mutation_declared_total: u64,
2044    pub promoted_mutations_total: u64,
2045    pub promotion_ratio: f64,
2046    pub gene_revocations_total: u64,
2047    pub mutation_velocity_last_hour: u64,
2048    pub revoke_frequency_last_hour: u64,
2049    pub promoted_genes: u64,
2050    pub promoted_capsules: u64,
2051    pub last_event_seq: u64,
2052}
2053
2054#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2055pub struct EvolutionHealthSnapshot {
2056    pub status: String,
2057    pub last_event_seq: u64,
2058    pub promoted_genes: u64,
2059    pub promoted_capsules: u64,
2060}
2061
2062#[derive(Clone)]
2063pub struct EvolutionNetworkNode {
2064    pub store: Arc<dyn EvolutionStore>,
2065}
2066
2067impl EvolutionNetworkNode {
2068    pub fn new(store: Arc<dyn EvolutionStore>) -> Self {
2069        Self { store }
2070    }
2071
2072    pub fn with_default_store() -> Self {
2073        Self {
2074            store: Arc::new(JsonlEvolutionStore::new(default_store_root())),
2075        }
2076    }
2077
2078    pub fn accept_publish_request(
2079        &self,
2080        request: &PublishRequest,
2081    ) -> Result<ImportOutcome, EvoKernelError> {
2082        let requested_cursor = resolve_requested_cursor(
2083            &request.sender_id,
2084            request.since_cursor.as_deref(),
2085            request.resume_token.as_deref(),
2086        )?;
2087        import_remote_envelope_into_store(
2088            self.store.as_ref(),
2089            &EvolutionEnvelope::publish(request.sender_id.clone(), request.assets.clone()),
2090            None,
2091            requested_cursor,
2092        )
2093    }
2094
2095    pub fn ensure_builtin_experience_assets(
2096        &self,
2097        sender_id: impl Into<String>,
2098    ) -> Result<ImportOutcome, EvoKernelError> {
2099        ensure_builtin_experience_assets_in_store(self.store.as_ref(), sender_id.into())
2100    }
2101
2102    pub fn record_reported_experience(
2103        &self,
2104        sender_id: impl Into<String>,
2105        gene_id: impl Into<String>,
2106        signals: Vec<String>,
2107        strategy: Vec<String>,
2108        validation: Vec<String>,
2109    ) -> Result<ImportOutcome, EvoKernelError> {
2110        record_reported_experience_in_store(
2111            self.store.as_ref(),
2112            sender_id.into(),
2113            gene_id.into(),
2114            signals,
2115            strategy,
2116            validation,
2117        )
2118    }
2119
2120    pub fn publish_local_assets(
2121        &self,
2122        sender_id: impl Into<String>,
2123    ) -> Result<EvolutionEnvelope, EvoKernelError> {
2124        export_promoted_assets_from_store(self.store.as_ref(), sender_id)
2125    }
2126
2127    pub fn fetch_assets(
2128        &self,
2129        responder_id: impl Into<String>,
2130        query: &FetchQuery,
2131    ) -> Result<FetchResponse, EvoKernelError> {
2132        fetch_assets_from_store(self.store.as_ref(), responder_id, query)
2133    }
2134
2135    pub fn revoke_assets(&self, notice: &RevokeNotice) -> Result<RevokeNotice, EvoKernelError> {
2136        revoke_assets_in_store(self.store.as_ref(), notice)
2137    }
2138
2139    pub fn metrics_snapshot(&self) -> Result<EvolutionMetricsSnapshot, EvoKernelError> {
2140        evolution_metrics_snapshot(self.store.as_ref())
2141    }
2142
2143    pub fn replay_roi_release_gate_summary(
2144        &self,
2145        window_seconds: u64,
2146    ) -> Result<ReplayRoiWindowSummary, EvoKernelError> {
2147        replay_roi_release_gate_summary(self.store.as_ref(), window_seconds)
2148    }
2149
2150    pub fn render_replay_roi_release_gate_summary_json(
2151        &self,
2152        window_seconds: u64,
2153    ) -> Result<String, EvoKernelError> {
2154        let summary = self.replay_roi_release_gate_summary(window_seconds)?;
2155        serde_json::to_string_pretty(&summary)
2156            .map_err(|err| EvoKernelError::Validation(err.to_string()))
2157    }
2158
2159    pub fn replay_roi_release_gate_contract(
2160        &self,
2161        window_seconds: u64,
2162        thresholds: ReplayRoiReleaseGateThresholds,
2163    ) -> Result<ReplayRoiReleaseGateContract, EvoKernelError> {
2164        let summary = self.replay_roi_release_gate_summary(window_seconds)?;
2165        Ok(replay_roi_release_gate_contract(&summary, thresholds))
2166    }
2167
2168    pub fn render_replay_roi_release_gate_contract_json(
2169        &self,
2170        window_seconds: u64,
2171        thresholds: ReplayRoiReleaseGateThresholds,
2172    ) -> Result<String, EvoKernelError> {
2173        let contract = self.replay_roi_release_gate_contract(window_seconds, thresholds)?;
2174        serde_json::to_string_pretty(&contract)
2175            .map_err(|err| EvoKernelError::Validation(err.to_string()))
2176    }
2177
2178    pub fn render_metrics_prometheus(&self) -> Result<String, EvoKernelError> {
2179        self.metrics_snapshot().map(|snapshot| {
2180            let health = evolution_health_snapshot(&snapshot);
2181            render_evolution_metrics_prometheus(&snapshot, &health)
2182        })
2183    }
2184
2185    pub fn health_snapshot(&self) -> Result<EvolutionHealthSnapshot, EvoKernelError> {
2186        self.metrics_snapshot()
2187            .map(|snapshot| evolution_health_snapshot(&snapshot))
2188    }
2189}
2190
2191pub struct EvoKernel<S: KernelState> {
2192    pub kernel: Arc<Kernel<S>>,
2193    pub sandbox: Arc<dyn Sandbox>,
2194    pub validator: Arc<dyn Validator>,
2195    pub store: Arc<dyn EvolutionStore>,
2196    pub selector: Arc<dyn Selector>,
2197    pub governor: Arc<dyn Governor>,
2198    pub economics: Arc<Mutex<EvuLedger>>,
2199    pub remote_publishers: Arc<Mutex<BTreeMap<String, String>>>,
2200    pub stake_policy: StakePolicy,
2201    pub sandbox_policy: SandboxPolicy,
2202    pub validation_plan: ValidationPlan,
2203}
2204
2205impl<S: KernelState> EvoKernel<S> {
2206    fn recent_prior_mutation_ages_secs(
2207        &self,
2208        exclude_mutation_id: Option<&str>,
2209    ) -> Result<Vec<u64>, EvolutionError> {
2210        let now = Utc::now();
2211        let mut ages = self
2212            .store
2213            .scan(1)?
2214            .into_iter()
2215            .filter_map(|stored| match stored.event {
2216                EvolutionEvent::MutationDeclared { mutation }
2217                    if exclude_mutation_id != Some(mutation.intent.id.as_str()) =>
2218                {
2219                    Self::seconds_since_timestamp(&stored.timestamp, now)
2220                }
2221                _ => None,
2222            })
2223            .collect::<Vec<_>>();
2224        ages.sort_unstable();
2225        Ok(ages)
2226    }
2227
2228    fn seconds_since_timestamp(timestamp: &str, now: DateTime<Utc>) -> Option<u64> {
2229        let parsed = DateTime::parse_from_rfc3339(timestamp)
2230            .ok()?
2231            .with_timezone(&Utc);
2232        let elapsed = now.signed_duration_since(parsed);
2233        if elapsed < Duration::zero() {
2234            Some(0)
2235        } else {
2236            u64::try_from(elapsed.num_seconds()).ok()
2237        }
2238    }
2239
2240    pub fn new(
2241        kernel: Arc<Kernel<S>>,
2242        sandbox: Arc<dyn Sandbox>,
2243        validator: Arc<dyn Validator>,
2244        store: Arc<dyn EvolutionStore>,
2245    ) -> Self {
2246        let selector: Arc<dyn Selector> = Arc::new(StoreBackedSelector::new(store.clone()));
2247        Self {
2248            kernel,
2249            sandbox,
2250            validator,
2251            store,
2252            selector,
2253            governor: Arc::new(DefaultGovernor::default()),
2254            economics: Arc::new(Mutex::new(EvuLedger::default())),
2255            remote_publishers: Arc::new(Mutex::new(BTreeMap::new())),
2256            stake_policy: StakePolicy::default(),
2257            sandbox_policy: SandboxPolicy::oris_default(),
2258            validation_plan: ValidationPlan::oris_default(),
2259        }
2260    }
2261
2262    pub fn with_selector(mut self, selector: Arc<dyn Selector>) -> Self {
2263        self.selector = selector;
2264        self
2265    }
2266
2267    pub fn with_sandbox_policy(mut self, policy: SandboxPolicy) -> Self {
2268        self.sandbox_policy = policy;
2269        self
2270    }
2271
2272    pub fn with_governor(mut self, governor: Arc<dyn Governor>) -> Self {
2273        self.governor = governor;
2274        self
2275    }
2276
2277    pub fn with_economics(mut self, economics: Arc<Mutex<EvuLedger>>) -> Self {
2278        self.economics = economics;
2279        self
2280    }
2281
2282    pub fn with_stake_policy(mut self, policy: StakePolicy) -> Self {
2283        self.stake_policy = policy;
2284        self
2285    }
2286
2287    pub fn with_validation_plan(mut self, plan: ValidationPlan) -> Self {
2288        self.validation_plan = plan;
2289        self
2290    }
2291
2292    pub fn select_candidates(&self, input: &SelectorInput) -> Vec<GeneCandidate> {
2293        let executor = StoreReplayExecutor {
2294            sandbox: self.sandbox.clone(),
2295            validator: self.validator.clone(),
2296            store: self.store.clone(),
2297            selector: self.selector.clone(),
2298            governor: self.governor.clone(),
2299            economics: Some(self.economics.clone()),
2300            remote_publishers: Some(self.remote_publishers.clone()),
2301            stake_policy: self.stake_policy.clone(),
2302        };
2303        executor.collect_replay_candidates(input).candidates
2304    }
2305
2306    pub fn bootstrap_if_empty(&self, run_id: &RunId) -> Result<BootstrapReport, EvoKernelError> {
2307        let projection = projection_snapshot(self.store.as_ref())?;
2308        if !projection.genes.is_empty() {
2309            return Ok(BootstrapReport::default());
2310        }
2311
2312        let templates = built_in_seed_templates();
2313        for template in &templates {
2314            let mutation = build_seed_mutation(template);
2315            let extracted = extract_seed_signals(template);
2316            let gene = build_bootstrap_gene(template, &extracted)
2317                .map_err(|err| EvoKernelError::Validation(err.to_string()))?;
2318            let capsule = build_bootstrap_capsule(run_id, template, &mutation, &gene)
2319                .map_err(|err| EvoKernelError::Validation(err.to_string()))?;
2320
2321            self.store
2322                .append_event(EvolutionEvent::MutationDeclared {
2323                    mutation: mutation.clone(),
2324                })
2325                .map_err(store_err)?;
2326            self.store
2327                .append_event(EvolutionEvent::SignalsExtracted {
2328                    mutation_id: mutation.intent.id.clone(),
2329                    hash: extracted.hash.clone(),
2330                    signals: extracted.values.clone(),
2331                })
2332                .map_err(store_err)?;
2333            self.store
2334                .append_event(EvolutionEvent::GeneProjected { gene: gene.clone() })
2335                .map_err(store_err)?;
2336            self.store
2337                .append_event(EvolutionEvent::PromotionEvaluated {
2338                    gene_id: gene.id.clone(),
2339                    state: AssetState::Quarantined,
2340                    reason: "bootstrap seeds require local validation before replay".into(),
2341                    reason_code: TransitionReasonCode::DowngradeBootstrapRequiresLocalValidation,
2342                    evidence: None,
2343                })
2344                .map_err(store_err)?;
2345            self.store
2346                .append_event(EvolutionEvent::CapsuleCommitted {
2347                    capsule: capsule.clone(),
2348                })
2349                .map_err(store_err)?;
2350            self.store
2351                .append_event(EvolutionEvent::CapsuleQuarantined {
2352                    capsule_id: capsule.id,
2353                })
2354                .map_err(store_err)?;
2355        }
2356
2357        Ok(BootstrapReport {
2358            seeded: true,
2359            genes_added: templates.len(),
2360            capsules_added: templates.len(),
2361        })
2362    }
2363
2364    pub async fn capture_successful_mutation(
2365        &self,
2366        run_id: &RunId,
2367        mutation: PreparedMutation,
2368    ) -> Result<Capsule, EvoKernelError> {
2369        Ok(self
2370            .capture_mutation_with_governor(run_id, mutation)
2371            .await?
2372            .capsule)
2373    }
2374
2375    pub async fn capture_mutation_with_governor(
2376        &self,
2377        run_id: &RunId,
2378        mutation: PreparedMutation,
2379    ) -> Result<CaptureOutcome, EvoKernelError> {
2380        self.store
2381            .append_event(EvolutionEvent::MutationDeclared {
2382                mutation: mutation.clone(),
2383            })
2384            .map_err(store_err)?;
2385
2386        let receipt = match self.sandbox.apply(&mutation, &self.sandbox_policy).await {
2387            Ok(receipt) => receipt,
2388            Err(err) => {
2389                let message = err.to_string();
2390                let contract = mutation_needed_contract_for_error_message(&message);
2391                self.store
2392                    .append_event(EvolutionEvent::MutationRejected {
2393                        mutation_id: mutation.intent.id.clone(),
2394                        reason: contract.failure_reason,
2395                        reason_code: Some(
2396                            mutation_needed_reason_code_key(contract.reason_code).to_string(),
2397                        ),
2398                        recovery_hint: Some(contract.recovery_hint),
2399                        fail_closed: contract.fail_closed,
2400                    })
2401                    .map_err(store_err)?;
2402                return Err(EvoKernelError::Sandbox(message));
2403            }
2404        };
2405
2406        self.store
2407            .append_event(EvolutionEvent::MutationApplied {
2408                mutation_id: mutation.intent.id.clone(),
2409                patch_hash: receipt.patch_hash.clone(),
2410                changed_files: receipt
2411                    .changed_files
2412                    .iter()
2413                    .map(|path| path.to_string_lossy().to_string())
2414                    .collect(),
2415            })
2416            .map_err(store_err)?;
2417
2418        let report = match self.validator.run(&receipt, &self.validation_plan).await {
2419            Ok(report) => report,
2420            Err(err) => {
2421                let message = format!("mutation-needed validation execution error: {err}");
2422                let contract = mutation_needed_contract_for_error_message(&message);
2423                self.store
2424                    .append_event(EvolutionEvent::MutationRejected {
2425                        mutation_id: mutation.intent.id.clone(),
2426                        reason: contract.failure_reason,
2427                        reason_code: Some(
2428                            mutation_needed_reason_code_key(contract.reason_code).to_string(),
2429                        ),
2430                        recovery_hint: Some(contract.recovery_hint),
2431                        fail_closed: contract.fail_closed,
2432                    })
2433                    .map_err(store_err)?;
2434                return Err(EvoKernelError::Validation(message));
2435            }
2436        };
2437        if !report.success {
2438            self.store
2439                .append_event(EvolutionEvent::ValidationFailed {
2440                    mutation_id: mutation.intent.id.clone(),
2441                    report: report.to_snapshot(&self.validation_plan.profile),
2442                    gene_id: None,
2443                })
2444                .map_err(store_err)?;
2445            let contract = mutation_needed_contract_for_validation_failure(
2446                &self.validation_plan.profile,
2447                &report,
2448            );
2449            self.store
2450                .append_event(EvolutionEvent::MutationRejected {
2451                    mutation_id: mutation.intent.id.clone(),
2452                    reason: contract.failure_reason,
2453                    reason_code: Some(
2454                        mutation_needed_reason_code_key(contract.reason_code).to_string(),
2455                    ),
2456                    recovery_hint: Some(contract.recovery_hint),
2457                    fail_closed: contract.fail_closed,
2458                })
2459                .map_err(store_err)?;
2460            return Err(EvoKernelError::ValidationFailed(report));
2461        }
2462
2463        self.store
2464            .append_event(EvolutionEvent::ValidationPassed {
2465                mutation_id: mutation.intent.id.clone(),
2466                report: report.to_snapshot(&self.validation_plan.profile),
2467                gene_id: None,
2468            })
2469            .map_err(store_err)?;
2470
2471        let extracted_signals = extract_deterministic_signals(&SignalExtractionInput {
2472            patch_diff: mutation.artifact.payload.clone(),
2473            intent: mutation.intent.intent.clone(),
2474            expected_effect: mutation.intent.expected_effect.clone(),
2475            declared_signals: mutation.intent.signals.clone(),
2476            changed_files: receipt
2477                .changed_files
2478                .iter()
2479                .map(|path| path.to_string_lossy().to_string())
2480                .collect(),
2481            validation_success: report.success,
2482            validation_logs: report.logs.clone(),
2483            stage_outputs: report
2484                .stages
2485                .iter()
2486                .flat_map(|stage| [stage.stdout.clone(), stage.stderr.clone()])
2487                .filter(|value| !value.is_empty())
2488                .collect(),
2489        });
2490        self.store
2491            .append_event(EvolutionEvent::SignalsExtracted {
2492                mutation_id: mutation.intent.id.clone(),
2493                hash: extracted_signals.hash.clone(),
2494                signals: extracted_signals.values.clone(),
2495            })
2496            .map_err(store_err)?;
2497
2498        let projection = projection_snapshot(self.store.as_ref())?;
2499        let blast_radius = compute_blast_radius(&mutation.artifact.payload);
2500        let recent_mutation_ages_secs = self
2501            .recent_prior_mutation_ages_secs(Some(mutation.intent.id.as_str()))
2502            .map_err(store_err)?;
2503        let mut gene = derive_gene(
2504            &mutation,
2505            &receipt,
2506            &self.validation_plan.profile,
2507            &extracted_signals.values,
2508        );
2509        let confidence_context = StoreReplayExecutor::confidence_context(&projection, &gene.id);
2510        let success_count = projection
2511            .genes
2512            .iter()
2513            .find(|existing| existing.id == gene.id)
2514            .map(|existing| {
2515                projection
2516                    .capsules
2517                    .iter()
2518                    .filter(|capsule| capsule.gene_id == existing.id)
2519                    .count() as u64
2520            })
2521            .unwrap_or(0)
2522            + 1;
2523        let governor_decision = self.governor.evaluate(GovernorInput {
2524            candidate_source: CandidateSource::Local,
2525            success_count,
2526            blast_radius: blast_radius.clone(),
2527            replay_failures: 0,
2528            recent_mutation_ages_secs,
2529            current_confidence: confidence_context.current_confidence,
2530            historical_peak_confidence: confidence_context.historical_peak_confidence,
2531            confidence_last_updated_secs: confidence_context.confidence_last_updated_secs,
2532        });
2533
2534        gene.state = governor_decision.target_state.clone();
2535        self.store
2536            .append_event(EvolutionEvent::GeneProjected { gene: gene.clone() })
2537            .map_err(store_err)?;
2538        self.store
2539            .append_event(EvolutionEvent::PromotionEvaluated {
2540                gene_id: gene.id.clone(),
2541                state: governor_decision.target_state.clone(),
2542                reason: governor_decision.reason.clone(),
2543                reason_code: governor_decision.reason_code.clone(),
2544                evidence: confidence_transition_evidence_for_governor(
2545                    confidence_context,
2546                    &governor_decision,
2547                    success_count,
2548                ),
2549            })
2550            .map_err(store_err)?;
2551        if matches!(governor_decision.target_state, AssetState::Promoted) {
2552            self.store
2553                .append_event(EvolutionEvent::GenePromoted {
2554                    gene_id: gene.id.clone(),
2555                })
2556                .map_err(store_err)?;
2557        }
2558        if matches!(governor_decision.target_state, AssetState::Revoked) {
2559            self.store
2560                .append_event(EvolutionEvent::GeneRevoked {
2561                    gene_id: gene.id.clone(),
2562                    reason: governor_decision.reason.clone(),
2563                })
2564                .map_err(store_err)?;
2565        }
2566        if let Some(spec_id) = &mutation.intent.spec_id {
2567            self.store
2568                .append_event(EvolutionEvent::SpecLinked {
2569                    mutation_id: mutation.intent.id.clone(),
2570                    spec_id: spec_id.clone(),
2571                })
2572                .map_err(store_err)?;
2573        }
2574
2575        let mut capsule = build_capsule(
2576            run_id,
2577            &mutation,
2578            &receipt,
2579            &report,
2580            &self.validation_plan.profile,
2581            &gene,
2582            &blast_radius,
2583        )
2584        .map_err(|err| EvoKernelError::Validation(err.to_string()))?;
2585        capsule.state = governor_decision.target_state.clone();
2586        self.store
2587            .append_event(EvolutionEvent::CapsuleCommitted {
2588                capsule: capsule.clone(),
2589            })
2590            .map_err(store_err)?;
2591        if matches!(governor_decision.target_state, AssetState::Quarantined) {
2592            self.store
2593                .append_event(EvolutionEvent::CapsuleQuarantined {
2594                    capsule_id: capsule.id.clone(),
2595                })
2596                .map_err(store_err)?;
2597        }
2598
2599        Ok(CaptureOutcome {
2600            capsule,
2601            gene,
2602            governor_decision,
2603        })
2604    }
2605
2606    pub async fn capture_from_proposal(
2607        &self,
2608        run_id: &RunId,
2609        proposal: &AgentMutationProposal,
2610        diff_payload: String,
2611        base_revision: Option<String>,
2612    ) -> Result<CaptureOutcome, EvoKernelError> {
2613        let intent = MutationIntent {
2614            id: next_id("proposal"),
2615            intent: proposal.intent.clone(),
2616            target: MutationTarget::Paths {
2617                allow: proposal.files.clone(),
2618            },
2619            expected_effect: proposal.expected_effect.clone(),
2620            risk: RiskLevel::Low,
2621            signals: proposal.files.clone(),
2622            spec_id: None,
2623        };
2624        self.capture_mutation_with_governor(
2625            run_id,
2626            prepare_mutation(intent, diff_payload, base_revision),
2627        )
2628        .await
2629    }
2630
2631    pub fn feedback_for_agent(outcome: &CaptureOutcome) -> ExecutionFeedback {
2632        ExecutionFeedback {
2633            accepted: !matches!(outcome.governor_decision.target_state, AssetState::Revoked),
2634            asset_state: Some(format!("{:?}", outcome.governor_decision.target_state)),
2635            summary: outcome.governor_decision.reason.clone(),
2636        }
2637    }
2638
2639    pub fn replay_feedback_for_agent(
2640        signals: &[String],
2641        decision: &ReplayDecision,
2642    ) -> ReplayFeedback {
2643        let (task_class_id, task_label) = replay_task_descriptor(signals);
2644        let planner_directive = if decision.used_capsule {
2645            ReplayPlannerDirective::SkipPlanner
2646        } else {
2647            ReplayPlannerDirective::PlanFallback
2648        };
2649        let reasoning_steps_avoided = u64::from(decision.used_capsule);
2650        let reason_code_hint = decision
2651            .detect_evidence
2652            .mismatch_reasons
2653            .first()
2654            .and_then(|reason| infer_replay_fallback_reason_code(reason));
2655        let fallback_contract = normalize_replay_fallback_contract(
2656            &planner_directive,
2657            decision
2658                .fallback_to_planner
2659                .then_some(decision.reason.as_str()),
2660            reason_code_hint,
2661            None,
2662            None,
2663            None,
2664        );
2665        let summary = if decision.used_capsule {
2666            format!("reused prior capsule for task class '{task_label}'; skip planner")
2667        } else {
2668            format!(
2669                "planner fallback required for task class '{task_label}': {}",
2670                decision.reason
2671            )
2672        };
2673
2674        ReplayFeedback {
2675            used_capsule: decision.used_capsule,
2676            capsule_id: decision.capsule_id.clone(),
2677            planner_directive,
2678            reasoning_steps_avoided,
2679            fallback_reason: fallback_contract
2680                .as_ref()
2681                .map(|contract| contract.fallback_reason.clone()),
2682            reason_code: fallback_contract
2683                .as_ref()
2684                .map(|contract| contract.reason_code),
2685            repair_hint: fallback_contract
2686                .as_ref()
2687                .map(|contract| contract.repair_hint.clone()),
2688            next_action: fallback_contract
2689                .as_ref()
2690                .map(|contract| contract.next_action),
2691            confidence: fallback_contract
2692                .as_ref()
2693                .map(|contract| contract.confidence),
2694            task_class_id,
2695            task_label,
2696            summary,
2697        }
2698    }
2699
2700    fn mutation_needed_failure_outcome(
2701        &self,
2702        request: &SupervisedDevloopRequest,
2703        task_class: Option<BoundedTaskClass>,
2704        status: SupervisedDevloopStatus,
2705        contract: MutationNeededFailureContract,
2706        mutation_id_for_audit: Option<String>,
2707    ) -> Result<SupervisedDevloopOutcome, EvoKernelError> {
2708        if let Some(mutation_id) = mutation_id_for_audit {
2709            self.store
2710                .append_event(EvolutionEvent::MutationRejected {
2711                    mutation_id,
2712                    reason: contract.failure_reason.clone(),
2713                    reason_code: Some(
2714                        mutation_needed_reason_code_key(contract.reason_code).to_string(),
2715                    ),
2716                    recovery_hint: Some(contract.recovery_hint.clone()),
2717                    fail_closed: contract.fail_closed,
2718                })
2719                .map_err(store_err)?;
2720        }
2721        let status_label = match status {
2722            SupervisedDevloopStatus::AwaitingApproval => "awaiting_approval",
2723            SupervisedDevloopStatus::RejectedByPolicy => "rejected_by_policy",
2724            SupervisedDevloopStatus::FailedClosed => "failed_closed",
2725            SupervisedDevloopStatus::Executed => "executed",
2726        };
2727        let reason_code_key = mutation_needed_reason_code_key(contract.reason_code);
2728        Ok(SupervisedDevloopOutcome {
2729            task_id: request.task.id.clone(),
2730            task_class,
2731            status,
2732            execution_feedback: None,
2733            failure_contract: Some(contract.clone()),
2734            summary: format!(
2735                "supervised devloop {status_label} task '{}' [{reason_code_key}]: {}",
2736                request.task.id, contract.failure_reason
2737            ),
2738        })
2739    }
2740
2741    pub async fn run_supervised_devloop(
2742        &self,
2743        run_id: &RunId,
2744        request: &SupervisedDevloopRequest,
2745        diff_payload: String,
2746        base_revision: Option<String>,
2747    ) -> Result<SupervisedDevloopOutcome, EvoKernelError> {
2748        let audit_mutation_id = mutation_needed_audit_mutation_id(request);
2749        let task_class = classify_supervised_devloop_request(request);
2750        let Some(task_class) = task_class else {
2751            let contract = normalize_mutation_needed_failure_contract(
2752                Some(&format!(
2753                    "supervised devloop rejected task '{}' because it is an unsupported task outside the bounded scope",
2754                    request.task.id
2755                )),
2756                Some(MutationNeededFailureReasonCode::PolicyDenied),
2757            );
2758            return self.mutation_needed_failure_outcome(
2759                request,
2760                None,
2761                SupervisedDevloopStatus::RejectedByPolicy,
2762                contract,
2763                Some(audit_mutation_id),
2764            );
2765        };
2766
2767        if !request.approval.approved {
2768            return Ok(SupervisedDevloopOutcome {
2769                task_id: request.task.id.clone(),
2770                task_class: Some(task_class),
2771                status: SupervisedDevloopStatus::AwaitingApproval,
2772                execution_feedback: None,
2773                failure_contract: None,
2774                summary: format!(
2775                    "supervised devloop paused task '{}' until explicit human approval is granted",
2776                    request.task.id
2777                ),
2778            });
2779        }
2780
2781        if diff_payload.len() > MUTATION_NEEDED_MAX_DIFF_BYTES {
2782            let contract = normalize_mutation_needed_failure_contract(
2783                Some(&format!(
2784                    "mutation-needed diff payload exceeds bounded byte budget (size={}, max={})",
2785                    diff_payload.len(),
2786                    MUTATION_NEEDED_MAX_DIFF_BYTES
2787                )),
2788                Some(MutationNeededFailureReasonCode::PolicyDenied),
2789            );
2790            return self.mutation_needed_failure_outcome(
2791                request,
2792                Some(task_class),
2793                SupervisedDevloopStatus::RejectedByPolicy,
2794                contract,
2795                Some(audit_mutation_id),
2796            );
2797        }
2798
2799        let blast_radius = compute_blast_radius(&diff_payload);
2800        if blast_radius.lines_changed > MUTATION_NEEDED_MAX_CHANGED_LINES {
2801            let contract = normalize_mutation_needed_failure_contract(
2802                Some(&format!(
2803                    "mutation-needed patch exceeds bounded changed-line budget (lines_changed={}, max={})",
2804                    blast_radius.lines_changed,
2805                    MUTATION_NEEDED_MAX_CHANGED_LINES
2806                )),
2807                Some(MutationNeededFailureReasonCode::UnsafePatch),
2808            );
2809            return self.mutation_needed_failure_outcome(
2810                request,
2811                Some(task_class),
2812                SupervisedDevloopStatus::FailedClosed,
2813                contract,
2814                Some(audit_mutation_id),
2815            );
2816        }
2817
2818        if self.sandbox_policy.max_duration_ms > MUTATION_NEEDED_MAX_SANDBOX_DURATION_MS {
2819            let contract = normalize_mutation_needed_failure_contract(
2820                Some(&format!(
2821                    "mutation-needed sandbox duration budget exceeds bounded policy (configured={}ms, max={}ms)",
2822                    self.sandbox_policy.max_duration_ms,
2823                    MUTATION_NEEDED_MAX_SANDBOX_DURATION_MS
2824                )),
2825                Some(MutationNeededFailureReasonCode::PolicyDenied),
2826            );
2827            return self.mutation_needed_failure_outcome(
2828                request,
2829                Some(task_class),
2830                SupervisedDevloopStatus::RejectedByPolicy,
2831                contract,
2832                Some(audit_mutation_id),
2833            );
2834        }
2835
2836        let validation_budget_ms = validation_plan_timeout_budget_ms(&self.validation_plan);
2837        if validation_budget_ms > MUTATION_NEEDED_MAX_VALIDATION_BUDGET_MS {
2838            let contract = normalize_mutation_needed_failure_contract(
2839                Some(&format!(
2840                    "mutation-needed validation timeout budget exceeds bounded policy (configured={}ms, max={}ms)",
2841                    validation_budget_ms,
2842                    MUTATION_NEEDED_MAX_VALIDATION_BUDGET_MS
2843                )),
2844                Some(MutationNeededFailureReasonCode::PolicyDenied),
2845            );
2846            return self.mutation_needed_failure_outcome(
2847                request,
2848                Some(task_class),
2849                SupervisedDevloopStatus::RejectedByPolicy,
2850                contract,
2851                Some(audit_mutation_id),
2852            );
2853        }
2854
2855        let capture = match self
2856            .capture_from_proposal(run_id, &request.proposal, diff_payload, base_revision)
2857            .await
2858        {
2859            Ok(capture) => capture,
2860            Err(EvoKernelError::Sandbox(message)) => {
2861                let contract = mutation_needed_contract_for_error_message(&message);
2862                let status = mutation_needed_status_from_reason_code(contract.reason_code);
2863                return self.mutation_needed_failure_outcome(
2864                    request,
2865                    Some(task_class),
2866                    status,
2867                    contract,
2868                    None,
2869                );
2870            }
2871            Err(EvoKernelError::ValidationFailed(report)) => {
2872                let contract = mutation_needed_contract_for_validation_failure(
2873                    &self.validation_plan.profile,
2874                    &report,
2875                );
2876                let status = mutation_needed_status_from_reason_code(contract.reason_code);
2877                return self.mutation_needed_failure_outcome(
2878                    request,
2879                    Some(task_class),
2880                    status,
2881                    contract,
2882                    None,
2883                );
2884            }
2885            Err(EvoKernelError::Validation(message)) => {
2886                let contract = mutation_needed_contract_for_error_message(&message);
2887                let status = mutation_needed_status_from_reason_code(contract.reason_code);
2888                return self.mutation_needed_failure_outcome(
2889                    request,
2890                    Some(task_class),
2891                    status,
2892                    contract,
2893                    None,
2894                );
2895            }
2896            Err(err) => return Err(err),
2897        };
2898        let approver = request
2899            .approval
2900            .approver
2901            .as_deref()
2902            .unwrap_or("unknown approver");
2903
2904        Ok(SupervisedDevloopOutcome {
2905            task_id: request.task.id.clone(),
2906            task_class: Some(task_class),
2907            status: SupervisedDevloopStatus::Executed,
2908            execution_feedback: Some(Self::feedback_for_agent(&capture)),
2909            failure_contract: None,
2910            summary: format!(
2911                "supervised devloop executed task '{}' with explicit approval from {approver}",
2912                request.task.id
2913            ),
2914        })
2915    }
2916    pub fn coordinate(&self, plan: CoordinationPlan) -> CoordinationResult {
2917        MultiAgentCoordinator::new().coordinate(plan)
2918    }
2919
2920    pub fn export_promoted_assets(
2921        &self,
2922        sender_id: impl Into<String>,
2923    ) -> Result<EvolutionEnvelope, EvoKernelError> {
2924        let sender_id = sender_id.into();
2925        let envelope = export_promoted_assets_from_store(self.store.as_ref(), sender_id.clone())?;
2926        if !envelope.assets.is_empty() {
2927            let mut ledger = self
2928                .economics
2929                .lock()
2930                .map_err(|_| EvoKernelError::Validation("economics ledger lock poisoned".into()))?;
2931            if ledger
2932                .reserve_publish_stake(&sender_id, &self.stake_policy)
2933                .is_none()
2934            {
2935                return Err(EvoKernelError::Validation(
2936                    "insufficient EVU for remote publish".into(),
2937                ));
2938            }
2939        }
2940        Ok(envelope)
2941    }
2942
2943    pub fn import_remote_envelope(
2944        &self,
2945        envelope: &EvolutionEnvelope,
2946    ) -> Result<ImportOutcome, EvoKernelError> {
2947        import_remote_envelope_into_store(
2948            self.store.as_ref(),
2949            envelope,
2950            Some(self.remote_publishers.as_ref()),
2951            None,
2952        )
2953    }
2954
2955    pub fn fetch_assets(
2956        &self,
2957        responder_id: impl Into<String>,
2958        query: &FetchQuery,
2959    ) -> Result<FetchResponse, EvoKernelError> {
2960        fetch_assets_from_store(self.store.as_ref(), responder_id, query)
2961    }
2962
2963    pub fn revoke_assets(&self, notice: &RevokeNotice) -> Result<RevokeNotice, EvoKernelError> {
2964        revoke_assets_in_store(self.store.as_ref(), notice)
2965    }
2966
2967    pub async fn replay_or_fallback(
2968        &self,
2969        input: SelectorInput,
2970    ) -> Result<ReplayDecision, EvoKernelError> {
2971        let replay_run_id = next_id("replay");
2972        self.replay_or_fallback_for_run(&replay_run_id, input).await
2973    }
2974
2975    pub async fn replay_or_fallback_for_run(
2976        &self,
2977        run_id: &RunId,
2978        input: SelectorInput,
2979    ) -> Result<ReplayDecision, EvoKernelError> {
2980        let executor = StoreReplayExecutor {
2981            sandbox: self.sandbox.clone(),
2982            validator: self.validator.clone(),
2983            store: self.store.clone(),
2984            selector: self.selector.clone(),
2985            governor: self.governor.clone(),
2986            economics: Some(self.economics.clone()),
2987            remote_publishers: Some(self.remote_publishers.clone()),
2988            stake_policy: self.stake_policy.clone(),
2989        };
2990        executor
2991            .try_replay_for_run(run_id, &input, &self.sandbox_policy, &self.validation_plan)
2992            .await
2993            .map_err(|err| EvoKernelError::Validation(err.to_string()))
2994    }
2995
2996    pub fn economics_signal(&self, node_id: &str) -> Option<EconomicsSignal> {
2997        self.economics.lock().ok()?.governor_signal(node_id)
2998    }
2999
3000    pub fn selector_reputation_bias(&self) -> BTreeMap<String, f32> {
3001        self.economics
3002            .lock()
3003            .ok()
3004            .map(|locked| locked.selector_reputation_bias())
3005            .unwrap_or_default()
3006    }
3007
3008    pub fn metrics_snapshot(&self) -> Result<EvolutionMetricsSnapshot, EvoKernelError> {
3009        evolution_metrics_snapshot(self.store.as_ref())
3010    }
3011
3012    pub fn replay_roi_release_gate_summary(
3013        &self,
3014        window_seconds: u64,
3015    ) -> Result<ReplayRoiWindowSummary, EvoKernelError> {
3016        replay_roi_release_gate_summary(self.store.as_ref(), window_seconds)
3017    }
3018
3019    pub fn render_replay_roi_release_gate_summary_json(
3020        &self,
3021        window_seconds: u64,
3022    ) -> Result<String, EvoKernelError> {
3023        let summary = self.replay_roi_release_gate_summary(window_seconds)?;
3024        serde_json::to_string_pretty(&summary)
3025            .map_err(|err| EvoKernelError::Validation(err.to_string()))
3026    }
3027
3028    pub fn replay_roi_release_gate_contract(
3029        &self,
3030        window_seconds: u64,
3031        thresholds: ReplayRoiReleaseGateThresholds,
3032    ) -> Result<ReplayRoiReleaseGateContract, EvoKernelError> {
3033        let summary = self.replay_roi_release_gate_summary(window_seconds)?;
3034        Ok(replay_roi_release_gate_contract(&summary, thresholds))
3035    }
3036
3037    pub fn render_replay_roi_release_gate_contract_json(
3038        &self,
3039        window_seconds: u64,
3040        thresholds: ReplayRoiReleaseGateThresholds,
3041    ) -> Result<String, EvoKernelError> {
3042        let contract = self.replay_roi_release_gate_contract(window_seconds, thresholds)?;
3043        serde_json::to_string_pretty(&contract)
3044            .map_err(|err| EvoKernelError::Validation(err.to_string()))
3045    }
3046
3047    pub fn render_metrics_prometheus(&self) -> Result<String, EvoKernelError> {
3048        self.metrics_snapshot().map(|snapshot| {
3049            let health = evolution_health_snapshot(&snapshot);
3050            render_evolution_metrics_prometheus(&snapshot, &health)
3051        })
3052    }
3053
3054    pub fn health_snapshot(&self) -> Result<EvolutionHealthSnapshot, EvoKernelError> {
3055        self.metrics_snapshot()
3056            .map(|snapshot| evolution_health_snapshot(&snapshot))
3057    }
3058}
3059
3060pub fn prepare_mutation(
3061    intent: MutationIntent,
3062    diff_payload: String,
3063    base_revision: Option<String>,
3064) -> PreparedMutation {
3065    PreparedMutation {
3066        intent,
3067        artifact: MutationArtifact {
3068            encoding: ArtifactEncoding::UnifiedDiff,
3069            content_hash: compute_artifact_hash(&diff_payload),
3070            payload: diff_payload,
3071            base_revision,
3072        },
3073    }
3074}
3075
3076pub fn prepare_mutation_from_spec(
3077    plan: CompiledMutationPlan,
3078    diff_payload: String,
3079    base_revision: Option<String>,
3080) -> PreparedMutation {
3081    prepare_mutation(plan.mutation_intent, diff_payload, base_revision)
3082}
3083
3084pub fn default_evolution_store() -> Arc<dyn EvolutionStore> {
3085    Arc::new(oris_evolution::JsonlEvolutionStore::new(
3086        default_store_root(),
3087    ))
3088}
3089
3090fn built_in_seed_templates() -> Vec<SeedTemplate> {
3091    vec![
3092        SeedTemplate {
3093            id: "bootstrap-readme".into(),
3094            intent: "Seed a baseline README recovery pattern".into(),
3095            signals: vec!["bootstrap readme".into(), "missing readme".into()],
3096            diff_payload: "\
3097diff --git a/README.md b/README.md
3098new file mode 100644
3099index 0000000..1111111
3100--- /dev/null
3101+++ b/README.md
3102@@ -0,0 +1,3 @@
3103+# Oris
3104+Bootstrap documentation seed
3105+"
3106            .into(),
3107            validation_profile: "bootstrap-seed".into(),
3108        },
3109        SeedTemplate {
3110            id: "bootstrap-test-fix".into(),
3111            intent: "Seed a deterministic test stabilization pattern".into(),
3112            signals: vec!["bootstrap test fix".into(), "failing tests".into()],
3113            diff_payload: "\
3114diff --git a/src/lib.rs b/src/lib.rs
3115index 1111111..2222222 100644
3116--- a/src/lib.rs
3117+++ b/src/lib.rs
3118@@ -1 +1,2 @@
3119 pub fn demo() -> usize { 1 }
3120+pub fn normalize_test_output() -> bool { true }
3121"
3122            .into(),
3123            validation_profile: "bootstrap-seed".into(),
3124        },
3125        SeedTemplate {
3126            id: "bootstrap-refactor".into(),
3127            intent: "Seed a low-risk refactor capsule".into(),
3128            signals: vec!["bootstrap refactor".into(), "small refactor".into()],
3129            diff_payload: "\
3130diff --git a/src/lib.rs b/src/lib.rs
3131index 2222222..3333333 100644
3132--- a/src/lib.rs
3133+++ b/src/lib.rs
3134@@ -1 +1,3 @@
3135 pub fn demo() -> usize { 1 }
3136+
3137+fn extract_strategy_key(input: &str) -> &str { input }
3138"
3139            .into(),
3140            validation_profile: "bootstrap-seed".into(),
3141        },
3142        SeedTemplate {
3143            id: "bootstrap-logging".into(),
3144            intent: "Seed a baseline structured logging mutation".into(),
3145            signals: vec!["bootstrap logging".into(), "structured logs".into()],
3146            diff_payload: "\
3147diff --git a/src/lib.rs b/src/lib.rs
3148index 3333333..4444444 100644
3149--- a/src/lib.rs
3150+++ b/src/lib.rs
3151@@ -1 +1,3 @@
3152 pub fn demo() -> usize { 1 }
3153+
3154+fn emit_bootstrap_log() { println!(\"bootstrap-log\"); }
3155"
3156            .into(),
3157            validation_profile: "bootstrap-seed".into(),
3158        },
3159    ]
3160}
3161
3162fn build_seed_mutation(template: &SeedTemplate) -> PreparedMutation {
3163    let changed_files = seed_changed_files(&template.diff_payload);
3164    let target = if changed_files.is_empty() {
3165        MutationTarget::WorkspaceRoot
3166    } else {
3167        MutationTarget::Paths {
3168            allow: changed_files,
3169        }
3170    };
3171    prepare_mutation(
3172        MutationIntent {
3173            id: stable_hash_json(&("bootstrap-mutation", &template.id))
3174                .unwrap_or_else(|_| format!("bootstrap-mutation-{}", template.id)),
3175            intent: template.intent.clone(),
3176            target,
3177            expected_effect: format!("seed {}", template.id),
3178            risk: RiskLevel::Low,
3179            signals: template.signals.clone(),
3180            spec_id: None,
3181        },
3182        template.diff_payload.clone(),
3183        None,
3184    )
3185}
3186
3187fn extract_seed_signals(template: &SeedTemplate) -> SignalExtractionOutput {
3188    let mut signals = BTreeSet::new();
3189    for declared in &template.signals {
3190        if let Some(phrase) = normalize_signal_phrase(declared) {
3191            signals.insert(phrase);
3192        }
3193        extend_signal_tokens(&mut signals, declared);
3194    }
3195    extend_signal_tokens(&mut signals, &template.intent);
3196    extend_signal_tokens(&mut signals, &template.diff_payload);
3197    for changed_file in seed_changed_files(&template.diff_payload) {
3198        extend_signal_tokens(&mut signals, &changed_file);
3199    }
3200    let values = signals.into_iter().take(32).collect::<Vec<_>>();
3201    let hash =
3202        stable_hash_json(&values).unwrap_or_else(|_| compute_artifact_hash(&values.join("\n")));
3203    SignalExtractionOutput { values, hash }
3204}
3205
3206fn seed_changed_files(diff_payload: &str) -> Vec<String> {
3207    let mut changed_files = BTreeSet::new();
3208    for line in diff_payload.lines() {
3209        if let Some(path) = line.strip_prefix("+++ b/") {
3210            let normalized = path.trim();
3211            if !normalized.is_empty() {
3212                changed_files.insert(normalized.to_string());
3213            }
3214        }
3215    }
3216    changed_files.into_iter().collect()
3217}
3218
3219fn build_bootstrap_gene(
3220    template: &SeedTemplate,
3221    extracted: &SignalExtractionOutput,
3222) -> Result<Gene, EvolutionError> {
3223    let strategy = vec![template.id.clone(), "bootstrap".into()];
3224    let id = stable_hash_json(&(
3225        "bootstrap-gene",
3226        &template.id,
3227        &extracted.values,
3228        &template.validation_profile,
3229    ))?;
3230    Ok(Gene {
3231        id,
3232        signals: extracted.values.clone(),
3233        strategy,
3234        validation: vec![template.validation_profile.clone()],
3235        state: AssetState::Quarantined,
3236    })
3237}
3238
3239fn build_bootstrap_capsule(
3240    run_id: &RunId,
3241    template: &SeedTemplate,
3242    mutation: &PreparedMutation,
3243    gene: &Gene,
3244) -> Result<Capsule, EvolutionError> {
3245    let cwd = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
3246    let env = current_env_fingerprint(&cwd);
3247    let diff_hash = mutation.artifact.content_hash.clone();
3248    let changed_files = seed_changed_files(&template.diff_payload);
3249    let validator_hash = stable_hash_json(&(
3250        "bootstrap-validator",
3251        &template.id,
3252        &template.validation_profile,
3253        &diff_hash,
3254    ))?;
3255    let id = stable_hash_json(&(
3256        "bootstrap-capsule",
3257        &template.id,
3258        run_id,
3259        &gene.id,
3260        &diff_hash,
3261        &env,
3262    ))?;
3263    Ok(Capsule {
3264        id,
3265        gene_id: gene.id.clone(),
3266        mutation_id: mutation.intent.id.clone(),
3267        run_id: run_id.clone(),
3268        diff_hash,
3269        confidence: 0.0,
3270        env,
3271        outcome: Outcome {
3272            success: false,
3273            validation_profile: template.validation_profile.clone(),
3274            validation_duration_ms: 0,
3275            changed_files,
3276            validator_hash,
3277            lines_changed: compute_blast_radius(&template.diff_payload).lines_changed,
3278            replay_verified: false,
3279        },
3280        state: AssetState::Quarantined,
3281    })
3282}
3283
3284fn derive_gene(
3285    mutation: &PreparedMutation,
3286    receipt: &SandboxReceipt,
3287    validation_profile: &str,
3288    extracted_signals: &[String],
3289) -> Gene {
3290    let mut strategy = BTreeSet::new();
3291    for file in &receipt.changed_files {
3292        if let Some(component) = file.components().next() {
3293            strategy.insert(component.as_os_str().to_string_lossy().to_string());
3294        }
3295    }
3296    for token in mutation
3297        .artifact
3298        .payload
3299        .split(|ch: char| !ch.is_ascii_alphanumeric())
3300    {
3301        if token.len() == 5
3302            && token.starts_with('E')
3303            && token[1..].chars().all(|ch| ch.is_ascii_digit())
3304        {
3305            strategy.insert(token.to_string());
3306        }
3307    }
3308    for token in mutation.intent.intent.split_whitespace().take(8) {
3309        strategy.insert(token.to_ascii_lowercase());
3310    }
3311    let strategy = strategy.into_iter().collect::<Vec<_>>();
3312    let id = stable_hash_json(&(extracted_signals, &strategy, validation_profile))
3313        .unwrap_or_else(|_| next_id("gene"));
3314    Gene {
3315        id,
3316        signals: extracted_signals.to_vec(),
3317        strategy,
3318        validation: vec![validation_profile.to_string()],
3319        state: AssetState::Promoted,
3320    }
3321}
3322
3323fn build_capsule(
3324    run_id: &RunId,
3325    mutation: &PreparedMutation,
3326    receipt: &SandboxReceipt,
3327    report: &ValidationReport,
3328    validation_profile: &str,
3329    gene: &Gene,
3330    blast_radius: &BlastRadius,
3331) -> Result<Capsule, EvolutionError> {
3332    let env = current_env_fingerprint(&receipt.workdir);
3333    let validator_hash = stable_hash_json(report)?;
3334    let diff_hash = mutation.artifact.content_hash.clone();
3335    let id = stable_hash_json(&(run_id, &gene.id, &diff_hash, &mutation.intent.id))?;
3336    Ok(Capsule {
3337        id,
3338        gene_id: gene.id.clone(),
3339        mutation_id: mutation.intent.id.clone(),
3340        run_id: run_id.clone(),
3341        diff_hash,
3342        confidence: 0.7,
3343        env,
3344        outcome: oris_evolution::Outcome {
3345            success: true,
3346            validation_profile: validation_profile.to_string(),
3347            validation_duration_ms: report.duration_ms,
3348            changed_files: receipt
3349                .changed_files
3350                .iter()
3351                .map(|path| path.to_string_lossy().to_string())
3352                .collect(),
3353            validator_hash,
3354            lines_changed: blast_radius.lines_changed,
3355            replay_verified: false,
3356        },
3357        state: AssetState::Promoted,
3358    })
3359}
3360
3361fn current_env_fingerprint(workdir: &Path) -> EnvFingerprint {
3362    let rustc_version = Command::new("rustc")
3363        .arg("--version")
3364        .output()
3365        .ok()
3366        .filter(|output| output.status.success())
3367        .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
3368        .unwrap_or_else(|| "rustc unknown".into());
3369    let cargo_lock_hash = fs::read(workdir.join("Cargo.lock"))
3370        .ok()
3371        .map(|bytes| {
3372            let value = String::from_utf8_lossy(&bytes);
3373            compute_artifact_hash(&value)
3374        })
3375        .unwrap_or_else(|| "missing-cargo-lock".into());
3376    let target_triple = format!(
3377        "{}-unknown-{}",
3378        std::env::consts::ARCH,
3379        std::env::consts::OS
3380    );
3381    EnvFingerprint {
3382        rustc_version,
3383        cargo_lock_hash,
3384        target_triple,
3385        os: std::env::consts::OS.to_string(),
3386    }
3387}
3388
3389fn extend_signal_tokens(out: &mut BTreeSet<String>, input: &str) {
3390    for raw in input.split(|ch: char| !ch.is_ascii_alphanumeric()) {
3391        let trimmed = raw.trim();
3392        if trimmed.is_empty() {
3393            continue;
3394        }
3395        let normalized = if is_rust_error_code(trimmed) {
3396            let mut chars = trimmed.chars();
3397            let prefix = chars
3398                .next()
3399                .map(|ch| ch.to_ascii_uppercase())
3400                .unwrap_or('E');
3401            format!("{prefix}{}", chars.as_str())
3402        } else {
3403            trimmed.to_ascii_lowercase()
3404        };
3405        if normalized.len() < 3 {
3406            continue;
3407        }
3408        out.insert(normalized);
3409    }
3410}
3411
3412fn normalize_signal_phrase(input: &str) -> Option<String> {
3413    let normalized = input
3414        .split(|ch: char| !ch.is_ascii_alphanumeric())
3415        .filter_map(|raw| {
3416            let trimmed = raw.trim();
3417            if trimmed.is_empty() {
3418                return None;
3419            }
3420            let normalized = if is_rust_error_code(trimmed) {
3421                let mut chars = trimmed.chars();
3422                let prefix = chars
3423                    .next()
3424                    .map(|ch| ch.to_ascii_uppercase())
3425                    .unwrap_or('E');
3426                format!("{prefix}{}", chars.as_str())
3427            } else {
3428                trimmed.to_ascii_lowercase()
3429            };
3430            if normalized.len() < 3 {
3431                None
3432            } else {
3433                Some(normalized)
3434            }
3435        })
3436        .collect::<Vec<_>>()
3437        .join(" ");
3438    if normalized.is_empty() {
3439        None
3440    } else {
3441        Some(normalized)
3442    }
3443}
3444
3445fn replay_task_descriptor(signals: &[String]) -> (String, String) {
3446    let normalized = signals
3447        .iter()
3448        .filter_map(|signal| normalize_signal_phrase(signal))
3449        .collect::<BTreeSet<_>>()
3450        .into_iter()
3451        .collect::<Vec<_>>();
3452    if normalized.is_empty() {
3453        return ("unknown".into(), "unknown".into());
3454    }
3455    let task_label = normalized
3456        .iter()
3457        .find(|value| {
3458            value.as_str() != "validation passed" && value.as_str() != "validation failed"
3459        })
3460        .cloned()
3461        .unwrap_or_else(|| normalized[0].clone());
3462    let task_class_id = stable_hash_json(&normalized)
3463        .unwrap_or_else(|_| compute_artifact_hash(&normalized.join("\n")));
3464    (task_class_id, task_label)
3465}
3466
3467fn normalized_signal_values(signals: &[String]) -> Vec<String> {
3468    signals
3469        .iter()
3470        .filter_map(|signal| normalize_signal_phrase(signal))
3471        .collect::<BTreeSet<_>>()
3472        .into_iter()
3473        .collect::<Vec<_>>()
3474}
3475
3476fn matched_replay_signals(input_signals: &[String], candidate_signals: &[String]) -> Vec<String> {
3477    let normalized_input = normalized_signal_values(input_signals);
3478    if normalized_input.is_empty() {
3479        return Vec::new();
3480    }
3481    let normalized_candidate = normalized_signal_values(candidate_signals);
3482    if normalized_candidate.is_empty() {
3483        return normalized_input;
3484    }
3485    let matched = normalized_input
3486        .iter()
3487        .filter(|signal| {
3488            normalized_candidate
3489                .iter()
3490                .any(|candidate| candidate.contains(signal.as_str()) || signal.contains(candidate))
3491        })
3492        .cloned()
3493        .collect::<Vec<_>>();
3494    if matched.is_empty() {
3495        normalized_input
3496    } else {
3497        matched
3498    }
3499}
3500
3501fn replay_detect_evidence_from_input(input: &SelectorInput) -> ReplayDetectEvidence {
3502    let (task_class_id, task_label) = replay_task_descriptor(&input.signals);
3503    ReplayDetectEvidence {
3504        task_class_id,
3505        task_label,
3506        matched_signals: normalized_signal_values(&input.signals),
3507        mismatch_reasons: Vec::new(),
3508    }
3509}
3510
3511fn replay_descriptor_from_candidate_or_input(
3512    candidate: Option<&GeneCandidate>,
3513    input: &SelectorInput,
3514) -> (String, String) {
3515    if let Some(candidate) = candidate {
3516        let task_class_id = strategy_metadata_value(&candidate.gene.strategy, "task_class");
3517        let task_label = strategy_metadata_value(&candidate.gene.strategy, "task_label");
3518        if let Some(task_class_id) = task_class_id {
3519            return (
3520                task_class_id.clone(),
3521                task_label.unwrap_or_else(|| task_class_id.clone()),
3522            );
3523        }
3524        return replay_task_descriptor(&candidate.gene.signals);
3525    }
3526    replay_task_descriptor(&input.signals)
3527}
3528
3529fn estimated_reasoning_tokens(signals: &[String]) -> u64 {
3530    let normalized = signals
3531        .iter()
3532        .filter_map(|signal| normalize_signal_phrase(signal))
3533        .collect::<BTreeSet<_>>();
3534    let signal_count = normalized.len() as u64;
3535    REPLAY_REASONING_TOKEN_FLOOR + REPLAY_REASONING_TOKEN_SIGNAL_WEIGHT * signal_count.max(1)
3536}
3537
3538fn compute_replay_roi(reasoning_avoided_tokens: u64, replay_fallback_cost: u64) -> f64 {
3539    let total = reasoning_avoided_tokens + replay_fallback_cost;
3540    if total == 0 {
3541        return 0.0;
3542    }
3543    (reasoning_avoided_tokens as f64 - replay_fallback_cost as f64) / total as f64
3544}
3545
3546fn is_rust_error_code(value: &str) -> bool {
3547    value.len() == 5
3548        && matches!(value.as_bytes().first(), Some(b'e') | Some(b'E'))
3549        && value[1..].chars().all(|ch| ch.is_ascii_digit())
3550}
3551
3552fn validation_plan_timeout_budget_ms(plan: &ValidationPlan) -> u64 {
3553    plan.stages.iter().fold(0_u64, |acc, stage| match stage {
3554        ValidationStage::Command { timeout_ms, .. } => acc.saturating_add(*timeout_ms),
3555    })
3556}
3557
3558fn mutation_needed_reason_code_key(reason_code: MutationNeededFailureReasonCode) -> &'static str {
3559    match reason_code {
3560        MutationNeededFailureReasonCode::PolicyDenied => "policy_denied",
3561        MutationNeededFailureReasonCode::ValidationFailed => "validation_failed",
3562        MutationNeededFailureReasonCode::UnsafePatch => "unsafe_patch",
3563        MutationNeededFailureReasonCode::Timeout => "timeout",
3564        MutationNeededFailureReasonCode::MutationPayloadMissing => "mutation_payload_missing",
3565        MutationNeededFailureReasonCode::UnknownFailClosed => "unknown_fail_closed",
3566    }
3567}
3568
3569fn mutation_needed_status_from_reason_code(
3570    reason_code: MutationNeededFailureReasonCode,
3571) -> SupervisedDevloopStatus {
3572    if matches!(reason_code, MutationNeededFailureReasonCode::PolicyDenied) {
3573        SupervisedDevloopStatus::RejectedByPolicy
3574    } else {
3575        SupervisedDevloopStatus::FailedClosed
3576    }
3577}
3578
3579fn mutation_needed_contract_for_validation_failure(
3580    profile: &str,
3581    report: &ValidationReport,
3582) -> MutationNeededFailureContract {
3583    let lower_logs = report.logs.to_ascii_lowercase();
3584    if lower_logs.contains("timed out") {
3585        normalize_mutation_needed_failure_contract(
3586            Some(&format!(
3587                "mutation-needed validation command timed out under profile '{profile}'"
3588            )),
3589            Some(MutationNeededFailureReasonCode::Timeout),
3590        )
3591    } else {
3592        normalize_mutation_needed_failure_contract(
3593            Some(&format!(
3594                "mutation-needed validation failed under profile '{profile}'"
3595            )),
3596            Some(MutationNeededFailureReasonCode::ValidationFailed),
3597        )
3598    }
3599}
3600
3601fn mutation_needed_contract_for_error_message(message: &str) -> MutationNeededFailureContract {
3602    let reason_code = infer_mutation_needed_failure_reason_code(message);
3603    normalize_mutation_needed_failure_contract(Some(message), reason_code)
3604}
3605
3606fn mutation_needed_audit_mutation_id(request: &SupervisedDevloopRequest) -> String {
3607    stable_hash_json(&(
3608        "mutation-needed-audit",
3609        &request.task.id,
3610        &request.proposal.intent,
3611        &request.proposal.files,
3612    ))
3613    .map(|hash| format!("mutation-needed-{hash}"))
3614    .unwrap_or_else(|_| format!("mutation-needed-{}", request.task.id))
3615}
3616
3617fn classify_supervised_devloop_request(
3618    request: &SupervisedDevloopRequest,
3619) -> Option<BoundedTaskClass> {
3620    let path = request.proposal.files.first()?.trim();
3621    if request.proposal.files.len() != 1 || path.is_empty() {
3622        return None;
3623    }
3624    let normalized = path.replace('\\', "/");
3625    if normalized.starts_with("docs/") && normalized.ends_with(".md") {
3626        Some(BoundedTaskClass::DocsSingleFile)
3627    } else {
3628        None
3629    }
3630}
3631
3632fn find_declared_mutation(
3633    store: &dyn EvolutionStore,
3634    mutation_id: &MutationId,
3635) -> Result<Option<PreparedMutation>, EvolutionError> {
3636    for stored in store.scan(1)? {
3637        if let EvolutionEvent::MutationDeclared { mutation } = stored.event {
3638            if &mutation.intent.id == mutation_id {
3639                return Ok(Some(mutation));
3640            }
3641        }
3642    }
3643    Ok(None)
3644}
3645
3646fn exact_match_candidates(store: &dyn EvolutionStore, input: &SelectorInput) -> Vec<GeneCandidate> {
3647    let Ok(projection) = projection_snapshot(store) else {
3648        return Vec::new();
3649    };
3650    let capsules = projection.capsules.clone();
3651    let spec_ids_by_gene = projection.spec_ids_by_gene.clone();
3652    let requested_spec_id = input
3653        .spec_id
3654        .as_deref()
3655        .map(str::trim)
3656        .filter(|value| !value.is_empty());
3657    let signal_set = input
3658        .signals
3659        .iter()
3660        .map(|signal| signal.to_ascii_lowercase())
3661        .collect::<BTreeSet<_>>();
3662    let mut candidates = projection
3663        .genes
3664        .into_iter()
3665        .filter_map(|gene| {
3666            if gene.state != AssetState::Promoted {
3667                return None;
3668            }
3669            if let Some(spec_id) = requested_spec_id {
3670                let matches_spec = spec_ids_by_gene
3671                    .get(&gene.id)
3672                    .map(|values| {
3673                        values
3674                            .iter()
3675                            .any(|value| value.eq_ignore_ascii_case(spec_id))
3676                    })
3677                    .unwrap_or(false);
3678                if !matches_spec {
3679                    return None;
3680                }
3681            }
3682            let gene_signals = gene
3683                .signals
3684                .iter()
3685                .map(|signal| signal.to_ascii_lowercase())
3686                .collect::<BTreeSet<_>>();
3687            if gene_signals == signal_set {
3688                let mut matched_capsules = capsules
3689                    .iter()
3690                    .filter(|capsule| {
3691                        capsule.gene_id == gene.id && capsule.state == AssetState::Promoted
3692                    })
3693                    .cloned()
3694                    .collect::<Vec<_>>();
3695                matched_capsules.sort_by(|left, right| {
3696                    replay_environment_match_factor(&input.env, &right.env)
3697                        .partial_cmp(&replay_environment_match_factor(&input.env, &left.env))
3698                        .unwrap_or(std::cmp::Ordering::Equal)
3699                        .then_with(|| {
3700                            right
3701                                .confidence
3702                                .partial_cmp(&left.confidence)
3703                                .unwrap_or(std::cmp::Ordering::Equal)
3704                        })
3705                        .then_with(|| left.id.cmp(&right.id))
3706                });
3707                if matched_capsules.is_empty() {
3708                    None
3709                } else {
3710                    let score = matched_capsules
3711                        .first()
3712                        .map(|capsule| replay_environment_match_factor(&input.env, &capsule.env))
3713                        .unwrap_or(0.0);
3714                    Some(GeneCandidate {
3715                        gene,
3716                        score,
3717                        capsules: matched_capsules,
3718                    })
3719                }
3720            } else {
3721                None
3722            }
3723        })
3724        .collect::<Vec<_>>();
3725    candidates.sort_by(|left, right| {
3726        right
3727            .score
3728            .partial_cmp(&left.score)
3729            .unwrap_or(std::cmp::Ordering::Equal)
3730            .then_with(|| left.gene.id.cmp(&right.gene.id))
3731    });
3732    candidates
3733}
3734
3735fn quarantined_remote_exact_match_candidates(
3736    store: &dyn EvolutionStore,
3737    input: &SelectorInput,
3738) -> Vec<GeneCandidate> {
3739    let remote_asset_ids = store
3740        .scan(1)
3741        .ok()
3742        .map(|events| {
3743            events
3744                .into_iter()
3745                .filter_map(|stored| match stored.event {
3746                    EvolutionEvent::RemoteAssetImported {
3747                        source: CandidateSource::Remote,
3748                        asset_ids,
3749                        ..
3750                    } => Some(asset_ids),
3751                    _ => None,
3752                })
3753                .flatten()
3754                .collect::<BTreeSet<_>>()
3755        })
3756        .unwrap_or_default();
3757    if remote_asset_ids.is_empty() {
3758        return Vec::new();
3759    }
3760
3761    let Ok(projection) = projection_snapshot(store) else {
3762        return Vec::new();
3763    };
3764    let capsules = projection.capsules.clone();
3765    let spec_ids_by_gene = projection.spec_ids_by_gene.clone();
3766    let requested_spec_id = input
3767        .spec_id
3768        .as_deref()
3769        .map(str::trim)
3770        .filter(|value| !value.is_empty());
3771    let normalized_signals = input
3772        .signals
3773        .iter()
3774        .filter_map(|signal| normalize_signal_phrase(signal))
3775        .collect::<BTreeSet<_>>()
3776        .into_iter()
3777        .collect::<Vec<_>>();
3778    if normalized_signals.is_empty() {
3779        return Vec::new();
3780    }
3781    let mut candidates = projection
3782        .genes
3783        .into_iter()
3784        .filter_map(|gene| {
3785            if !matches!(
3786                gene.state,
3787                AssetState::Promoted | AssetState::Quarantined | AssetState::ShadowValidated
3788            ) {
3789                return None;
3790            }
3791            if let Some(spec_id) = requested_spec_id {
3792                let matches_spec = spec_ids_by_gene
3793                    .get(&gene.id)
3794                    .map(|values| {
3795                        values
3796                            .iter()
3797                            .any(|value| value.eq_ignore_ascii_case(spec_id))
3798                    })
3799                    .unwrap_or(false);
3800                if !matches_spec {
3801                    return None;
3802                }
3803            }
3804            let normalized_gene_signals = gene
3805                .signals
3806                .iter()
3807                .filter_map(|candidate| normalize_signal_phrase(candidate))
3808                .collect::<Vec<_>>();
3809            let matched_query_count = normalized_signals
3810                .iter()
3811                .filter(|signal| {
3812                    normalized_gene_signals.iter().any(|candidate| {
3813                        candidate.contains(signal.as_str()) || signal.contains(candidate)
3814                    })
3815                })
3816                .count();
3817            if matched_query_count == 0 {
3818                return None;
3819            }
3820
3821            let mut matched_capsules = capsules
3822                .iter()
3823                .filter(|capsule| {
3824                    capsule.gene_id == gene.id
3825                        && matches!(
3826                            capsule.state,
3827                            AssetState::Quarantined | AssetState::ShadowValidated
3828                        )
3829                        && remote_asset_ids.contains(&capsule.id)
3830                })
3831                .cloned()
3832                .collect::<Vec<_>>();
3833            matched_capsules.sort_by(|left, right| {
3834                replay_environment_match_factor(&input.env, &right.env)
3835                    .partial_cmp(&replay_environment_match_factor(&input.env, &left.env))
3836                    .unwrap_or(std::cmp::Ordering::Equal)
3837                    .then_with(|| {
3838                        right
3839                            .confidence
3840                            .partial_cmp(&left.confidence)
3841                            .unwrap_or(std::cmp::Ordering::Equal)
3842                    })
3843                    .then_with(|| left.id.cmp(&right.id))
3844            });
3845            if matched_capsules.is_empty() {
3846                None
3847            } else {
3848                let overlap = matched_query_count as f32 / normalized_signals.len() as f32;
3849                let env_score = matched_capsules
3850                    .first()
3851                    .map(|capsule| replay_environment_match_factor(&input.env, &capsule.env))
3852                    .unwrap_or(0.0);
3853                Some(GeneCandidate {
3854                    gene,
3855                    score: overlap.max(env_score),
3856                    capsules: matched_capsules,
3857                })
3858            }
3859        })
3860        .collect::<Vec<_>>();
3861    candidates.sort_by(|left, right| {
3862        right
3863            .score
3864            .partial_cmp(&left.score)
3865            .unwrap_or(std::cmp::Ordering::Equal)
3866            .then_with(|| left.gene.id.cmp(&right.gene.id))
3867    });
3868    candidates
3869}
3870
3871fn replay_environment_match_factor(input: &EnvFingerprint, candidate: &EnvFingerprint) -> f32 {
3872    let fields = [
3873        input
3874            .rustc_version
3875            .eq_ignore_ascii_case(&candidate.rustc_version),
3876        input
3877            .cargo_lock_hash
3878            .eq_ignore_ascii_case(&candidate.cargo_lock_hash),
3879        input
3880            .target_triple
3881            .eq_ignore_ascii_case(&candidate.target_triple),
3882        input.os.eq_ignore_ascii_case(&candidate.os),
3883    ];
3884    let matched_fields = fields.into_iter().filter(|matched| *matched).count() as f32;
3885    0.5 + ((matched_fields / 4.0) * 0.5)
3886}
3887
3888fn effective_candidate_score(
3889    candidate: &GeneCandidate,
3890    publishers_by_asset: &BTreeMap<String, String>,
3891    reputation_bias: &BTreeMap<String, f32>,
3892) -> f32 {
3893    let bias = candidate
3894        .capsules
3895        .first()
3896        .and_then(|capsule| publishers_by_asset.get(&capsule.id))
3897        .and_then(|publisher| reputation_bias.get(publisher))
3898        .copied()
3899        .unwrap_or(0.0)
3900        .clamp(0.0, 1.0);
3901    candidate.score * (1.0 + (bias * 0.1))
3902}
3903
3904fn export_promoted_assets_from_store(
3905    store: &dyn EvolutionStore,
3906    sender_id: impl Into<String>,
3907) -> Result<EvolutionEnvelope, EvoKernelError> {
3908    let (events, projection) = scan_projection(store)?;
3909    let genes = projection
3910        .genes
3911        .into_iter()
3912        .filter(|gene| gene.state == AssetState::Promoted)
3913        .collect::<Vec<_>>();
3914    let capsules = projection
3915        .capsules
3916        .into_iter()
3917        .filter(|capsule| capsule.state == AssetState::Promoted)
3918        .collect::<Vec<_>>();
3919    let assets = replay_export_assets(&events, genes, capsules);
3920    Ok(EvolutionEnvelope::publish(sender_id, assets))
3921}
3922
3923fn scan_projection(
3924    store: &dyn EvolutionStore,
3925) -> Result<(Vec<StoredEvolutionEvent>, EvolutionProjection), EvoKernelError> {
3926    store.scan_projection().map_err(store_err)
3927}
3928
3929fn projection_snapshot(store: &dyn EvolutionStore) -> Result<EvolutionProjection, EvoKernelError> {
3930    scan_projection(store).map(|(_, projection)| projection)
3931}
3932
3933fn replay_export_assets(
3934    events: &[StoredEvolutionEvent],
3935    genes: Vec<Gene>,
3936    capsules: Vec<Capsule>,
3937) -> Vec<NetworkAsset> {
3938    let mutation_ids = capsules
3939        .iter()
3940        .map(|capsule| capsule.mutation_id.clone())
3941        .collect::<BTreeSet<_>>();
3942    let mut assets = replay_export_events_for_mutations(events, &mutation_ids);
3943    for gene in genes {
3944        assets.push(NetworkAsset::Gene { gene });
3945    }
3946    for capsule in capsules {
3947        assets.push(NetworkAsset::Capsule { capsule });
3948    }
3949    assets
3950}
3951
3952fn replay_export_events_for_mutations(
3953    events: &[StoredEvolutionEvent],
3954    mutation_ids: &BTreeSet<String>,
3955) -> Vec<NetworkAsset> {
3956    if mutation_ids.is_empty() {
3957        return Vec::new();
3958    }
3959
3960    let mut assets = Vec::new();
3961    let mut seen_mutations = BTreeSet::new();
3962    let mut seen_spec_links = BTreeSet::new();
3963    for stored in events {
3964        match &stored.event {
3965            EvolutionEvent::MutationDeclared { mutation }
3966                if mutation_ids.contains(mutation.intent.id.as_str())
3967                    && seen_mutations.insert(mutation.intent.id.clone()) =>
3968            {
3969                assets.push(NetworkAsset::EvolutionEvent {
3970                    event: EvolutionEvent::MutationDeclared {
3971                        mutation: mutation.clone(),
3972                    },
3973                });
3974            }
3975            EvolutionEvent::SpecLinked {
3976                mutation_id,
3977                spec_id,
3978            } if mutation_ids.contains(mutation_id.as_str())
3979                && seen_spec_links.insert((mutation_id.clone(), spec_id.clone())) =>
3980            {
3981                assets.push(NetworkAsset::EvolutionEvent {
3982                    event: EvolutionEvent::SpecLinked {
3983                        mutation_id: mutation_id.clone(),
3984                        spec_id: spec_id.clone(),
3985                    },
3986                });
3987            }
3988            _ => {}
3989        }
3990    }
3991
3992    assets
3993}
3994
3995const SYNC_CURSOR_PREFIX: &str = "seq:";
3996const SYNC_RESUME_TOKEN_PREFIX: &str = "gep-rt1|";
3997
3998#[derive(Clone, Debug)]
3999struct DeltaWindow {
4000    changed_gene_ids: BTreeSet<String>,
4001    changed_capsule_ids: BTreeSet<String>,
4002    changed_mutation_ids: BTreeSet<String>,
4003}
4004
4005fn normalize_sync_value(value: Option<&str>) -> Option<String> {
4006    value
4007        .map(str::trim)
4008        .filter(|value| !value.is_empty())
4009        .map(ToOwned::to_owned)
4010}
4011
4012fn parse_sync_cursor_seq(cursor: &str) -> Option<u64> {
4013    let trimmed = cursor.trim();
4014    if trimmed.is_empty() {
4015        return None;
4016    }
4017    let raw = trimmed.strip_prefix(SYNC_CURSOR_PREFIX).unwrap_or(trimmed);
4018    raw.parse::<u64>().ok()
4019}
4020
4021fn format_sync_cursor(seq: u64) -> String {
4022    format!("{SYNC_CURSOR_PREFIX}{seq}")
4023}
4024
4025fn encode_resume_token(sender_id: &str, cursor: &str) -> String {
4026    format!("{SYNC_RESUME_TOKEN_PREFIX}{sender_id}|{cursor}")
4027}
4028
4029fn decode_resume_token(sender_id: &str, token: &str) -> Result<String, EvoKernelError> {
4030    let token = token.trim();
4031    let Some(encoded) = token.strip_prefix(SYNC_RESUME_TOKEN_PREFIX) else {
4032        return Ok(token.to_string());
4033    };
4034    let (token_sender, cursor) = encoded.split_once('|').ok_or_else(|| {
4035        EvoKernelError::Validation(
4036            "invalid resume_token format; expected gep-rt1|<sender>|<seq>".into(),
4037        )
4038    })?;
4039    if token_sender != sender_id.trim() {
4040        return Err(EvoKernelError::Validation(
4041            "resume_token sender mismatch".into(),
4042        ));
4043    }
4044    Ok(cursor.to_string())
4045}
4046
4047fn resolve_requested_cursor(
4048    sender_id: &str,
4049    since_cursor: Option<&str>,
4050    resume_token: Option<&str>,
4051) -> Result<Option<String>, EvoKernelError> {
4052    let cursor = if let Some(token) = normalize_sync_value(resume_token) {
4053        Some(decode_resume_token(sender_id, &token)?)
4054    } else {
4055        normalize_sync_value(since_cursor)
4056    };
4057
4058    let Some(cursor) = cursor else {
4059        return Ok(None);
4060    };
4061    let seq = parse_sync_cursor_seq(&cursor).ok_or_else(|| {
4062        EvoKernelError::Validation("invalid since_cursor/resume_token cursor format".into())
4063    })?;
4064    Ok(Some(format_sync_cursor(seq)))
4065}
4066
4067fn latest_store_cursor(store: &dyn EvolutionStore) -> Result<Option<String>, EvoKernelError> {
4068    let events = store.scan(1).map_err(store_err)?;
4069    Ok(events.last().map(|stored| format_sync_cursor(stored.seq)))
4070}
4071
4072fn delta_window(events: &[StoredEvolutionEvent], since_seq: u64) -> DeltaWindow {
4073    let mut changed_gene_ids = BTreeSet::new();
4074    let mut changed_capsule_ids = BTreeSet::new();
4075    let mut changed_mutation_ids = BTreeSet::new();
4076
4077    for stored in events {
4078        if stored.seq <= since_seq {
4079            continue;
4080        }
4081        match &stored.event {
4082            EvolutionEvent::MutationDeclared { mutation } => {
4083                changed_mutation_ids.insert(mutation.intent.id.clone());
4084            }
4085            EvolutionEvent::SpecLinked { mutation_id, .. } => {
4086                changed_mutation_ids.insert(mutation_id.clone());
4087            }
4088            EvolutionEvent::GeneProjected { gene } => {
4089                changed_gene_ids.insert(gene.id.clone());
4090            }
4091            EvolutionEvent::GenePromoted { gene_id }
4092            | EvolutionEvent::GeneRevoked { gene_id, .. }
4093            | EvolutionEvent::PromotionEvaluated { gene_id, .. } => {
4094                changed_gene_ids.insert(gene_id.clone());
4095            }
4096            EvolutionEvent::CapsuleCommitted { capsule } => {
4097                changed_capsule_ids.insert(capsule.id.clone());
4098                changed_gene_ids.insert(capsule.gene_id.clone());
4099                changed_mutation_ids.insert(capsule.mutation_id.clone());
4100            }
4101            EvolutionEvent::CapsuleReleased { capsule_id, .. }
4102            | EvolutionEvent::CapsuleQuarantined { capsule_id } => {
4103                changed_capsule_ids.insert(capsule_id.clone());
4104            }
4105            EvolutionEvent::RemoteAssetImported { asset_ids, .. } => {
4106                for asset_id in asset_ids {
4107                    changed_gene_ids.insert(asset_id.clone());
4108                    changed_capsule_ids.insert(asset_id.clone());
4109                }
4110            }
4111            _ => {}
4112        }
4113    }
4114
4115    DeltaWindow {
4116        changed_gene_ids,
4117        changed_capsule_ids,
4118        changed_mutation_ids,
4119    }
4120}
4121
4122fn import_remote_envelope_into_store(
4123    store: &dyn EvolutionStore,
4124    envelope: &EvolutionEnvelope,
4125    remote_publishers: Option<&Mutex<BTreeMap<String, String>>>,
4126    requested_cursor: Option<String>,
4127) -> Result<ImportOutcome, EvoKernelError> {
4128    if !envelope.verify_content_hash() {
4129        record_manifest_validation(store, envelope, false, "invalid evolution envelope hash")?;
4130        return Err(EvoKernelError::Validation(
4131            "invalid evolution envelope hash".into(),
4132        ));
4133    }
4134    if let Err(reason) = envelope.verify_manifest() {
4135        record_manifest_validation(
4136            store,
4137            envelope,
4138            false,
4139            format!("manifest validation failed: {reason}"),
4140        )?;
4141        return Err(EvoKernelError::Validation(format!(
4142            "invalid evolution envelope manifest: {reason}"
4143        )));
4144    }
4145    record_manifest_validation(store, envelope, true, "manifest validated")?;
4146
4147    let sender_id = normalized_sender_id(&envelope.sender_id);
4148    let (events, projection) = scan_projection(store)?;
4149    let mut known_gene_ids = projection
4150        .genes
4151        .into_iter()
4152        .map(|gene| gene.id)
4153        .collect::<BTreeSet<_>>();
4154    let mut known_capsule_ids = projection
4155        .capsules
4156        .into_iter()
4157        .map(|capsule| capsule.id)
4158        .collect::<BTreeSet<_>>();
4159    let mut known_mutation_ids = BTreeSet::new();
4160    let mut known_spec_links = BTreeSet::new();
4161    for stored in &events {
4162        match &stored.event {
4163            EvolutionEvent::MutationDeclared { mutation } => {
4164                known_mutation_ids.insert(mutation.intent.id.clone());
4165            }
4166            EvolutionEvent::SpecLinked {
4167                mutation_id,
4168                spec_id,
4169            } => {
4170                known_spec_links.insert((mutation_id.clone(), spec_id.clone()));
4171            }
4172            _ => {}
4173        }
4174    }
4175    let mut imported_asset_ids = Vec::new();
4176    let mut applied_count = 0usize;
4177    let mut skipped_count = 0usize;
4178    for asset in &envelope.assets {
4179        match asset {
4180            NetworkAsset::Gene { gene } => {
4181                if !known_gene_ids.insert(gene.id.clone()) {
4182                    skipped_count += 1;
4183                    continue;
4184                }
4185                imported_asset_ids.push(gene.id.clone());
4186                applied_count += 1;
4187                let mut quarantined_gene = gene.clone();
4188                quarantined_gene.state = AssetState::Quarantined;
4189                store
4190                    .append_event(EvolutionEvent::RemoteAssetImported {
4191                        source: CandidateSource::Remote,
4192                        asset_ids: vec![gene.id.clone()],
4193                        sender_id: sender_id.clone(),
4194                    })
4195                    .map_err(store_err)?;
4196                store
4197                    .append_event(EvolutionEvent::GeneProjected {
4198                        gene: quarantined_gene.clone(),
4199                    })
4200                    .map_err(store_err)?;
4201                record_remote_publisher_for_asset(remote_publishers, &envelope.sender_id, asset);
4202                store
4203                    .append_event(EvolutionEvent::PromotionEvaluated {
4204                        gene_id: quarantined_gene.id,
4205                        state: AssetState::Quarantined,
4206                        reason: "remote asset requires local validation before promotion".into(),
4207                        reason_code: TransitionReasonCode::DowngradeRemoteRequiresLocalValidation,
4208                        evidence: Some(TransitionEvidence {
4209                            replay_attempts: None,
4210                            replay_successes: None,
4211                            replay_success_rate: None,
4212                            environment_match_factor: None,
4213                            decayed_confidence: None,
4214                            confidence_decay_ratio: None,
4215                            summary: Some("phase=remote_import; source=remote; action=quarantine_before_shadow_validation".into()),
4216                        }),
4217                    })
4218                    .map_err(store_err)?;
4219            }
4220            NetworkAsset::Capsule { capsule } => {
4221                if !known_capsule_ids.insert(capsule.id.clone()) {
4222                    skipped_count += 1;
4223                    continue;
4224                }
4225                imported_asset_ids.push(capsule.id.clone());
4226                applied_count += 1;
4227                store
4228                    .append_event(EvolutionEvent::RemoteAssetImported {
4229                        source: CandidateSource::Remote,
4230                        asset_ids: vec![capsule.id.clone()],
4231                        sender_id: sender_id.clone(),
4232                    })
4233                    .map_err(store_err)?;
4234                let mut quarantined = capsule.clone();
4235                quarantined.state = AssetState::Quarantined;
4236                store
4237                    .append_event(EvolutionEvent::CapsuleCommitted {
4238                        capsule: quarantined.clone(),
4239                    })
4240                    .map_err(store_err)?;
4241                record_remote_publisher_for_asset(remote_publishers, &envelope.sender_id, asset);
4242                store
4243                    .append_event(EvolutionEvent::CapsuleQuarantined {
4244                        capsule_id: quarantined.id,
4245                    })
4246                    .map_err(store_err)?;
4247            }
4248            NetworkAsset::EvolutionEvent { event } => {
4249                let should_append = match event {
4250                    EvolutionEvent::MutationDeclared { mutation } => {
4251                        known_mutation_ids.insert(mutation.intent.id.clone())
4252                    }
4253                    EvolutionEvent::SpecLinked {
4254                        mutation_id,
4255                        spec_id,
4256                    } => known_spec_links.insert((mutation_id.clone(), spec_id.clone())),
4257                    _ if should_import_remote_event(event) => true,
4258                    _ => false,
4259                };
4260                if should_append {
4261                    store.append_event(event.clone()).map_err(store_err)?;
4262                    applied_count += 1;
4263                } else {
4264                    skipped_count += 1;
4265                }
4266            }
4267        }
4268    }
4269    let next_cursor = latest_store_cursor(store)?;
4270    let resume_token = next_cursor.as_ref().and_then(|cursor| {
4271        normalized_sender_id(&envelope.sender_id).map(|sender| encode_resume_token(&sender, cursor))
4272    });
4273
4274    Ok(ImportOutcome {
4275        imported_asset_ids,
4276        accepted: true,
4277        next_cursor: next_cursor.clone(),
4278        resume_token,
4279        sync_audit: SyncAudit {
4280            batch_id: next_id("sync-import"),
4281            requested_cursor,
4282            scanned_count: envelope.assets.len(),
4283            applied_count,
4284            skipped_count,
4285            failed_count: 0,
4286            failure_reasons: Vec::new(),
4287        },
4288    })
4289}
4290
4291const EVOMAP_SNAPSHOT_ROOT: &str = "assets/gep/evomap_snapshot";
4292const EVOMAP_SNAPSHOT_GENES_FILE: &str = "genes.json";
4293const EVOMAP_SNAPSHOT_CAPSULES_FILE: &str = "capsules.json";
4294const EVOMAP_BUILTIN_RUN_ID: &str = "builtin-evomap-seed";
4295
4296#[derive(Debug, Deserialize)]
4297struct EvoMapGeneDocument {
4298    #[serde(default)]
4299    genes: Vec<EvoMapGeneAsset>,
4300}
4301
4302#[derive(Debug, Deserialize)]
4303struct EvoMapGeneAsset {
4304    id: String,
4305    #[serde(default)]
4306    category: Option<String>,
4307    #[serde(default)]
4308    signals_match: Vec<Value>,
4309    #[serde(default)]
4310    strategy: Vec<String>,
4311    #[serde(default)]
4312    validation: Vec<String>,
4313    #[serde(default)]
4314    constraints: Option<EvoMapConstraintAsset>,
4315    #[serde(default)]
4316    model_name: Option<String>,
4317    #[serde(default)]
4318    schema_version: Option<String>,
4319    #[serde(default)]
4320    compatibility: Option<Value>,
4321}
4322
4323#[derive(Clone, Debug, Deserialize, Default)]
4324struct EvoMapConstraintAsset {
4325    #[serde(default)]
4326    max_files: Option<usize>,
4327    #[serde(default)]
4328    forbidden_paths: Vec<String>,
4329}
4330
4331#[derive(Debug, Deserialize)]
4332struct EvoMapCapsuleDocument {
4333    #[serde(default)]
4334    capsules: Vec<EvoMapCapsuleAsset>,
4335}
4336
4337#[derive(Debug, Deserialize)]
4338struct EvoMapCapsuleAsset {
4339    id: String,
4340    gene: String,
4341    #[serde(default)]
4342    trigger: Vec<String>,
4343    #[serde(default)]
4344    summary: String,
4345    #[serde(default)]
4346    diff: Option<String>,
4347    #[serde(default)]
4348    confidence: Option<f32>,
4349    #[serde(default)]
4350    outcome: Option<EvoMapOutcomeAsset>,
4351    #[serde(default)]
4352    blast_radius: Option<EvoMapBlastRadiusAsset>,
4353    #[serde(default)]
4354    content: Option<EvoMapCapsuleContentAsset>,
4355    #[serde(default)]
4356    env_fingerprint: Option<Value>,
4357    #[serde(default)]
4358    model_name: Option<String>,
4359    #[serde(default)]
4360    schema_version: Option<String>,
4361    #[serde(default)]
4362    compatibility: Option<Value>,
4363}
4364
4365#[derive(Clone, Debug, Deserialize, Default)]
4366struct EvoMapOutcomeAsset {
4367    #[serde(default)]
4368    status: Option<String>,
4369    #[serde(default)]
4370    score: Option<f32>,
4371}
4372
4373#[derive(Clone, Debug, Deserialize, Default)]
4374struct EvoMapBlastRadiusAsset {
4375    #[serde(default)]
4376    lines: usize,
4377}
4378
4379#[derive(Clone, Debug, Deserialize, Default)]
4380struct EvoMapCapsuleContentAsset {
4381    #[serde(default)]
4382    changed_files: Vec<String>,
4383}
4384
4385#[derive(Debug)]
4386struct BuiltinCapsuleSeed {
4387    capsule: Capsule,
4388    mutation: PreparedMutation,
4389}
4390
4391#[derive(Debug)]
4392struct BuiltinAssetBundle {
4393    genes: Vec<Gene>,
4394    capsules: Vec<BuiltinCapsuleSeed>,
4395}
4396
4397fn built_in_experience_genes() -> Vec<Gene> {
4398    vec![
4399        Gene {
4400            id: "builtin-experience-docs-rewrite-v1".into(),
4401            signals: vec!["docs.rewrite".into(), "docs".into(), "rewrite".into()],
4402            strategy: vec![
4403                "asset_origin=builtin".into(),
4404                "task_class=docs.rewrite".into(),
4405                "task_label=Docs rewrite".into(),
4406                "template_id=builtin-docs-rewrite-v1".into(),
4407                "summary=baseline docs rewrite experience".into(),
4408            ],
4409            validation: vec!["builtin-template".into(), "origin=builtin".into()],
4410            state: AssetState::Promoted,
4411        },
4412        Gene {
4413            id: "builtin-experience-ci-fix-v1".into(),
4414            signals: vec![
4415                "ci.fix".into(),
4416                "ci".into(),
4417                "test".into(),
4418                "failure".into(),
4419            ],
4420            strategy: vec![
4421                "asset_origin=builtin".into(),
4422                "task_class=ci.fix".into(),
4423                "task_label=CI fix".into(),
4424                "template_id=builtin-ci-fix-v1".into(),
4425                "summary=baseline ci stabilization experience".into(),
4426            ],
4427            validation: vec!["builtin-template".into(), "origin=builtin".into()],
4428            state: AssetState::Promoted,
4429        },
4430        Gene {
4431            id: "builtin-experience-task-decomposition-v1".into(),
4432            signals: vec![
4433                "task.decomposition".into(),
4434                "task".into(),
4435                "decomposition".into(),
4436                "planning".into(),
4437            ],
4438            strategy: vec![
4439                "asset_origin=builtin".into(),
4440                "task_class=task.decomposition".into(),
4441                "task_label=Task decomposition".into(),
4442                "template_id=builtin-task-decomposition-v1".into(),
4443                "summary=baseline task decomposition and routing experience".into(),
4444            ],
4445            validation: vec!["builtin-template".into(), "origin=builtin".into()],
4446            state: AssetState::Promoted,
4447        },
4448        Gene {
4449            id: "builtin-experience-project-workflow-v1".into(),
4450            signals: vec![
4451                "project.workflow".into(),
4452                "project".into(),
4453                "workflow".into(),
4454                "milestone".into(),
4455            ],
4456            strategy: vec![
4457                "asset_origin=builtin".into(),
4458                "task_class=project.workflow".into(),
4459                "task_label=Project workflow".into(),
4460                "template_id=builtin-project-workflow-v1".into(),
4461                "summary=baseline project proposal and merge workflow experience".into(),
4462            ],
4463            validation: vec!["builtin-template".into(), "origin=builtin".into()],
4464            state: AssetState::Promoted,
4465        },
4466        Gene {
4467            id: "builtin-experience-service-bid-v1".into(),
4468            signals: vec![
4469                "service.bid".into(),
4470                "service".into(),
4471                "bid".into(),
4472                "economics".into(),
4473            ],
4474            strategy: vec![
4475                "asset_origin=builtin".into(),
4476                "task_class=service.bid".into(),
4477                "task_label=Service bid".into(),
4478                "template_id=builtin-service-bid-v1".into(),
4479                "summary=baseline service bidding and settlement experience".into(),
4480            ],
4481            validation: vec!["builtin-template".into(), "origin=builtin".into()],
4482            state: AssetState::Promoted,
4483        },
4484    ]
4485}
4486
4487fn evomap_snapshot_path(file_name: &str) -> PathBuf {
4488    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
4489        .join(EVOMAP_SNAPSHOT_ROOT)
4490        .join(file_name)
4491}
4492
4493fn read_evomap_snapshot(file_name: &str) -> Result<Option<String>, EvoKernelError> {
4494    let path = evomap_snapshot_path(file_name);
4495    if !path.exists() {
4496        return Ok(None);
4497    }
4498    fs::read_to_string(&path).map(Some).map_err(|err| {
4499        EvoKernelError::Validation(format!(
4500            "failed to read EvoMap snapshot {}: {err}",
4501            path.display()
4502        ))
4503    })
4504}
4505
4506fn compatibility_state_from_value(value: Option<&Value>) -> Option<String> {
4507    let value = value?;
4508    if let Some(state) = value.as_str() {
4509        let normalized = state.trim().to_ascii_lowercase();
4510        if normalized.is_empty() {
4511            return None;
4512        }
4513        return Some(normalized);
4514    }
4515    value
4516        .get("state")
4517        .and_then(Value::as_str)
4518        .map(str::trim)
4519        .filter(|state| !state.is_empty())
4520        .map(|state| state.to_ascii_lowercase())
4521}
4522
4523fn map_evomap_state(value: Option<&Value>) -> AssetState {
4524    match compatibility_state_from_value(value).as_deref() {
4525        Some("promoted") => AssetState::Promoted,
4526        Some("candidate") => AssetState::Candidate,
4527        Some("quarantined") => AssetState::Quarantined,
4528        Some("shadow_validated") => AssetState::ShadowValidated,
4529        Some("revoked") => AssetState::Revoked,
4530        Some("rejected") => AssetState::Archived,
4531        Some("archived") => AssetState::Archived,
4532        _ => AssetState::Candidate,
4533    }
4534}
4535
4536fn value_as_signal_string(value: &Value) -> Option<String> {
4537    match value {
4538        Value::String(raw) => {
4539            let normalized = raw.trim();
4540            if normalized.is_empty() {
4541                None
4542            } else {
4543                Some(normalized.to_string())
4544            }
4545        }
4546        Value::Object(_) => {
4547            let serialized = serde_json::to_string(value).ok()?;
4548            let normalized = serialized.trim();
4549            if normalized.is_empty() {
4550                None
4551            } else {
4552                Some(normalized.to_string())
4553            }
4554        }
4555        Value::Null => None,
4556        other => {
4557            let rendered = other.to_string();
4558            let normalized = rendered.trim();
4559            if normalized.is_empty() {
4560                None
4561            } else {
4562                Some(normalized.to_string())
4563            }
4564        }
4565    }
4566}
4567
4568fn parse_diff_changed_files(payload: &str) -> Vec<String> {
4569    let mut changed_files = BTreeSet::new();
4570    for line in payload.lines() {
4571        let line = line.trim();
4572        if let Some(path) = line.strip_prefix("+++ b/") {
4573            let path = path.trim();
4574            if !path.is_empty() && path != "/dev/null" {
4575                changed_files.insert(path.to_string());
4576            }
4577            continue;
4578        }
4579        if let Some(path) = line.strip_prefix("diff --git a/") {
4580            if let Some((_, right)) = path.split_once(" b/") {
4581                let right = right.trim();
4582                if !right.is_empty() {
4583                    changed_files.insert(right.to_string());
4584                }
4585            }
4586        }
4587    }
4588    changed_files.into_iter().collect()
4589}
4590
4591fn strip_diff_code_fence(payload: &str) -> String {
4592    let trimmed = payload.trim();
4593    if !trimmed.starts_with("```") {
4594        return trimmed.to_string();
4595    }
4596    let mut lines = trimmed.lines().collect::<Vec<_>>();
4597    if lines.is_empty() {
4598        return String::new();
4599    }
4600    lines.remove(0);
4601    if lines
4602        .last()
4603        .map(|line| line.trim() == "```")
4604        .unwrap_or(false)
4605    {
4606        lines.pop();
4607    }
4608    lines.join("\n").trim().to_string()
4609}
4610
4611fn synthetic_diff_for_capsule(capsule: &EvoMapCapsuleAsset) -> String {
4612    let file_path = format!("docs/evomap_builtin_capsules/{}.md", capsule.id);
4613    let mut content = Vec::new();
4614    content.push(format!("# EvoMap Builtin Capsule {}", capsule.id));
4615    if capsule.summary.trim().is_empty() {
4616        content.push("summary: missing".to_string());
4617    } else {
4618        content.push(format!("summary: {}", capsule.summary.trim()));
4619    }
4620    if !capsule.trigger.is_empty() {
4621        content.push(format!("trigger: {}", capsule.trigger.join(", ")));
4622    }
4623    content.push(format!("gene: {}", capsule.gene));
4624    let added = content
4625        .into_iter()
4626        .map(|line| format!("+{}", line.replace('\r', "")))
4627        .collect::<Vec<_>>()
4628        .join("\n");
4629    format!(
4630        "diff --git a/{file_path} b/{file_path}\nnew file mode 100644\nindex 0000000..1111111\n--- /dev/null\n+++ b/{file_path}\n@@ -0,0 +1,{line_count} @@\n{added}\n",
4631        line_count = added.lines().count()
4632    )
4633}
4634
4635fn normalized_diff_payload(capsule: &EvoMapCapsuleAsset) -> String {
4636    if let Some(raw) = capsule.diff.as_deref() {
4637        let normalized = strip_diff_code_fence(raw);
4638        if !normalized.trim().is_empty() {
4639            return normalized;
4640        }
4641    }
4642    synthetic_diff_for_capsule(capsule)
4643}
4644
4645fn env_field(value: Option<&Value>, keys: &[&str]) -> Option<String> {
4646    let object = value?.as_object()?;
4647    keys.iter().find_map(|key| {
4648        object
4649            .get(*key)
4650            .and_then(Value::as_str)
4651            .map(str::trim)
4652            .filter(|value| !value.is_empty())
4653            .map(|value| value.to_string())
4654    })
4655}
4656
4657fn map_evomap_env_fingerprint(value: Option<&Value>) -> EnvFingerprint {
4658    let os =
4659        env_field(value, &["os", "platform", "os_release"]).unwrap_or_else(|| "unknown".into());
4660    let target_triple = env_field(value, &["target_triple"]).unwrap_or_else(|| {
4661        let arch = env_field(value, &["arch"]).unwrap_or_else(|| "unknown".into());
4662        format!("{arch}-unknown-{os}")
4663    });
4664    EnvFingerprint {
4665        rustc_version: env_field(value, &["runtime", "rustc_version", "node_version"])
4666            .unwrap_or_else(|| "unknown".into()),
4667        cargo_lock_hash: env_field(value, &["cargo_lock_hash"]).unwrap_or_else(|| "unknown".into()),
4668        target_triple,
4669        os,
4670    }
4671}
4672
4673fn load_evomap_builtin_assets() -> Result<Option<BuiltinAssetBundle>, EvoKernelError> {
4674    let genes_raw = read_evomap_snapshot(EVOMAP_SNAPSHOT_GENES_FILE)?;
4675    let capsules_raw = read_evomap_snapshot(EVOMAP_SNAPSHOT_CAPSULES_FILE)?;
4676    let (Some(genes_raw), Some(capsules_raw)) = (genes_raw, capsules_raw) else {
4677        return Ok(None);
4678    };
4679
4680    let genes_doc: EvoMapGeneDocument = serde_json::from_str(&genes_raw).map_err(|err| {
4681        EvoKernelError::Validation(format!("failed to parse EvoMap genes snapshot: {err}"))
4682    })?;
4683    let capsules_doc: EvoMapCapsuleDocument =
4684        serde_json::from_str(&capsules_raw).map_err(|err| {
4685            EvoKernelError::Validation(format!("failed to parse EvoMap capsules snapshot: {err}"))
4686        })?;
4687
4688    let mut genes = Vec::new();
4689    let mut known_gene_ids = BTreeSet::new();
4690    for source in genes_doc.genes {
4691        let EvoMapGeneAsset {
4692            id,
4693            category,
4694            signals_match,
4695            strategy,
4696            validation,
4697            constraints,
4698            model_name,
4699            schema_version,
4700            compatibility,
4701        } = source;
4702        let gene_id = id.trim();
4703        if gene_id.is_empty() {
4704            return Err(EvoKernelError::Validation(
4705                "EvoMap snapshot gene id must not be empty".into(),
4706            ));
4707        }
4708        if !known_gene_ids.insert(gene_id.to_string()) {
4709            continue;
4710        }
4711
4712        let mut seen_signals = BTreeSet::new();
4713        let mut signals = Vec::new();
4714        for signal in signals_match {
4715            let Some(normalized) = value_as_signal_string(&signal) else {
4716                continue;
4717            };
4718            if seen_signals.insert(normalized.clone()) {
4719                signals.push(normalized);
4720            }
4721        }
4722        if signals.is_empty() {
4723            signals.push(format!("gene:{}", gene_id.to_ascii_lowercase()));
4724        }
4725
4726        let mut strategy = strategy
4727            .into_iter()
4728            .map(|item| item.trim().to_string())
4729            .filter(|item| !item.is_empty())
4730            .collect::<Vec<_>>();
4731        if strategy.is_empty() {
4732            strategy.push("evomap strategy missing in snapshot".into());
4733        }
4734        let constraint = constraints.unwrap_or_default();
4735        let compat_state = compatibility_state_from_value(compatibility.as_ref())
4736            .unwrap_or_else(|| "candidate".to_string());
4737        ensure_strategy_metadata(&mut strategy, "asset_origin", "builtin_evomap");
4738        ensure_strategy_metadata(
4739            &mut strategy,
4740            "evomap_category",
4741            category.as_deref().unwrap_or("unknown"),
4742        );
4743        ensure_strategy_metadata(
4744            &mut strategy,
4745            "evomap_constraints_max_files",
4746            &constraint.max_files.unwrap_or_default().to_string(),
4747        );
4748        ensure_strategy_metadata(
4749            &mut strategy,
4750            "evomap_constraints_forbidden_paths",
4751            &constraint.forbidden_paths.join("|"),
4752        );
4753        ensure_strategy_metadata(
4754            &mut strategy,
4755            "evomap_model_name",
4756            model_name.as_deref().unwrap_or("unknown"),
4757        );
4758        ensure_strategy_metadata(
4759            &mut strategy,
4760            "evomap_schema_version",
4761            schema_version.as_deref().unwrap_or("1.5.0"),
4762        );
4763        ensure_strategy_metadata(&mut strategy, "evomap_compatibility_state", &compat_state);
4764
4765        let mut validation = validation
4766            .into_iter()
4767            .map(|item| item.trim().to_string())
4768            .filter(|item| !item.is_empty())
4769            .collect::<Vec<_>>();
4770        if validation.is_empty() {
4771            validation.push("evomap-builtin-seed".into());
4772        }
4773
4774        genes.push(Gene {
4775            id: gene_id.to_string(),
4776            signals,
4777            strategy,
4778            validation,
4779            state: map_evomap_state(compatibility.as_ref()),
4780        });
4781    }
4782
4783    let mut capsules = Vec::new();
4784    let known_gene_ids = genes
4785        .iter()
4786        .map(|gene| gene.id.clone())
4787        .collect::<BTreeSet<_>>();
4788    for source in capsules_doc.capsules {
4789        let EvoMapCapsuleAsset {
4790            id,
4791            gene,
4792            trigger,
4793            summary,
4794            diff,
4795            confidence,
4796            outcome,
4797            blast_radius,
4798            content,
4799            env_fingerprint,
4800            model_name: _model_name,
4801            schema_version: _schema_version,
4802            compatibility,
4803        } = source;
4804        let source_for_diff = EvoMapCapsuleAsset {
4805            id: id.clone(),
4806            gene: gene.clone(),
4807            trigger: trigger.clone(),
4808            summary: summary.clone(),
4809            diff,
4810            confidence,
4811            outcome: outcome.clone(),
4812            blast_radius: blast_radius.clone(),
4813            content: content.clone(),
4814            env_fingerprint: env_fingerprint.clone(),
4815            model_name: None,
4816            schema_version: None,
4817            compatibility: compatibility.clone(),
4818        };
4819        if !known_gene_ids.contains(gene.as_str()) {
4820            return Err(EvoKernelError::Validation(format!(
4821                "EvoMap capsule {} references unknown gene {}",
4822                id, gene
4823            )));
4824        }
4825        let normalized_diff = normalized_diff_payload(&source_for_diff);
4826        if normalized_diff.trim().is_empty() {
4827            return Err(EvoKernelError::Validation(format!(
4828                "EvoMap capsule {} has empty normalized diff payload",
4829                id
4830            )));
4831        }
4832        let mut changed_files = content
4833            .as_ref()
4834            .map(|content| {
4835                content
4836                    .changed_files
4837                    .iter()
4838                    .map(|item| item.trim().to_string())
4839                    .filter(|item| !item.is_empty())
4840                    .collect::<Vec<_>>()
4841            })
4842            .unwrap_or_default();
4843        if changed_files.is_empty() {
4844            changed_files = parse_diff_changed_files(&normalized_diff);
4845        }
4846        if changed_files.is_empty() {
4847            changed_files.push(format!("docs/evomap_builtin_capsules/{}.md", id));
4848        }
4849
4850        let confidence = confidence
4851            .or_else(|| outcome.as_ref().and_then(|outcome| outcome.score))
4852            .unwrap_or(0.6)
4853            .clamp(0.0, 1.0);
4854        let status_success = outcome
4855            .as_ref()
4856            .and_then(|outcome| outcome.status.as_deref())
4857            .map(|status| status.eq_ignore_ascii_case("success"))
4858            .unwrap_or(true);
4859        let blast_radius = blast_radius.unwrap_or_default();
4860        let mutation_id = format!("builtin-evomap-mutation-{}", id);
4861        let intent = MutationIntent {
4862            id: mutation_id.clone(),
4863            intent: if summary.trim().is_empty() {
4864                format!("apply EvoMap capsule {}", id)
4865            } else {
4866                summary.trim().to_string()
4867            },
4868            target: MutationTarget::Paths {
4869                allow: changed_files.clone(),
4870            },
4871            expected_effect: format!("seed replay candidate from EvoMap capsule {}", id),
4872            risk: RiskLevel::Low,
4873            signals: if trigger.is_empty() {
4874                vec![format!("capsule:{}", id.to_ascii_lowercase())]
4875            } else {
4876                trigger
4877                    .iter()
4878                    .map(|signal| signal.trim().to_ascii_lowercase())
4879                    .filter(|signal| !signal.is_empty())
4880                    .collect::<Vec<_>>()
4881            },
4882            spec_id: None,
4883        };
4884        let mutation = PreparedMutation {
4885            intent,
4886            artifact: oris_evolution::MutationArtifact {
4887                encoding: ArtifactEncoding::UnifiedDiff,
4888                payload: normalized_diff.clone(),
4889                base_revision: None,
4890                content_hash: compute_artifact_hash(&normalized_diff),
4891            },
4892        };
4893        let capsule = Capsule {
4894            id: id.clone(),
4895            gene_id: gene.clone(),
4896            mutation_id,
4897            run_id: EVOMAP_BUILTIN_RUN_ID.to_string(),
4898            diff_hash: compute_artifact_hash(&normalized_diff),
4899            confidence,
4900            env: map_evomap_env_fingerprint(env_fingerprint.as_ref()),
4901            outcome: Outcome {
4902                success: status_success,
4903                validation_profile: "evomap-builtin-seed".into(),
4904                validation_duration_ms: 0,
4905                changed_files,
4906                validator_hash: "builtin-evomap".into(),
4907                lines_changed: blast_radius.lines,
4908                replay_verified: false,
4909            },
4910            state: map_evomap_state(compatibility.as_ref()),
4911        };
4912        capsules.push(BuiltinCapsuleSeed { capsule, mutation });
4913    }
4914
4915    Ok(Some(BuiltinAssetBundle { genes, capsules }))
4916}
4917
4918fn ensure_builtin_experience_assets_in_store(
4919    store: &dyn EvolutionStore,
4920    sender_id: String,
4921) -> Result<ImportOutcome, EvoKernelError> {
4922    let (events, projection) = scan_projection(store)?;
4923    let mut known_gene_ids = projection
4924        .genes
4925        .into_iter()
4926        .map(|gene| gene.id)
4927        .collect::<BTreeSet<_>>();
4928    let mut known_capsule_ids = projection
4929        .capsules
4930        .into_iter()
4931        .map(|capsule| capsule.id)
4932        .collect::<BTreeSet<_>>();
4933    let mut known_mutation_ids = BTreeSet::new();
4934    for stored in &events {
4935        if let EvolutionEvent::MutationDeclared { mutation } = &stored.event {
4936            known_mutation_ids.insert(mutation.intent.id.clone());
4937        }
4938    }
4939    let normalized_sender = normalized_sender_id(&sender_id);
4940    let mut imported_asset_ids = Vec::new();
4941    // Keep legacy compatibility templates available even when EvoMap snapshots
4942    // are present, so A2A compatibility fetch flows retain stable builtin IDs.
4943    let mut bundle = BuiltinAssetBundle {
4944        genes: built_in_experience_genes(),
4945        capsules: Vec::new(),
4946    };
4947    if let Some(snapshot_bundle) = load_evomap_builtin_assets()? {
4948        bundle.genes.extend(snapshot_bundle.genes);
4949        bundle.capsules.extend(snapshot_bundle.capsules);
4950    }
4951    let scanned_count = bundle.genes.len() + bundle.capsules.len();
4952
4953    for gene in bundle.genes {
4954        if !known_gene_ids.insert(gene.id.clone()) {
4955            continue;
4956        }
4957
4958        store
4959            .append_event(EvolutionEvent::RemoteAssetImported {
4960                source: CandidateSource::Local,
4961                asset_ids: vec![gene.id.clone()],
4962                sender_id: normalized_sender.clone(),
4963            })
4964            .map_err(store_err)?;
4965        store
4966            .append_event(EvolutionEvent::GeneProjected { gene: gene.clone() })
4967            .map_err(store_err)?;
4968        match gene.state {
4969            AssetState::Revoked | AssetState::Archived => {}
4970            AssetState::Quarantined | AssetState::ShadowValidated => {
4971                store
4972                    .append_event(EvolutionEvent::PromotionEvaluated {
4973                        gene_id: gene.id.clone(),
4974                        state: AssetState::Quarantined,
4975                        reason:
4976                            "built-in EvoMap asset requires additional validation before promotion"
4977                                .into(),
4978                        reason_code: TransitionReasonCode::DowngradeBuiltinRequiresValidation,
4979                        evidence: None,
4980                    })
4981                    .map_err(store_err)?;
4982            }
4983            AssetState::Promoted | AssetState::Candidate => {
4984                store
4985                    .append_event(EvolutionEvent::PromotionEvaluated {
4986                        gene_id: gene.id.clone(),
4987                        state: AssetState::Promoted,
4988                        reason: "built-in experience asset promoted for cold-start compatibility"
4989                            .into(),
4990                        reason_code: TransitionReasonCode::PromotionBuiltinColdStartCompatibility,
4991                        evidence: None,
4992                    })
4993                    .map_err(store_err)?;
4994                store
4995                    .append_event(EvolutionEvent::GenePromoted {
4996                        gene_id: gene.id.clone(),
4997                    })
4998                    .map_err(store_err)?;
4999            }
5000        }
5001        imported_asset_ids.push(gene.id.clone());
5002    }
5003
5004    for seed in bundle.capsules {
5005        if !known_gene_ids.contains(seed.capsule.gene_id.as_str()) {
5006            return Err(EvoKernelError::Validation(format!(
5007                "built-in capsule {} references unknown gene {}",
5008                seed.capsule.id, seed.capsule.gene_id
5009            )));
5010        }
5011        if known_mutation_ids.insert(seed.mutation.intent.id.clone()) {
5012            store
5013                .append_event(EvolutionEvent::MutationDeclared {
5014                    mutation: seed.mutation.clone(),
5015                })
5016                .map_err(store_err)?;
5017        }
5018        if !known_capsule_ids.insert(seed.capsule.id.clone()) {
5019            continue;
5020        }
5021        store
5022            .append_event(EvolutionEvent::RemoteAssetImported {
5023                source: CandidateSource::Local,
5024                asset_ids: vec![seed.capsule.id.clone()],
5025                sender_id: normalized_sender.clone(),
5026            })
5027            .map_err(store_err)?;
5028        store
5029            .append_event(EvolutionEvent::CapsuleCommitted {
5030                capsule: seed.capsule.clone(),
5031            })
5032            .map_err(store_err)?;
5033        match seed.capsule.state {
5034            AssetState::Revoked | AssetState::Archived => {}
5035            AssetState::Quarantined | AssetState::ShadowValidated => {
5036                store
5037                    .append_event(EvolutionEvent::CapsuleQuarantined {
5038                        capsule_id: seed.capsule.id.clone(),
5039                    })
5040                    .map_err(store_err)?;
5041            }
5042            AssetState::Promoted | AssetState::Candidate => {
5043                store
5044                    .append_event(EvolutionEvent::CapsuleReleased {
5045                        capsule_id: seed.capsule.id.clone(),
5046                        state: AssetState::Promoted,
5047                    })
5048                    .map_err(store_err)?;
5049            }
5050        }
5051        imported_asset_ids.push(seed.capsule.id.clone());
5052    }
5053
5054    let next_cursor = latest_store_cursor(store)?;
5055    let resume_token = next_cursor.as_ref().and_then(|cursor| {
5056        normalized_sender
5057            .as_deref()
5058            .map(|sender| encode_resume_token(sender, cursor))
5059    });
5060    let applied_count = imported_asset_ids.len();
5061    let skipped_count = scanned_count.saturating_sub(applied_count);
5062
5063    Ok(ImportOutcome {
5064        imported_asset_ids,
5065        accepted: true,
5066        next_cursor: next_cursor.clone(),
5067        resume_token,
5068        sync_audit: SyncAudit {
5069            batch_id: next_id("sync-import"),
5070            requested_cursor: None,
5071            scanned_count,
5072            applied_count,
5073            skipped_count,
5074            failed_count: 0,
5075            failure_reasons: Vec::new(),
5076        },
5077    })
5078}
5079
5080fn strategy_metadata_value(strategy: &[String], key: &str) -> Option<String> {
5081    strategy.iter().find_map(|entry| {
5082        let (entry_key, entry_value) = entry.split_once('=')?;
5083        if entry_key.trim().eq_ignore_ascii_case(key) {
5084            let normalized = entry_value.trim();
5085            if normalized.is_empty() {
5086                None
5087            } else {
5088                Some(normalized.to_string())
5089            }
5090        } else {
5091            None
5092        }
5093    })
5094}
5095
5096fn ensure_strategy_metadata(strategy: &mut Vec<String>, key: &str, value: &str) {
5097    let normalized = value.trim();
5098    if normalized.is_empty() || strategy_metadata_value(strategy, key).is_some() {
5099        return;
5100    }
5101    strategy.push(format!("{key}={normalized}"));
5102}
5103
5104fn enforce_reported_experience_retention(
5105    store: &dyn EvolutionStore,
5106    task_class: &str,
5107    keep_latest: usize,
5108) -> Result<(), EvoKernelError> {
5109    let task_class = task_class.trim();
5110    if task_class.is_empty() || keep_latest == 0 {
5111        return Ok(());
5112    }
5113
5114    let (_, projection) = scan_projection(store)?;
5115    let mut candidates = projection
5116        .genes
5117        .iter()
5118        .filter(|gene| gene.state == AssetState::Promoted)
5119        .filter_map(|gene| {
5120            let origin = strategy_metadata_value(&gene.strategy, "asset_origin")?;
5121            if !origin.eq_ignore_ascii_case("reported_experience") {
5122                return None;
5123            }
5124            let gene_task_class = strategy_metadata_value(&gene.strategy, "task_class")?;
5125            if !gene_task_class.eq_ignore_ascii_case(task_class) {
5126                return None;
5127            }
5128            let updated_at = projection
5129                .last_updated_at
5130                .get(&gene.id)
5131                .cloned()
5132                .unwrap_or_default();
5133            Some((gene.id.clone(), updated_at))
5134        })
5135        .collect::<Vec<_>>();
5136    if candidates.len() <= keep_latest {
5137        return Ok(());
5138    }
5139
5140    candidates.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| right.0.cmp(&left.0)));
5141    let stale_gene_ids = candidates
5142        .into_iter()
5143        .skip(keep_latest)
5144        .map(|(gene_id, _)| gene_id)
5145        .collect::<BTreeSet<_>>();
5146    if stale_gene_ids.is_empty() {
5147        return Ok(());
5148    }
5149
5150    let reason =
5151        format!("reported experience retention limit exceeded for task_class={task_class}");
5152    for gene_id in &stale_gene_ids {
5153        store
5154            .append_event(EvolutionEvent::GeneRevoked {
5155                gene_id: gene_id.clone(),
5156                reason: reason.clone(),
5157            })
5158            .map_err(store_err)?;
5159    }
5160
5161    let stale_capsule_ids = projection
5162        .capsules
5163        .iter()
5164        .filter(|capsule| stale_gene_ids.contains(&capsule.gene_id))
5165        .map(|capsule| capsule.id.clone())
5166        .collect::<BTreeSet<_>>();
5167    for capsule_id in stale_capsule_ids {
5168        store
5169            .append_event(EvolutionEvent::CapsuleQuarantined { capsule_id })
5170            .map_err(store_err)?;
5171    }
5172    Ok(())
5173}
5174
5175fn record_reported_experience_in_store(
5176    store: &dyn EvolutionStore,
5177    sender_id: String,
5178    gene_id: String,
5179    signals: Vec<String>,
5180    strategy: Vec<String>,
5181    validation: Vec<String>,
5182) -> Result<ImportOutcome, EvoKernelError> {
5183    let gene_id = gene_id.trim();
5184    if gene_id.is_empty() {
5185        return Err(EvoKernelError::Validation(
5186            "reported experience gene_id must not be empty".into(),
5187        ));
5188    }
5189
5190    let mut unique_signals = BTreeSet::new();
5191    let mut normalized_signals = Vec::new();
5192    for signal in signals {
5193        let normalized = signal.trim().to_ascii_lowercase();
5194        if normalized.is_empty() {
5195            continue;
5196        }
5197        if unique_signals.insert(normalized.clone()) {
5198            normalized_signals.push(normalized);
5199        }
5200    }
5201    if normalized_signals.is_empty() {
5202        return Err(EvoKernelError::Validation(
5203            "reported experience signals must not be empty".into(),
5204        ));
5205    }
5206
5207    let mut unique_strategy = BTreeSet::new();
5208    let mut normalized_strategy = Vec::new();
5209    for entry in strategy {
5210        let normalized = entry.trim().to_string();
5211        if normalized.is_empty() {
5212            continue;
5213        }
5214        if unique_strategy.insert(normalized.clone()) {
5215            normalized_strategy.push(normalized);
5216        }
5217    }
5218    if normalized_strategy.is_empty() {
5219        normalized_strategy.push("reported local replay experience".into());
5220    }
5221    let task_class_id = strategy_metadata_value(&normalized_strategy, "task_class")
5222        .or_else(|| normalized_signals.first().cloned())
5223        .unwrap_or_else(|| "reported-experience".into());
5224    let task_label = strategy_metadata_value(&normalized_strategy, "task_label")
5225        .or_else(|| normalized_signals.first().cloned())
5226        .unwrap_or_else(|| task_class_id.clone());
5227    ensure_strategy_metadata(
5228        &mut normalized_strategy,
5229        "asset_origin",
5230        "reported_experience",
5231    );
5232    ensure_strategy_metadata(&mut normalized_strategy, "task_class", &task_class_id);
5233    ensure_strategy_metadata(&mut normalized_strategy, "task_label", &task_label);
5234
5235    let mut unique_validation = BTreeSet::new();
5236    let mut normalized_validation = Vec::new();
5237    for entry in validation {
5238        let normalized = entry.trim().to_string();
5239        if normalized.is_empty() {
5240            continue;
5241        }
5242        if unique_validation.insert(normalized.clone()) {
5243            normalized_validation.push(normalized);
5244        }
5245    }
5246    if normalized_validation.is_empty() {
5247        normalized_validation.push("a2a.tasks.report".into());
5248    }
5249
5250    let gene = Gene {
5251        id: gene_id.to_string(),
5252        signals: normalized_signals,
5253        strategy: normalized_strategy,
5254        validation: normalized_validation,
5255        state: AssetState::Promoted,
5256    };
5257    let normalized_sender = normalized_sender_id(&sender_id);
5258
5259    store
5260        .append_event(EvolutionEvent::RemoteAssetImported {
5261            source: CandidateSource::Local,
5262            asset_ids: vec![gene.id.clone()],
5263            sender_id: normalized_sender.clone(),
5264        })
5265        .map_err(store_err)?;
5266    store
5267        .append_event(EvolutionEvent::GeneProjected { gene: gene.clone() })
5268        .map_err(store_err)?;
5269    store
5270        .append_event(EvolutionEvent::PromotionEvaluated {
5271            gene_id: gene.id.clone(),
5272            state: AssetState::Promoted,
5273            reason: "trusted local report promoted reusable experience".into(),
5274            reason_code: TransitionReasonCode::PromotionTrustedLocalReport,
5275            evidence: None,
5276        })
5277        .map_err(store_err)?;
5278    store
5279        .append_event(EvolutionEvent::GenePromoted {
5280            gene_id: gene.id.clone(),
5281        })
5282        .map_err(store_err)?;
5283    enforce_reported_experience_retention(
5284        store,
5285        &task_class_id,
5286        REPORTED_EXPERIENCE_RETENTION_LIMIT,
5287    )?;
5288
5289    let imported_asset_ids = vec![gene.id];
5290    let next_cursor = latest_store_cursor(store)?;
5291    let resume_token = next_cursor.as_ref().and_then(|cursor| {
5292        normalized_sender
5293            .as_deref()
5294            .map(|sender| encode_resume_token(sender, cursor))
5295    });
5296    Ok(ImportOutcome {
5297        imported_asset_ids,
5298        accepted: true,
5299        next_cursor,
5300        resume_token,
5301        sync_audit: SyncAudit {
5302            batch_id: next_id("sync-import"),
5303            requested_cursor: None,
5304            scanned_count: 1,
5305            applied_count: 1,
5306            skipped_count: 0,
5307            failed_count: 0,
5308            failure_reasons: Vec::new(),
5309        },
5310    })
5311}
5312
5313fn normalized_sender_id(sender_id: &str) -> Option<String> {
5314    let trimmed = sender_id.trim();
5315    if trimmed.is_empty() {
5316        None
5317    } else {
5318        Some(trimmed.to_string())
5319    }
5320}
5321
5322fn record_manifest_validation(
5323    store: &dyn EvolutionStore,
5324    envelope: &EvolutionEnvelope,
5325    accepted: bool,
5326    reason: impl Into<String>,
5327) -> Result<(), EvoKernelError> {
5328    let manifest = envelope.manifest.as_ref();
5329    let sender_id = manifest
5330        .and_then(|value| normalized_sender_id(&value.sender_id))
5331        .or_else(|| normalized_sender_id(&envelope.sender_id));
5332    let publisher = manifest.and_then(|value| normalized_sender_id(&value.publisher));
5333    let asset_ids = manifest
5334        .map(|value| value.asset_ids.clone())
5335        .unwrap_or_else(|| EvolutionEnvelope::manifest_asset_ids(&envelope.assets));
5336
5337    store
5338        .append_event(EvolutionEvent::ManifestValidated {
5339            accepted,
5340            reason: reason.into(),
5341            sender_id,
5342            publisher,
5343            asset_ids,
5344        })
5345        .map_err(store_err)?;
5346    Ok(())
5347}
5348
5349fn record_remote_publisher_for_asset(
5350    remote_publishers: Option<&Mutex<BTreeMap<String, String>>>,
5351    sender_id: &str,
5352    asset: &NetworkAsset,
5353) {
5354    let Some(remote_publishers) = remote_publishers else {
5355        return;
5356    };
5357    let sender_id = sender_id.trim();
5358    if sender_id.is_empty() {
5359        return;
5360    }
5361    let Ok(mut publishers) = remote_publishers.lock() else {
5362        return;
5363    };
5364    match asset {
5365        NetworkAsset::Gene { gene } => {
5366            publishers.insert(gene.id.clone(), sender_id.to_string());
5367        }
5368        NetworkAsset::Capsule { capsule } => {
5369            publishers.insert(capsule.id.clone(), sender_id.to_string());
5370        }
5371        NetworkAsset::EvolutionEvent { .. } => {}
5372    }
5373}
5374
5375fn remote_publishers_by_asset_from_store(store: &dyn EvolutionStore) -> BTreeMap<String, String> {
5376    let Ok(events) = store.scan(1) else {
5377        return BTreeMap::new();
5378    };
5379    remote_publishers_by_asset_from_events(&events)
5380}
5381
5382fn remote_publishers_by_asset_from_events(
5383    events: &[StoredEvolutionEvent],
5384) -> BTreeMap<String, String> {
5385    let mut imported_asset_publishers = BTreeMap::<String, String>::new();
5386    let mut known_gene_ids = BTreeSet::<String>::new();
5387    let mut known_capsule_ids = BTreeSet::<String>::new();
5388    let mut publishers_by_asset = BTreeMap::<String, String>::new();
5389
5390    for stored in events {
5391        match &stored.event {
5392            EvolutionEvent::RemoteAssetImported {
5393                source: CandidateSource::Remote,
5394                asset_ids,
5395                sender_id,
5396            } => {
5397                let Some(sender_id) = sender_id.as_deref().and_then(normalized_sender_id) else {
5398                    continue;
5399                };
5400                for asset_id in asset_ids {
5401                    imported_asset_publishers.insert(asset_id.clone(), sender_id.clone());
5402                    if known_gene_ids.contains(asset_id) || known_capsule_ids.contains(asset_id) {
5403                        publishers_by_asset.insert(asset_id.clone(), sender_id.clone());
5404                    }
5405                }
5406            }
5407            EvolutionEvent::GeneProjected { gene } => {
5408                known_gene_ids.insert(gene.id.clone());
5409                if let Some(sender_id) = imported_asset_publishers.get(&gene.id) {
5410                    publishers_by_asset.insert(gene.id.clone(), sender_id.clone());
5411                }
5412            }
5413            EvolutionEvent::CapsuleCommitted { capsule } => {
5414                known_capsule_ids.insert(capsule.id.clone());
5415                if let Some(sender_id) = imported_asset_publishers.get(&capsule.id) {
5416                    publishers_by_asset.insert(capsule.id.clone(), sender_id.clone());
5417                }
5418            }
5419            _ => {}
5420        }
5421    }
5422
5423    publishers_by_asset
5424}
5425
5426fn should_import_remote_event(event: &EvolutionEvent) -> bool {
5427    matches!(
5428        event,
5429        EvolutionEvent::MutationDeclared { .. } | EvolutionEvent::SpecLinked { .. }
5430    )
5431}
5432
5433fn fetch_assets_from_store(
5434    store: &dyn EvolutionStore,
5435    responder_id: impl Into<String>,
5436    query: &FetchQuery,
5437) -> Result<FetchResponse, EvoKernelError> {
5438    let (events, projection) = scan_projection(store)?;
5439    let requested_cursor = resolve_requested_cursor(
5440        &query.sender_id,
5441        query.since_cursor.as_deref(),
5442        query.resume_token.as_deref(),
5443    )?;
5444    let since_seq = requested_cursor
5445        .as_deref()
5446        .and_then(parse_sync_cursor_seq)
5447        .unwrap_or(0);
5448    let normalized_signals: Vec<String> = query
5449        .signals
5450        .iter()
5451        .map(|signal| signal.trim().to_ascii_lowercase())
5452        .filter(|signal| !signal.is_empty())
5453        .collect();
5454    let matches_any_signal = |candidate: &str| {
5455        if normalized_signals.is_empty() {
5456            return true;
5457        }
5458        let candidate = candidate.to_ascii_lowercase();
5459        normalized_signals
5460            .iter()
5461            .any(|signal| candidate.contains(signal) || signal.contains(&candidate))
5462    };
5463
5464    let matched_genes: Vec<Gene> = projection
5465        .genes
5466        .into_iter()
5467        .filter(|gene| gene.state == AssetState::Promoted)
5468        .filter(|gene| gene.signals.iter().any(|signal| matches_any_signal(signal)))
5469        .collect();
5470    let matched_gene_ids: BTreeSet<String> =
5471        matched_genes.iter().map(|gene| gene.id.clone()).collect();
5472    let matched_capsules: Vec<Capsule> = projection
5473        .capsules
5474        .into_iter()
5475        .filter(|capsule| capsule.state == AssetState::Promoted)
5476        .filter(|capsule| matched_gene_ids.contains(&capsule.gene_id))
5477        .collect();
5478    let all_assets = replay_export_assets(&events, matched_genes.clone(), matched_capsules.clone());
5479    let (selected_genes, selected_capsules) = if requested_cursor.is_some() {
5480        let delta = delta_window(&events, since_seq);
5481        let selected_capsules = matched_capsules
5482            .into_iter()
5483            .filter(|capsule| {
5484                delta.changed_capsule_ids.contains(&capsule.id)
5485                    || delta.changed_mutation_ids.contains(&capsule.mutation_id)
5486            })
5487            .collect::<Vec<_>>();
5488        let selected_gene_ids = selected_capsules
5489            .iter()
5490            .map(|capsule| capsule.gene_id.clone())
5491            .collect::<BTreeSet<_>>();
5492        let selected_genes = matched_genes
5493            .into_iter()
5494            .filter(|gene| {
5495                delta.changed_gene_ids.contains(&gene.id) || selected_gene_ids.contains(&gene.id)
5496            })
5497            .collect::<Vec<_>>();
5498        (selected_genes, selected_capsules)
5499    } else {
5500        (matched_genes, matched_capsules)
5501    };
5502    let assets = replay_export_assets(&events, selected_genes, selected_capsules);
5503    let next_cursor = events.last().map(|stored| format_sync_cursor(stored.seq));
5504    let resume_token = next_cursor
5505        .as_ref()
5506        .map(|cursor| encode_resume_token(&query.sender_id, cursor));
5507    let applied_count = assets.len();
5508    let skipped_count = all_assets.len().saturating_sub(applied_count);
5509
5510    Ok(FetchResponse {
5511        sender_id: responder_id.into(),
5512        assets,
5513        next_cursor: next_cursor.clone(),
5514        resume_token,
5515        sync_audit: SyncAudit {
5516            batch_id: next_id("sync-fetch"),
5517            requested_cursor,
5518            scanned_count: all_assets.len(),
5519            applied_count,
5520            skipped_count,
5521            failed_count: 0,
5522            failure_reasons: Vec::new(),
5523        },
5524    })
5525}
5526
5527fn revoke_assets_in_store(
5528    store: &dyn EvolutionStore,
5529    notice: &RevokeNotice,
5530) -> Result<RevokeNotice, EvoKernelError> {
5531    let projection = projection_snapshot(store)?;
5532    let requested: BTreeSet<String> = notice
5533        .asset_ids
5534        .iter()
5535        .map(|asset_id| asset_id.trim().to_string())
5536        .filter(|asset_id| !asset_id.is_empty())
5537        .collect();
5538    let mut revoked_gene_ids = BTreeSet::new();
5539    let mut quarantined_capsule_ids = BTreeSet::new();
5540
5541    for gene in &projection.genes {
5542        if requested.contains(&gene.id) {
5543            revoked_gene_ids.insert(gene.id.clone());
5544        }
5545    }
5546    for capsule in &projection.capsules {
5547        if requested.contains(&capsule.id) {
5548            quarantined_capsule_ids.insert(capsule.id.clone());
5549            revoked_gene_ids.insert(capsule.gene_id.clone());
5550        }
5551    }
5552    for capsule in &projection.capsules {
5553        if revoked_gene_ids.contains(&capsule.gene_id) {
5554            quarantined_capsule_ids.insert(capsule.id.clone());
5555        }
5556    }
5557
5558    for gene_id in &revoked_gene_ids {
5559        store
5560            .append_event(EvolutionEvent::GeneRevoked {
5561                gene_id: gene_id.clone(),
5562                reason: notice.reason.clone(),
5563            })
5564            .map_err(store_err)?;
5565    }
5566    for capsule_id in &quarantined_capsule_ids {
5567        store
5568            .append_event(EvolutionEvent::CapsuleQuarantined {
5569                capsule_id: capsule_id.clone(),
5570            })
5571            .map_err(store_err)?;
5572    }
5573
5574    let mut affected_ids: Vec<String> = revoked_gene_ids.into_iter().collect();
5575    affected_ids.extend(quarantined_capsule_ids);
5576    affected_ids.sort();
5577    affected_ids.dedup();
5578
5579    Ok(RevokeNotice {
5580        sender_id: notice.sender_id.clone(),
5581        asset_ids: affected_ids,
5582        reason: notice.reason.clone(),
5583    })
5584}
5585
5586fn evolution_metrics_snapshot(
5587    store: &dyn EvolutionStore,
5588) -> Result<EvolutionMetricsSnapshot, EvoKernelError> {
5589    let (events, projection) = scan_projection(store)?;
5590    let mut replay_task_class_totals = BTreeMap::<(String, String), (u64, u64, u64, u64)>::new();
5591    let mut replay_source_totals = BTreeMap::<String, (u64, u64, u64, u64)>::new();
5592    let replay_evidences = events
5593        .iter()
5594        .filter_map(|stored| match &stored.event {
5595            EvolutionEvent::ReplayEconomicsRecorded { evidence, .. } => Some(evidence.clone()),
5596            _ => None,
5597        })
5598        .collect::<Vec<_>>();
5599    let (
5600        replay_success_total,
5601        replay_failures_total,
5602        replay_reasoning_avoided_total,
5603        reasoning_avoided_tokens_total,
5604        replay_fallback_cost_total,
5605    ) = if replay_evidences.is_empty() {
5606        let gene_task_classes = projection
5607            .genes
5608            .iter()
5609            .map(|gene| (gene.id.clone(), replay_task_descriptor(&gene.signals)))
5610            .collect::<BTreeMap<_, _>>();
5611        let replay_success_total = events
5612            .iter()
5613            .filter(|stored| matches!(stored.event, EvolutionEvent::CapsuleReused { .. }))
5614            .count() as u64;
5615        for stored in &events {
5616            if let EvolutionEvent::CapsuleReused { gene_id, .. } = &stored.event {
5617                if let Some((task_class_id, task_label)) = gene_task_classes.get(gene_id) {
5618                    let entry = replay_task_class_totals
5619                        .entry((task_class_id.clone(), task_label.clone()))
5620                        .or_insert((0, 0, 0, 0));
5621                    entry.0 += 1;
5622                    entry.2 += REPLAY_REASONING_TOKEN_FLOOR;
5623                }
5624            }
5625        }
5626        let replay_failures_total = events
5627            .iter()
5628            .filter(|stored| is_replay_validation_failure(&stored.event))
5629            .count() as u64;
5630        (
5631            replay_success_total,
5632            replay_failures_total,
5633            replay_success_total,
5634            replay_success_total * REPLAY_REASONING_TOKEN_FLOOR,
5635            replay_failures_total * REPLAY_REASONING_TOKEN_FLOOR,
5636        )
5637    } else {
5638        let mut replay_success_total = 0_u64;
5639        let mut replay_failures_total = 0_u64;
5640        let mut reasoning_avoided_tokens_total = 0_u64;
5641        let mut replay_fallback_cost_total = 0_u64;
5642        for evidence in &replay_evidences {
5643            if evidence.success {
5644                replay_success_total += 1;
5645            } else {
5646                replay_failures_total += 1;
5647            }
5648            reasoning_avoided_tokens_total += evidence.reasoning_avoided_tokens;
5649            replay_fallback_cost_total += evidence.replay_fallback_cost;
5650            let entry = replay_task_class_totals
5651                .entry((evidence.task_class_id.clone(), evidence.task_label.clone()))
5652                .or_insert((0, 0, 0, 0));
5653            if evidence.success {
5654                entry.0 += 1;
5655            } else {
5656                entry.1 += 1;
5657            }
5658            entry.2 += evidence.reasoning_avoided_tokens;
5659            entry.3 += evidence.replay_fallback_cost;
5660            if let Some(source_sender_id) = evidence.source_sender_id.as_deref() {
5661                let source_entry = replay_source_totals
5662                    .entry(source_sender_id.to_string())
5663                    .or_insert((0, 0, 0, 0));
5664                if evidence.success {
5665                    source_entry.0 += 1;
5666                } else {
5667                    source_entry.1 += 1;
5668                }
5669                source_entry.2 += evidence.reasoning_avoided_tokens;
5670                source_entry.3 += evidence.replay_fallback_cost;
5671            }
5672        }
5673        (
5674            replay_success_total,
5675            replay_failures_total,
5676            replay_success_total,
5677            reasoning_avoided_tokens_total,
5678            replay_fallback_cost_total,
5679        )
5680    };
5681    let replay_task_classes = replay_task_class_totals
5682        .into_iter()
5683        .map(
5684            |(
5685                (task_class_id, task_label),
5686                (
5687                    replay_success_total,
5688                    replay_failure_total,
5689                    reasoning_avoided_tokens_total,
5690                    replay_fallback_cost_total,
5691                ),
5692            )| ReplayTaskClassMetrics {
5693                task_class_id,
5694                task_label,
5695                replay_success_total,
5696                replay_failure_total,
5697                reasoning_steps_avoided_total: replay_success_total,
5698                reasoning_avoided_tokens_total,
5699                replay_fallback_cost_total,
5700                replay_roi: compute_replay_roi(
5701                    reasoning_avoided_tokens_total,
5702                    replay_fallback_cost_total,
5703                ),
5704            },
5705        )
5706        .collect::<Vec<_>>();
5707    let replay_sources = replay_source_totals
5708        .into_iter()
5709        .map(
5710            |(
5711                source_sender_id,
5712                (
5713                    replay_success_total,
5714                    replay_failure_total,
5715                    reasoning_avoided_tokens_total,
5716                    replay_fallback_cost_total,
5717                ),
5718            )| ReplaySourceRoiMetrics {
5719                source_sender_id,
5720                replay_success_total,
5721                replay_failure_total,
5722                reasoning_avoided_tokens_total,
5723                replay_fallback_cost_total,
5724                replay_roi: compute_replay_roi(
5725                    reasoning_avoided_tokens_total,
5726                    replay_fallback_cost_total,
5727                ),
5728            },
5729        )
5730        .collect::<Vec<_>>();
5731    let replay_attempts_total = replay_success_total + replay_failures_total;
5732    let confidence_revalidations_total = events
5733        .iter()
5734        .filter(|stored| is_confidence_revalidation_event(&stored.event))
5735        .count() as u64;
5736    let mutation_declared_total = events
5737        .iter()
5738        .filter(|stored| matches!(stored.event, EvolutionEvent::MutationDeclared { .. }))
5739        .count() as u64;
5740    let promoted_mutations_total = events
5741        .iter()
5742        .filter(|stored| matches!(stored.event, EvolutionEvent::GenePromoted { .. }))
5743        .count() as u64;
5744    let gene_revocations_total = events
5745        .iter()
5746        .filter(|stored| matches!(stored.event, EvolutionEvent::GeneRevoked { .. }))
5747        .count() as u64;
5748    let cutoff = Utc::now() - Duration::hours(1);
5749    let mutation_velocity_last_hour = count_recent_events(&events, cutoff, |event| {
5750        matches!(event, EvolutionEvent::MutationDeclared { .. })
5751    });
5752    let revoke_frequency_last_hour = count_recent_events(&events, cutoff, |event| {
5753        matches!(event, EvolutionEvent::GeneRevoked { .. })
5754    });
5755    let promoted_genes = projection
5756        .genes
5757        .iter()
5758        .filter(|gene| gene.state == AssetState::Promoted)
5759        .count() as u64;
5760    let promoted_capsules = projection
5761        .capsules
5762        .iter()
5763        .filter(|capsule| capsule.state == AssetState::Promoted)
5764        .count() as u64;
5765
5766    Ok(EvolutionMetricsSnapshot {
5767        replay_attempts_total,
5768        replay_success_total,
5769        replay_success_rate: safe_ratio(replay_success_total, replay_attempts_total),
5770        confidence_revalidations_total,
5771        replay_reasoning_avoided_total,
5772        reasoning_avoided_tokens_total,
5773        replay_fallback_cost_total,
5774        replay_roi: compute_replay_roi(reasoning_avoided_tokens_total, replay_fallback_cost_total),
5775        replay_task_classes,
5776        replay_sources,
5777        mutation_declared_total,
5778        promoted_mutations_total,
5779        promotion_ratio: safe_ratio(promoted_mutations_total, mutation_declared_total),
5780        gene_revocations_total,
5781        mutation_velocity_last_hour,
5782        revoke_frequency_last_hour,
5783        promoted_genes,
5784        promoted_capsules,
5785        last_event_seq: events.last().map(|stored| stored.seq).unwrap_or(0),
5786    })
5787}
5788
5789fn replay_roi_release_gate_summary(
5790    store: &dyn EvolutionStore,
5791    window_seconds: u64,
5792) -> Result<ReplayRoiWindowSummary, EvoKernelError> {
5793    let events = store.scan(1).map_err(store_err)?;
5794    let now = Utc::now();
5795    let cutoff = if window_seconds == 0 {
5796        None
5797    } else {
5798        let seconds = i64::try_from(window_seconds).unwrap_or(i64::MAX);
5799        Some(now - Duration::seconds(seconds))
5800    };
5801
5802    let mut replay_attempts_total = 0_u64;
5803    let mut replay_success_total = 0_u64;
5804    let mut replay_failure_total = 0_u64;
5805    let mut reasoning_avoided_tokens_total = 0_u64;
5806    let mut replay_fallback_cost_total = 0_u64;
5807    let mut task_totals = BTreeMap::<(String, String), (u64, u64, u64, u64)>::new();
5808    let mut source_totals = BTreeMap::<String, (u64, u64, u64, u64)>::new();
5809
5810    for stored in events {
5811        let EvolutionEvent::ReplayEconomicsRecorded { evidence, .. } = stored.event else {
5812            continue;
5813        };
5814        if let Some(cutoff) = cutoff {
5815            let Some(timestamp) = parse_event_timestamp(&stored.timestamp) else {
5816                continue;
5817            };
5818            if timestamp < cutoff {
5819                continue;
5820            }
5821        }
5822        replay_attempts_total += 1;
5823        if evidence.success {
5824            replay_success_total += 1;
5825        } else {
5826            replay_failure_total += 1;
5827        }
5828        reasoning_avoided_tokens_total += evidence.reasoning_avoided_tokens;
5829        replay_fallback_cost_total += evidence.replay_fallback_cost;
5830        let task_entry = task_totals
5831            .entry((evidence.task_class_id.clone(), evidence.task_label.clone()))
5832            .or_insert((0, 0, 0, 0));
5833        if evidence.success {
5834            task_entry.0 += 1;
5835        } else {
5836            task_entry.1 += 1;
5837        }
5838        task_entry.2 += evidence.reasoning_avoided_tokens;
5839        task_entry.3 += evidence.replay_fallback_cost;
5840        if let Some(source_sender_id) = evidence.source_sender_id.as_deref() {
5841            let source_entry = source_totals
5842                .entry(source_sender_id.to_string())
5843                .or_insert((0, 0, 0, 0));
5844            if evidence.success {
5845                source_entry.0 += 1;
5846            } else {
5847                source_entry.1 += 1;
5848            }
5849            source_entry.2 += evidence.reasoning_avoided_tokens;
5850            source_entry.3 += evidence.replay_fallback_cost;
5851        }
5852    }
5853
5854    let replay_task_classes = task_totals
5855        .into_iter()
5856        .map(
5857            |(
5858                (task_class_id, task_label),
5859                (
5860                    replay_success_total,
5861                    replay_failure_total,
5862                    reasoning_avoided_tokens_total,
5863                    replay_fallback_cost_total,
5864                ),
5865            )| ReplayTaskClassMetrics {
5866                task_class_id,
5867                task_label,
5868                replay_success_total,
5869                replay_failure_total,
5870                reasoning_steps_avoided_total: replay_success_total,
5871                reasoning_avoided_tokens_total,
5872                replay_fallback_cost_total,
5873                replay_roi: compute_replay_roi(
5874                    reasoning_avoided_tokens_total,
5875                    replay_fallback_cost_total,
5876                ),
5877            },
5878        )
5879        .collect::<Vec<_>>();
5880    let replay_sources = source_totals
5881        .into_iter()
5882        .map(
5883            |(
5884                source_sender_id,
5885                (
5886                    replay_success_total,
5887                    replay_failure_total,
5888                    reasoning_avoided_tokens_total,
5889                    replay_fallback_cost_total,
5890                ),
5891            )| ReplaySourceRoiMetrics {
5892                source_sender_id,
5893                replay_success_total,
5894                replay_failure_total,
5895                reasoning_avoided_tokens_total,
5896                replay_fallback_cost_total,
5897                replay_roi: compute_replay_roi(
5898                    reasoning_avoided_tokens_total,
5899                    replay_fallback_cost_total,
5900                ),
5901            },
5902        )
5903        .collect::<Vec<_>>();
5904
5905    Ok(ReplayRoiWindowSummary {
5906        generated_at: now.to_rfc3339(),
5907        window_seconds,
5908        replay_attempts_total,
5909        replay_success_total,
5910        replay_failure_total,
5911        reasoning_avoided_tokens_total,
5912        replay_fallback_cost_total,
5913        replay_roi: compute_replay_roi(reasoning_avoided_tokens_total, replay_fallback_cost_total),
5914        replay_task_classes,
5915        replay_sources,
5916    })
5917}
5918
5919fn replay_roi_release_gate_contract(
5920    summary: &ReplayRoiWindowSummary,
5921    thresholds: ReplayRoiReleaseGateThresholds,
5922) -> ReplayRoiReleaseGateContract {
5923    let input = replay_roi_release_gate_input_contract(summary, thresholds);
5924    let output = evaluate_replay_roi_release_gate_contract_input(&input);
5925    ReplayRoiReleaseGateContract { input, output }
5926}
5927
5928fn replay_roi_release_gate_input_contract(
5929    summary: &ReplayRoiWindowSummary,
5930    thresholds: ReplayRoiReleaseGateThresholds,
5931) -> ReplayRoiReleaseGateInputContract {
5932    let replay_safety_signal = replay_roi_release_gate_safety_signal(summary);
5933    let replay_safety = replay_safety_signal.fail_closed_default
5934        && replay_safety_signal.rollback_ready
5935        && replay_safety_signal.audit_trail_complete
5936        && replay_safety_signal.has_replay_activity;
5937    ReplayRoiReleaseGateInputContract {
5938        generated_at: summary.generated_at.clone(),
5939        window_seconds: summary.window_seconds,
5940        aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
5941            .iter()
5942            .map(|dimension| (*dimension).to_string())
5943            .collect(),
5944        replay_attempts_total: summary.replay_attempts_total,
5945        replay_success_total: summary.replay_success_total,
5946        replay_failure_total: summary.replay_failure_total,
5947        replay_hit_rate: safe_ratio(summary.replay_success_total, summary.replay_attempts_total),
5948        false_replay_rate: safe_ratio(summary.replay_failure_total, summary.replay_attempts_total),
5949        reasoning_avoided_tokens: summary.reasoning_avoided_tokens_total,
5950        replay_fallback_cost_total: summary.replay_fallback_cost_total,
5951        replay_roi: summary.replay_roi,
5952        replay_safety,
5953        replay_safety_signal,
5954        thresholds,
5955        fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
5956    }
5957}
5958
5959fn replay_roi_release_gate_safety_signal(
5960    summary: &ReplayRoiWindowSummary,
5961) -> ReplayRoiReleaseGateSafetySignal {
5962    ReplayRoiReleaseGateSafetySignal {
5963        fail_closed_default: true,
5964        rollback_ready: summary.replay_failure_total == 0 || summary.replay_fallback_cost_total > 0,
5965        audit_trail_complete: summary.replay_attempts_total
5966            == summary.replay_success_total + summary.replay_failure_total,
5967        has_replay_activity: summary.replay_attempts_total > 0,
5968    }
5969}
5970
5971pub fn evaluate_replay_roi_release_gate_contract_input(
5972    input: &ReplayRoiReleaseGateInputContract,
5973) -> ReplayRoiReleaseGateOutputContract {
5974    let mut failed_checks = Vec::new();
5975    let mut evidence_refs = Vec::new();
5976    let mut indeterminate = false;
5977
5978    replay_release_gate_push_unique(&mut evidence_refs, "replay_roi_release_gate_summary");
5979    replay_release_gate_push_unique(
5980        &mut evidence_refs,
5981        format!("window_seconds:{}", input.window_seconds),
5982    );
5983    if input.generated_at.trim().is_empty() {
5984        replay_release_gate_record_failed_check(
5985            &mut failed_checks,
5986            &mut evidence_refs,
5987            "missing_generated_at",
5988            &["field:generated_at"],
5989        );
5990        indeterminate = true;
5991    } else {
5992        replay_release_gate_push_unique(
5993            &mut evidence_refs,
5994            format!("generated_at:{}", input.generated_at),
5995        );
5996    }
5997
5998    let expected_attempts_total = input.replay_success_total + input.replay_failure_total;
5999    if input.replay_attempts_total != expected_attempts_total {
6000        replay_release_gate_record_failed_check(
6001            &mut failed_checks,
6002            &mut evidence_refs,
6003            "invalid_attempt_accounting",
6004            &[
6005                "metric:replay_attempts_total",
6006                "metric:replay_success_total",
6007                "metric:replay_failure_total",
6008            ],
6009        );
6010        indeterminate = true;
6011    }
6012
6013    if input.replay_attempts_total == 0 {
6014        replay_release_gate_record_failed_check(
6015            &mut failed_checks,
6016            &mut evidence_refs,
6017            "missing_replay_attempts",
6018            &["metric:replay_attempts_total"],
6019        );
6020        indeterminate = true;
6021    }
6022
6023    if !replay_release_gate_rate_valid(input.replay_hit_rate) {
6024        replay_release_gate_record_failed_check(
6025            &mut failed_checks,
6026            &mut evidence_refs,
6027            "invalid_replay_hit_rate",
6028            &["metric:replay_hit_rate"],
6029        );
6030        indeterminate = true;
6031    }
6032    if !replay_release_gate_rate_valid(input.false_replay_rate) {
6033        replay_release_gate_record_failed_check(
6034            &mut failed_checks,
6035            &mut evidence_refs,
6036            "invalid_false_replay_rate",
6037            &["metric:false_replay_rate"],
6038        );
6039        indeterminate = true;
6040    }
6041
6042    if !input.replay_roi.is_finite() {
6043        replay_release_gate_record_failed_check(
6044            &mut failed_checks,
6045            &mut evidence_refs,
6046            "invalid_replay_roi",
6047            &["metric:replay_roi"],
6048        );
6049        indeterminate = true;
6050    }
6051
6052    let expected_hit_rate = safe_ratio(input.replay_success_total, input.replay_attempts_total);
6053    let expected_false_rate = safe_ratio(input.replay_failure_total, input.replay_attempts_total);
6054    if input.replay_attempts_total > 0
6055        && !replay_release_gate_float_eq(input.replay_hit_rate, expected_hit_rate)
6056    {
6057        replay_release_gate_record_failed_check(
6058            &mut failed_checks,
6059            &mut evidence_refs,
6060            "invalid_replay_hit_rate_consistency",
6061            &["metric:replay_hit_rate", "metric:replay_success_total"],
6062        );
6063        indeterminate = true;
6064    }
6065    if input.replay_attempts_total > 0
6066        && !replay_release_gate_float_eq(input.false_replay_rate, expected_false_rate)
6067    {
6068        replay_release_gate_record_failed_check(
6069            &mut failed_checks,
6070            &mut evidence_refs,
6071            "invalid_false_replay_rate_consistency",
6072            &["metric:false_replay_rate", "metric:replay_failure_total"],
6073        );
6074        indeterminate = true;
6075    }
6076
6077    if !(0.0..=1.0).contains(&input.thresholds.min_replay_hit_rate) {
6078        replay_release_gate_record_failed_check(
6079            &mut failed_checks,
6080            &mut evidence_refs,
6081            "invalid_threshold_min_replay_hit_rate",
6082            &["threshold:min_replay_hit_rate"],
6083        );
6084        indeterminate = true;
6085    }
6086    if !(0.0..=1.0).contains(&input.thresholds.max_false_replay_rate) {
6087        replay_release_gate_record_failed_check(
6088            &mut failed_checks,
6089            &mut evidence_refs,
6090            "invalid_threshold_max_false_replay_rate",
6091            &["threshold:max_false_replay_rate"],
6092        );
6093        indeterminate = true;
6094    }
6095    if !input.thresholds.min_replay_roi.is_finite() {
6096        replay_release_gate_record_failed_check(
6097            &mut failed_checks,
6098            &mut evidence_refs,
6099            "invalid_threshold_min_replay_roi",
6100            &["threshold:min_replay_roi"],
6101        );
6102        indeterminate = true;
6103    }
6104
6105    if input.replay_attempts_total < input.thresholds.min_replay_attempts {
6106        replay_release_gate_record_failed_check(
6107            &mut failed_checks,
6108            &mut evidence_refs,
6109            "min_replay_attempts_below_threshold",
6110            &[
6111                "threshold:min_replay_attempts",
6112                "metric:replay_attempts_total",
6113            ],
6114        );
6115    }
6116    if input.replay_attempts_total > 0
6117        && input.replay_hit_rate < input.thresholds.min_replay_hit_rate
6118    {
6119        replay_release_gate_record_failed_check(
6120            &mut failed_checks,
6121            &mut evidence_refs,
6122            "replay_hit_rate_below_threshold",
6123            &["threshold:min_replay_hit_rate", "metric:replay_hit_rate"],
6124        );
6125    }
6126    if input.replay_attempts_total > 0
6127        && input.false_replay_rate > input.thresholds.max_false_replay_rate
6128    {
6129        replay_release_gate_record_failed_check(
6130            &mut failed_checks,
6131            &mut evidence_refs,
6132            "false_replay_rate_above_threshold",
6133            &[
6134                "threshold:max_false_replay_rate",
6135                "metric:false_replay_rate",
6136            ],
6137        );
6138    }
6139    if input.reasoning_avoided_tokens < input.thresholds.min_reasoning_avoided_tokens {
6140        replay_release_gate_record_failed_check(
6141            &mut failed_checks,
6142            &mut evidence_refs,
6143            "reasoning_avoided_tokens_below_threshold",
6144            &[
6145                "threshold:min_reasoning_avoided_tokens",
6146                "metric:reasoning_avoided_tokens",
6147            ],
6148        );
6149    }
6150    if input.replay_roi < input.thresholds.min_replay_roi {
6151        replay_release_gate_record_failed_check(
6152            &mut failed_checks,
6153            &mut evidence_refs,
6154            "replay_roi_below_threshold",
6155            &["threshold:min_replay_roi", "metric:replay_roi"],
6156        );
6157    }
6158    if input.thresholds.require_replay_safety && !input.replay_safety {
6159        replay_release_gate_record_failed_check(
6160            &mut failed_checks,
6161            &mut evidence_refs,
6162            "replay_safety_required",
6163            &["metric:replay_safety", "threshold:require_replay_safety"],
6164        );
6165    }
6166
6167    failed_checks.sort();
6168    evidence_refs.sort();
6169
6170    let status = if failed_checks.is_empty() {
6171        ReplayRoiReleaseGateStatus::Pass
6172    } else if indeterminate {
6173        ReplayRoiReleaseGateStatus::Indeterminate
6174    } else {
6175        ReplayRoiReleaseGateStatus::FailClosed
6176    };
6177    let joined_checks = if failed_checks.is_empty() {
6178        "none".to_string()
6179    } else {
6180        failed_checks.join(",")
6181    };
6182    let summary = match status {
6183        ReplayRoiReleaseGateStatus::Pass => format!(
6184            "release gate pass: attempts={} hit_rate={:.3} false_replay_rate={:.3} reasoning_avoided_tokens={} replay_roi={:.3} replay_safety={}",
6185            input.replay_attempts_total,
6186            input.replay_hit_rate,
6187            input.false_replay_rate,
6188            input.reasoning_avoided_tokens,
6189            input.replay_roi,
6190            input.replay_safety
6191        ),
6192        ReplayRoiReleaseGateStatus::FailClosed => format!(
6193            "release gate fail_closed: failed_checks=[{}] attempts={} hit_rate={:.3} false_replay_rate={:.3} reasoning_avoided_tokens={} replay_roi={:.3} replay_safety={}",
6194            joined_checks,
6195            input.replay_attempts_total,
6196            input.replay_hit_rate,
6197            input.false_replay_rate,
6198            input.reasoning_avoided_tokens,
6199            input.replay_roi,
6200            input.replay_safety
6201        ),
6202        ReplayRoiReleaseGateStatus::Indeterminate => format!(
6203            "release gate indeterminate (fail-closed): failed_checks=[{}] attempts={} hit_rate={:.3} false_replay_rate={:.3} reasoning_avoided_tokens={} replay_roi={:.3} replay_safety={}",
6204            joined_checks,
6205            input.replay_attempts_total,
6206            input.replay_hit_rate,
6207            input.false_replay_rate,
6208            input.reasoning_avoided_tokens,
6209            input.replay_roi,
6210            input.replay_safety
6211        ),
6212    };
6213
6214    ReplayRoiReleaseGateOutputContract {
6215        status,
6216        failed_checks,
6217        evidence_refs,
6218        summary,
6219    }
6220}
6221
6222fn replay_release_gate_record_failed_check(
6223    failed_checks: &mut Vec<String>,
6224    evidence_refs: &mut Vec<String>,
6225    check: &str,
6226    refs: &[&str],
6227) {
6228    replay_release_gate_push_unique(failed_checks, check.to_string());
6229    for entry in refs {
6230        replay_release_gate_push_unique(evidence_refs, (*entry).to_string());
6231    }
6232}
6233
6234fn replay_release_gate_push_unique(values: &mut Vec<String>, entry: impl Into<String>) {
6235    let entry = entry.into();
6236    if !values.iter().any(|current| current == &entry) {
6237        values.push(entry);
6238    }
6239}
6240
6241fn replay_release_gate_rate_valid(value: f64) -> bool {
6242    value.is_finite() && (0.0..=1.0).contains(&value)
6243}
6244
6245fn replay_release_gate_float_eq(left: f64, right: f64) -> bool {
6246    (left - right).abs() <= 1e-9
6247}
6248
6249fn evolution_health_snapshot(snapshot: &EvolutionMetricsSnapshot) -> EvolutionHealthSnapshot {
6250    EvolutionHealthSnapshot {
6251        status: "ok".into(),
6252        last_event_seq: snapshot.last_event_seq,
6253        promoted_genes: snapshot.promoted_genes,
6254        promoted_capsules: snapshot.promoted_capsules,
6255    }
6256}
6257
6258fn render_evolution_metrics_prometheus(
6259    snapshot: &EvolutionMetricsSnapshot,
6260    health: &EvolutionHealthSnapshot,
6261) -> String {
6262    let mut out = String::new();
6263    out.push_str(
6264        "# HELP oris_evolution_replay_attempts_total Total replay attempts that reached validation.\n",
6265    );
6266    out.push_str("# TYPE oris_evolution_replay_attempts_total counter\n");
6267    out.push_str(&format!(
6268        "oris_evolution_replay_attempts_total {}\n",
6269        snapshot.replay_attempts_total
6270    ));
6271    out.push_str("# HELP oris_evolution_replay_success_total Total replay attempts that reused a capsule successfully.\n");
6272    out.push_str("# TYPE oris_evolution_replay_success_total counter\n");
6273    out.push_str(&format!(
6274        "oris_evolution_replay_success_total {}\n",
6275        snapshot.replay_success_total
6276    ));
6277    out.push_str("# HELP oris_evolution_replay_reasoning_avoided_total Total planner steps avoided by successful replay.\n");
6278    out.push_str("# TYPE oris_evolution_replay_reasoning_avoided_total counter\n");
6279    out.push_str(&format!(
6280        "oris_evolution_replay_reasoning_avoided_total {}\n",
6281        snapshot.replay_reasoning_avoided_total
6282    ));
6283    out.push_str("# HELP oris_evolution_reasoning_avoided_tokens_total Estimated reasoning tokens avoided by replay hits.\n");
6284    out.push_str("# TYPE oris_evolution_reasoning_avoided_tokens_total counter\n");
6285    out.push_str(&format!(
6286        "oris_evolution_reasoning_avoided_tokens_total {}\n",
6287        snapshot.reasoning_avoided_tokens_total
6288    ));
6289    out.push_str("# HELP oris_evolution_replay_fallback_cost_total Estimated reasoning token cost spent on replay fallbacks.\n");
6290    out.push_str("# TYPE oris_evolution_replay_fallback_cost_total counter\n");
6291    out.push_str(&format!(
6292        "oris_evolution_replay_fallback_cost_total {}\n",
6293        snapshot.replay_fallback_cost_total
6294    ));
6295    out.push_str("# HELP oris_evolution_replay_roi Net replay ROI in token space ((avoided - fallback_cost) / total).\n");
6296    out.push_str("# TYPE oris_evolution_replay_roi gauge\n");
6297    out.push_str(&format!(
6298        "oris_evolution_replay_roi {:.6}\n",
6299        snapshot.replay_roi
6300    ));
6301    out.push_str("# HELP oris_evolution_replay_utilization_by_task_class_total Successful replay reuse counts grouped by deterministic task class.\n");
6302    out.push_str("# TYPE oris_evolution_replay_utilization_by_task_class_total counter\n");
6303    for task_class in &snapshot.replay_task_classes {
6304        out.push_str(&format!(
6305            "oris_evolution_replay_utilization_by_task_class_total{{task_class_id=\"{}\",task_label=\"{}\"}} {}\n",
6306            prometheus_label_value(&task_class.task_class_id),
6307            prometheus_label_value(&task_class.task_label),
6308            task_class.replay_success_total
6309        ));
6310    }
6311    out.push_str("# HELP oris_evolution_replay_reasoning_avoided_by_task_class_total Planner steps avoided by successful replay grouped by deterministic task class.\n");
6312    out.push_str("# TYPE oris_evolution_replay_reasoning_avoided_by_task_class_total counter\n");
6313    for task_class in &snapshot.replay_task_classes {
6314        out.push_str(&format!(
6315            "oris_evolution_replay_reasoning_avoided_by_task_class_total{{task_class_id=\"{}\",task_label=\"{}\"}} {}\n",
6316            prometheus_label_value(&task_class.task_class_id),
6317            prometheus_label_value(&task_class.task_label),
6318            task_class.reasoning_steps_avoided_total
6319        ));
6320    }
6321    out.push_str("# HELP oris_evolution_reasoning_avoided_tokens_by_task_class_total Estimated reasoning tokens avoided by replay hits grouped by deterministic task class.\n");
6322    out.push_str("# TYPE oris_evolution_reasoning_avoided_tokens_by_task_class_total counter\n");
6323    for task_class in &snapshot.replay_task_classes {
6324        out.push_str(&format!(
6325            "oris_evolution_reasoning_avoided_tokens_by_task_class_total{{task_class_id=\"{}\",task_label=\"{}\"}} {}\n",
6326            prometheus_label_value(&task_class.task_class_id),
6327            prometheus_label_value(&task_class.task_label),
6328            task_class.reasoning_avoided_tokens_total
6329        ));
6330    }
6331    out.push_str("# HELP oris_evolution_replay_fallback_cost_by_task_class_total Estimated fallback token cost grouped by deterministic task class.\n");
6332    out.push_str("# TYPE oris_evolution_replay_fallback_cost_by_task_class_total counter\n");
6333    for task_class in &snapshot.replay_task_classes {
6334        out.push_str(&format!(
6335            "oris_evolution_replay_fallback_cost_by_task_class_total{{task_class_id=\"{}\",task_label=\"{}\"}} {}\n",
6336            prometheus_label_value(&task_class.task_class_id),
6337            prometheus_label_value(&task_class.task_label),
6338            task_class.replay_fallback_cost_total
6339        ));
6340    }
6341    out.push_str("# HELP oris_evolution_replay_roi_by_task_class Replay ROI in token space grouped by deterministic task class.\n");
6342    out.push_str("# TYPE oris_evolution_replay_roi_by_task_class gauge\n");
6343    for task_class in &snapshot.replay_task_classes {
6344        out.push_str(&format!(
6345            "oris_evolution_replay_roi_by_task_class{{task_class_id=\"{}\",task_label=\"{}\"}} {:.6}\n",
6346            prometheus_label_value(&task_class.task_class_id),
6347            prometheus_label_value(&task_class.task_label),
6348            task_class.replay_roi
6349        ));
6350    }
6351    out.push_str("# HELP oris_evolution_replay_roi_by_source Replay ROI in token space grouped by remote sender id for cross-node reconciliation.\n");
6352    out.push_str("# TYPE oris_evolution_replay_roi_by_source gauge\n");
6353    for source in &snapshot.replay_sources {
6354        out.push_str(&format!(
6355            "oris_evolution_replay_roi_by_source{{source_sender_id=\"{}\"}} {:.6}\n",
6356            prometheus_label_value(&source.source_sender_id),
6357            source.replay_roi
6358        ));
6359    }
6360    out.push_str("# HELP oris_evolution_reasoning_avoided_tokens_by_source_total Estimated reasoning tokens avoided grouped by remote sender id.\n");
6361    out.push_str("# TYPE oris_evolution_reasoning_avoided_tokens_by_source_total counter\n");
6362    for source in &snapshot.replay_sources {
6363        out.push_str(&format!(
6364            "oris_evolution_reasoning_avoided_tokens_by_source_total{{source_sender_id=\"{}\"}} {}\n",
6365            prometheus_label_value(&source.source_sender_id),
6366            source.reasoning_avoided_tokens_total
6367        ));
6368    }
6369    out.push_str("# HELP oris_evolution_replay_fallback_cost_by_source_total Estimated replay fallback token cost grouped by remote sender id.\n");
6370    out.push_str("# TYPE oris_evolution_replay_fallback_cost_by_source_total counter\n");
6371    for source in &snapshot.replay_sources {
6372        out.push_str(&format!(
6373            "oris_evolution_replay_fallback_cost_by_source_total{{source_sender_id=\"{}\"}} {}\n",
6374            prometheus_label_value(&source.source_sender_id),
6375            source.replay_fallback_cost_total
6376        ));
6377    }
6378    out.push_str("# HELP oris_evolution_replay_success_rate Successful replay attempts divided by replay attempts that reached validation.\n");
6379    out.push_str("# TYPE oris_evolution_replay_success_rate gauge\n");
6380    out.push_str(&format!(
6381        "oris_evolution_replay_success_rate {:.6}\n",
6382        snapshot.replay_success_rate
6383    ));
6384    out.push_str("# HELP oris_evolution_confidence_revalidations_total Total confidence-driven demotions that require revalidation before replay.\n");
6385    out.push_str("# TYPE oris_evolution_confidence_revalidations_total counter\n");
6386    out.push_str(&format!(
6387        "oris_evolution_confidence_revalidations_total {}\n",
6388        snapshot.confidence_revalidations_total
6389    ));
6390    out.push_str(
6391        "# HELP oris_evolution_mutation_declared_total Total declared mutations recorded in the evolution log.\n",
6392    );
6393    out.push_str("# TYPE oris_evolution_mutation_declared_total counter\n");
6394    out.push_str(&format!(
6395        "oris_evolution_mutation_declared_total {}\n",
6396        snapshot.mutation_declared_total
6397    ));
6398    out.push_str("# HELP oris_evolution_promoted_mutations_total Total mutations promoted by the governor.\n");
6399    out.push_str("# TYPE oris_evolution_promoted_mutations_total counter\n");
6400    out.push_str(&format!(
6401        "oris_evolution_promoted_mutations_total {}\n",
6402        snapshot.promoted_mutations_total
6403    ));
6404    out.push_str(
6405        "# HELP oris_evolution_promotion_ratio Promoted mutations divided by declared mutations.\n",
6406    );
6407    out.push_str("# TYPE oris_evolution_promotion_ratio gauge\n");
6408    out.push_str(&format!(
6409        "oris_evolution_promotion_ratio {:.6}\n",
6410        snapshot.promotion_ratio
6411    ));
6412    out.push_str("# HELP oris_evolution_gene_revocations_total Total gene revocations recorded in the evolution log.\n");
6413    out.push_str("# TYPE oris_evolution_gene_revocations_total counter\n");
6414    out.push_str(&format!(
6415        "oris_evolution_gene_revocations_total {}\n",
6416        snapshot.gene_revocations_total
6417    ));
6418    out.push_str("# HELP oris_evolution_mutation_velocity_last_hour Declared mutations observed in the last hour.\n");
6419    out.push_str("# TYPE oris_evolution_mutation_velocity_last_hour gauge\n");
6420    out.push_str(&format!(
6421        "oris_evolution_mutation_velocity_last_hour {}\n",
6422        snapshot.mutation_velocity_last_hour
6423    ));
6424    out.push_str("# HELP oris_evolution_revoke_frequency_last_hour Gene revocations observed in the last hour.\n");
6425    out.push_str("# TYPE oris_evolution_revoke_frequency_last_hour gauge\n");
6426    out.push_str(&format!(
6427        "oris_evolution_revoke_frequency_last_hour {}\n",
6428        snapshot.revoke_frequency_last_hour
6429    ));
6430    out.push_str("# HELP oris_evolution_promoted_genes Current promoted genes in the evolution projection.\n");
6431    out.push_str("# TYPE oris_evolution_promoted_genes gauge\n");
6432    out.push_str(&format!(
6433        "oris_evolution_promoted_genes {}\n",
6434        snapshot.promoted_genes
6435    ));
6436    out.push_str("# HELP oris_evolution_promoted_capsules Current promoted capsules in the evolution projection.\n");
6437    out.push_str("# TYPE oris_evolution_promoted_capsules gauge\n");
6438    out.push_str(&format!(
6439        "oris_evolution_promoted_capsules {}\n",
6440        snapshot.promoted_capsules
6441    ));
6442    out.push_str("# HELP oris_evolution_store_last_event_seq Last visible append-only evolution event sequence.\n");
6443    out.push_str("# TYPE oris_evolution_store_last_event_seq gauge\n");
6444    out.push_str(&format!(
6445        "oris_evolution_store_last_event_seq {}\n",
6446        snapshot.last_event_seq
6447    ));
6448    out.push_str(
6449        "# HELP oris_evolution_health Evolution observability store health (1 = healthy).\n",
6450    );
6451    out.push_str("# TYPE oris_evolution_health gauge\n");
6452    out.push_str(&format!(
6453        "oris_evolution_health {}\n",
6454        u8::from(health.status == "ok")
6455    ));
6456    out
6457}
6458
6459fn count_recent_events(
6460    events: &[StoredEvolutionEvent],
6461    cutoff: DateTime<Utc>,
6462    predicate: impl Fn(&EvolutionEvent) -> bool,
6463) -> u64 {
6464    events
6465        .iter()
6466        .filter(|stored| {
6467            predicate(&stored.event)
6468                && parse_event_timestamp(&stored.timestamp)
6469                    .map(|timestamp| timestamp >= cutoff)
6470                    .unwrap_or(false)
6471        })
6472        .count() as u64
6473}
6474
6475fn prometheus_label_value(input: &str) -> String {
6476    input
6477        .replace('\\', "\\\\")
6478        .replace('\n', "\\n")
6479        .replace('"', "\\\"")
6480}
6481
6482fn parse_event_timestamp(raw: &str) -> Option<DateTime<Utc>> {
6483    DateTime::parse_from_rfc3339(raw)
6484        .ok()
6485        .map(|parsed| parsed.with_timezone(&Utc))
6486}
6487
6488fn is_replay_validation_failure(event: &EvolutionEvent) -> bool {
6489    matches!(
6490        event,
6491        EvolutionEvent::ValidationFailed {
6492            gene_id: Some(_),
6493            ..
6494        }
6495    )
6496}
6497
6498fn is_confidence_revalidation_event(event: &EvolutionEvent) -> bool {
6499    matches!(
6500        event,
6501        EvolutionEvent::PromotionEvaluated {
6502            state,
6503            reason,
6504            reason_code,
6505            ..
6506        }
6507            if *state == AssetState::Quarantined
6508                && (reason_code == &TransitionReasonCode::RevalidationConfidenceDecay
6509                    || (reason_code == &TransitionReasonCode::Unspecified
6510                        && reason.contains("confidence decayed")))
6511    )
6512}
6513
6514fn safe_ratio(numerator: u64, denominator: u64) -> f64 {
6515    if denominator == 0 {
6516        0.0
6517    } else {
6518        numerator as f64 / denominator as f64
6519    }
6520}
6521
6522fn store_err(err: EvolutionError) -> EvoKernelError {
6523    EvoKernelError::Store(err.to_string())
6524}
6525
6526#[cfg(test)]
6527mod tests {
6528    use super::*;
6529    use oris_agent_contract::{
6530        AgentRole, CoordinationPlan, CoordinationPrimitive, CoordinationTask,
6531    };
6532    use oris_kernel::{
6533        AllowAllPolicy, InMemoryEventStore, KernelMode, KernelState, NoopActionExecutor,
6534        NoopStepFn, StateUpdatedOnlyReducer,
6535    };
6536    use serde::{Deserialize, Serialize};
6537
6538    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
6539    struct TestState;
6540
6541    impl KernelState for TestState {
6542        fn version(&self) -> u32 {
6543            1
6544        }
6545    }
6546
6547    #[test]
6548    fn repair_quality_gate_accepts_semantic_variants() {
6549        let plan = r#"
6550根本原因:脚本中拼写错误导致 unknown command 'process'。
6551修复建议:将 `proccess` 更正为 `process`,并统一命令入口。
6552验证方式:执行 `cargo check -p oris-runtime` 与回归测试。
6553恢复方案:若新入口异常,立即回滚到旧命令映射。
6554"#;
6555        let report = evaluate_repair_quality_gate(plan);
6556        assert!(report.passes());
6557        assert!(report.failed_checks().is_empty());
6558    }
6559
6560    #[test]
6561    fn repair_quality_gate_rejects_missing_incident_anchor() {
6562        let plan = r#"
6563原因分析:逻辑分支覆盖不足。
6564修复方案:补充分支与日志。
6565验证命令:cargo check -p oris-runtime
6566回滚方案:git revert HEAD
6567"#;
6568        let report = evaluate_repair_quality_gate(plan);
6569        assert!(!report.passes());
6570        assert!(report
6571            .failed_checks()
6572            .iter()
6573            .any(|check| check.contains("unknown command")));
6574    }
6575
6576    fn temp_workspace(name: &str) -> std::path::PathBuf {
6577        let root =
6578            std::env::temp_dir().join(format!("oris-evokernel-{name}-{}", std::process::id()));
6579        if root.exists() {
6580            fs::remove_dir_all(&root).unwrap();
6581        }
6582        fs::create_dir_all(root.join("src")).unwrap();
6583        fs::write(
6584            root.join("Cargo.toml"),
6585            "[package]\nname = \"sample\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
6586        )
6587        .unwrap();
6588        fs::write(root.join("Cargo.lock"), "# lock\n").unwrap();
6589        fs::write(root.join("src/lib.rs"), "pub fn demo() -> usize { 1 }\n").unwrap();
6590        root
6591    }
6592
6593    fn test_kernel() -> Arc<Kernel<TestState>> {
6594        Arc::new(Kernel::<TestState> {
6595            events: Box::new(InMemoryEventStore::new()),
6596            snaps: None,
6597            reducer: Box::new(StateUpdatedOnlyReducer),
6598            exec: Box::new(NoopActionExecutor),
6599            step: Box::new(NoopStepFn),
6600            policy: Box::new(AllowAllPolicy),
6601            effect_sink: None,
6602            mode: KernelMode::Normal,
6603        })
6604    }
6605
6606    fn lightweight_plan() -> ValidationPlan {
6607        ValidationPlan {
6608            profile: "test".into(),
6609            stages: vec![ValidationStage::Command {
6610                program: "git".into(),
6611                args: vec!["--version".into()],
6612                timeout_ms: 5_000,
6613            }],
6614        }
6615    }
6616
6617    fn sample_mutation() -> PreparedMutation {
6618        prepare_mutation(
6619            MutationIntent {
6620                id: "mutation-1".into(),
6621                intent: "add README".into(),
6622                target: MutationTarget::Paths {
6623                    allow: vec!["README.md".into()],
6624                },
6625                expected_effect: "repo still builds".into(),
6626                risk: RiskLevel::Low,
6627                signals: vec!["missing readme".into()],
6628                spec_id: None,
6629            },
6630            "\
6631diff --git a/README.md b/README.md
6632new file mode 100644
6633index 0000000..1111111
6634--- /dev/null
6635+++ b/README.md
6636@@ -0,0 +1 @@
6637+# sample
6638"
6639            .into(),
6640            Some("HEAD".into()),
6641        )
6642    }
6643
6644    fn base_sandbox_policy() -> SandboxPolicy {
6645        SandboxPolicy {
6646            allowed_programs: vec!["git".into()],
6647            max_duration_ms: 60_000,
6648            max_output_bytes: 1024 * 1024,
6649            denied_env_prefixes: Vec::new(),
6650        }
6651    }
6652
6653    fn command_validator() -> Arc<dyn Validator> {
6654        Arc::new(CommandValidator::new(base_sandbox_policy()))
6655    }
6656
6657    fn replay_input(signal: &str) -> SelectorInput {
6658        let rustc_version = std::process::Command::new("rustc")
6659            .arg("--version")
6660            .output()
6661            .ok()
6662            .filter(|output| output.status.success())
6663            .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
6664            .unwrap_or_else(|| "rustc unknown".into());
6665        SelectorInput {
6666            signals: vec![signal.into()],
6667            env: EnvFingerprint {
6668                rustc_version,
6669                cargo_lock_hash: compute_artifact_hash("# lock\n"),
6670                target_triple: format!(
6671                    "{}-unknown-{}",
6672                    std::env::consts::ARCH,
6673                    std::env::consts::OS
6674                ),
6675                os: std::env::consts::OS.into(),
6676            },
6677            spec_id: None,
6678            limit: 1,
6679        }
6680    }
6681
6682    fn build_test_evo_with_store(
6683        name: &str,
6684        run_id: &str,
6685        validator: Arc<dyn Validator>,
6686        store: Arc<dyn EvolutionStore>,
6687    ) -> EvoKernel<TestState> {
6688        let workspace = temp_workspace(name);
6689        let sandbox: Arc<dyn Sandbox> = Arc::new(oris_sandbox::LocalProcessSandbox::new(
6690            run_id,
6691            &workspace,
6692            std::env::temp_dir(),
6693        ));
6694        EvoKernel::new(test_kernel(), sandbox, validator, store)
6695            .with_governor(Arc::new(DefaultGovernor::new(
6696                oris_governor::GovernorConfig {
6697                    promote_after_successes: 1,
6698                    ..Default::default()
6699                },
6700            )))
6701            .with_validation_plan(lightweight_plan())
6702            .with_sandbox_policy(base_sandbox_policy())
6703    }
6704
6705    fn build_test_evo(
6706        name: &str,
6707        run_id: &str,
6708        validator: Arc<dyn Validator>,
6709    ) -> (EvoKernel<TestState>, Arc<dyn EvolutionStore>) {
6710        let store_root = std::env::temp_dir().join(format!(
6711            "oris-evokernel-{name}-store-{}",
6712            std::process::id()
6713        ));
6714        if store_root.exists() {
6715            fs::remove_dir_all(&store_root).unwrap();
6716        }
6717        let store: Arc<dyn EvolutionStore> =
6718            Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
6719        let evo = build_test_evo_with_store(name, run_id, validator, store.clone());
6720        (evo, store)
6721    }
6722
6723    fn remote_publish_envelope(
6724        sender_id: &str,
6725        run_id: &str,
6726        gene_id: &str,
6727        capsule_id: &str,
6728        mutation_id: &str,
6729        signal: &str,
6730        file_name: &str,
6731        line: &str,
6732    ) -> EvolutionEnvelope {
6733        remote_publish_envelope_with_env(
6734            sender_id,
6735            run_id,
6736            gene_id,
6737            capsule_id,
6738            mutation_id,
6739            signal,
6740            file_name,
6741            line,
6742            replay_input(signal).env,
6743        )
6744    }
6745
6746    fn remote_publish_envelope_with_env(
6747        sender_id: &str,
6748        run_id: &str,
6749        gene_id: &str,
6750        capsule_id: &str,
6751        mutation_id: &str,
6752        signal: &str,
6753        file_name: &str,
6754        line: &str,
6755        env: EnvFingerprint,
6756    ) -> EvolutionEnvelope {
6757        let mutation = prepare_mutation(
6758            MutationIntent {
6759                id: mutation_id.into(),
6760                intent: format!("add {file_name}"),
6761                target: MutationTarget::Paths {
6762                    allow: vec![file_name.into()],
6763                },
6764                expected_effect: "replay should still validate".into(),
6765                risk: RiskLevel::Low,
6766                signals: vec![signal.into()],
6767                spec_id: None,
6768            },
6769            format!(
6770                "\
6771diff --git a/{file_name} b/{file_name}
6772new file mode 100644
6773index 0000000..1111111
6774--- /dev/null
6775+++ b/{file_name}
6776@@ -0,0 +1 @@
6777+{line}
6778"
6779            ),
6780            Some("HEAD".into()),
6781        );
6782        let gene = Gene {
6783            id: gene_id.into(),
6784            signals: vec![signal.into()],
6785            strategy: vec![file_name.into()],
6786            validation: vec!["test".into()],
6787            state: AssetState::Promoted,
6788        };
6789        let capsule = Capsule {
6790            id: capsule_id.into(),
6791            gene_id: gene_id.into(),
6792            mutation_id: mutation_id.into(),
6793            run_id: run_id.into(),
6794            diff_hash: mutation.artifact.content_hash.clone(),
6795            confidence: 0.9,
6796            env,
6797            outcome: Outcome {
6798                success: true,
6799                validation_profile: "test".into(),
6800                validation_duration_ms: 1,
6801                changed_files: vec![file_name.into()],
6802                validator_hash: "validator-hash".into(),
6803                lines_changed: 1,
6804                replay_verified: false,
6805            },
6806            state: AssetState::Promoted,
6807        };
6808        EvolutionEnvelope::publish(
6809            sender_id,
6810            vec![
6811                NetworkAsset::EvolutionEvent {
6812                    event: EvolutionEvent::MutationDeclared { mutation },
6813                },
6814                NetworkAsset::Gene { gene: gene.clone() },
6815                NetworkAsset::Capsule {
6816                    capsule: capsule.clone(),
6817                },
6818                NetworkAsset::EvolutionEvent {
6819                    event: EvolutionEvent::CapsuleReleased {
6820                        capsule_id: capsule.id.clone(),
6821                        state: AssetState::Promoted,
6822                    },
6823                },
6824            ],
6825        )
6826    }
6827
6828    fn remote_publish_envelope_with_signals(
6829        sender_id: &str,
6830        run_id: &str,
6831        gene_id: &str,
6832        capsule_id: &str,
6833        mutation_id: &str,
6834        mutation_signals: Vec<String>,
6835        gene_signals: Vec<String>,
6836        file_name: &str,
6837        line: &str,
6838        env: EnvFingerprint,
6839    ) -> EvolutionEnvelope {
6840        let mutation = prepare_mutation(
6841            MutationIntent {
6842                id: mutation_id.into(),
6843                intent: format!("add {file_name}"),
6844                target: MutationTarget::Paths {
6845                    allow: vec![file_name.into()],
6846                },
6847                expected_effect: "replay should still validate".into(),
6848                risk: RiskLevel::Low,
6849                signals: mutation_signals,
6850                spec_id: None,
6851            },
6852            format!(
6853                "\
6854diff --git a/{file_name} b/{file_name}
6855new file mode 100644
6856index 0000000..1111111
6857--- /dev/null
6858+++ b/{file_name}
6859@@ -0,0 +1 @@
6860+{line}
6861"
6862            ),
6863            Some("HEAD".into()),
6864        );
6865        let gene = Gene {
6866            id: gene_id.into(),
6867            signals: gene_signals,
6868            strategy: vec![file_name.into()],
6869            validation: vec!["test".into()],
6870            state: AssetState::Promoted,
6871        };
6872        let capsule = Capsule {
6873            id: capsule_id.into(),
6874            gene_id: gene_id.into(),
6875            mutation_id: mutation_id.into(),
6876            run_id: run_id.into(),
6877            diff_hash: mutation.artifact.content_hash.clone(),
6878            confidence: 0.9,
6879            env,
6880            outcome: Outcome {
6881                success: true,
6882                validation_profile: "test".into(),
6883                validation_duration_ms: 1,
6884                changed_files: vec![file_name.into()],
6885                validator_hash: "validator-hash".into(),
6886                lines_changed: 1,
6887                replay_verified: false,
6888            },
6889            state: AssetState::Promoted,
6890        };
6891        EvolutionEnvelope::publish(
6892            sender_id,
6893            vec![
6894                NetworkAsset::EvolutionEvent {
6895                    event: EvolutionEvent::MutationDeclared { mutation },
6896                },
6897                NetworkAsset::Gene { gene: gene.clone() },
6898                NetworkAsset::Capsule {
6899                    capsule: capsule.clone(),
6900                },
6901                NetworkAsset::EvolutionEvent {
6902                    event: EvolutionEvent::CapsuleReleased {
6903                        capsule_id: capsule.id.clone(),
6904                        state: AssetState::Promoted,
6905                    },
6906                },
6907            ],
6908        )
6909    }
6910
6911    struct FixedValidator {
6912        success: bool,
6913    }
6914
6915    #[async_trait]
6916    impl Validator for FixedValidator {
6917        async fn run(
6918            &self,
6919            _receipt: &SandboxReceipt,
6920            plan: &ValidationPlan,
6921        ) -> Result<ValidationReport, ValidationError> {
6922            Ok(ValidationReport {
6923                success: self.success,
6924                duration_ms: 1,
6925                stages: Vec::new(),
6926                logs: if self.success {
6927                    format!("{} ok", plan.profile)
6928                } else {
6929                    format!("{} failed", plan.profile)
6930                },
6931            })
6932        }
6933    }
6934
6935    struct FailOnAppendStore {
6936        inner: JsonlEvolutionStore,
6937        fail_on_call: usize,
6938        call_count: Mutex<usize>,
6939    }
6940
6941    impl FailOnAppendStore {
6942        fn new(root_dir: std::path::PathBuf, fail_on_call: usize) -> Self {
6943            Self {
6944                inner: JsonlEvolutionStore::new(root_dir),
6945                fail_on_call,
6946                call_count: Mutex::new(0),
6947            }
6948        }
6949    }
6950
6951    impl EvolutionStore for FailOnAppendStore {
6952        fn append_event(&self, event: EvolutionEvent) -> Result<u64, EvolutionError> {
6953            let mut call_count = self
6954                .call_count
6955                .lock()
6956                .map_err(|_| EvolutionError::Io("test store lock poisoned".into()))?;
6957            *call_count += 1;
6958            if *call_count == self.fail_on_call {
6959                return Err(EvolutionError::Io("injected append failure".into()));
6960            }
6961            self.inner.append_event(event)
6962        }
6963
6964        fn scan(&self, from_seq: u64) -> Result<Vec<StoredEvolutionEvent>, EvolutionError> {
6965            self.inner.scan(from_seq)
6966        }
6967
6968        fn rebuild_projection(&self) -> Result<EvolutionProjection, EvolutionError> {
6969            self.inner.rebuild_projection()
6970        }
6971    }
6972
6973    #[test]
6974    fn coordination_planner_to_coder_handoff_is_deterministic() {
6975        let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
6976            root_goal: "ship feature".into(),
6977            primitive: CoordinationPrimitive::Sequential,
6978            tasks: vec![
6979                CoordinationTask {
6980                    id: "planner".into(),
6981                    role: AgentRole::Planner,
6982                    description: "split the work".into(),
6983                    depends_on: Vec::new(),
6984                },
6985                CoordinationTask {
6986                    id: "coder".into(),
6987                    role: AgentRole::Coder,
6988                    description: "implement the patch".into(),
6989                    depends_on: vec!["planner".into()],
6990                },
6991            ],
6992            timeout_ms: 5_000,
6993            max_retries: 0,
6994        });
6995
6996        assert_eq!(result.completed_tasks, vec!["planner", "coder"]);
6997        assert!(result.failed_tasks.is_empty());
6998        assert!(result.messages.iter().any(|message| {
6999            message.from_role == AgentRole::Planner
7000                && message.to_role == AgentRole::Coder
7001                && message.task_id == "coder"
7002        }));
7003    }
7004
7005    #[test]
7006    fn coordination_repair_runs_only_after_coder_failure() {
7007        let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
7008            root_goal: "fix broken implementation".into(),
7009            primitive: CoordinationPrimitive::Sequential,
7010            tasks: vec![
7011                CoordinationTask {
7012                    id: "coder".into(),
7013                    role: AgentRole::Coder,
7014                    description: "force-fail initial implementation".into(),
7015                    depends_on: Vec::new(),
7016                },
7017                CoordinationTask {
7018                    id: "repair".into(),
7019                    role: AgentRole::Repair,
7020                    description: "patch the failed implementation".into(),
7021                    depends_on: vec!["coder".into()],
7022                },
7023            ],
7024            timeout_ms: 5_000,
7025            max_retries: 0,
7026        });
7027
7028        assert_eq!(result.completed_tasks, vec!["repair"]);
7029        assert_eq!(result.failed_tasks, vec!["coder"]);
7030        assert!(result.messages.iter().any(|message| {
7031            message.from_role == AgentRole::Coder
7032                && message.to_role == AgentRole::Repair
7033                && message.task_id == "repair"
7034        }));
7035    }
7036
7037    #[test]
7038    fn coordination_optimizer_runs_after_successful_implementation_step() {
7039        let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
7040            root_goal: "ship optimized patch".into(),
7041            primitive: CoordinationPrimitive::Sequential,
7042            tasks: vec![
7043                CoordinationTask {
7044                    id: "coder".into(),
7045                    role: AgentRole::Coder,
7046                    description: "implement a working patch".into(),
7047                    depends_on: Vec::new(),
7048                },
7049                CoordinationTask {
7050                    id: "optimizer".into(),
7051                    role: AgentRole::Optimizer,
7052                    description: "tighten the implementation".into(),
7053                    depends_on: vec!["coder".into()],
7054                },
7055            ],
7056            timeout_ms: 5_000,
7057            max_retries: 0,
7058        });
7059
7060        assert_eq!(result.completed_tasks, vec!["coder", "optimizer"]);
7061        assert!(result.failed_tasks.is_empty());
7062    }
7063
7064    #[test]
7065    fn coordination_parallel_waves_preserve_sorted_merge_order() {
7066        let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
7067            root_goal: "parallelize safe tasks".into(),
7068            primitive: CoordinationPrimitive::Parallel,
7069            tasks: vec![
7070                CoordinationTask {
7071                    id: "z-task".into(),
7072                    role: AgentRole::Planner,
7073                    description: "analyze z".into(),
7074                    depends_on: Vec::new(),
7075                },
7076                CoordinationTask {
7077                    id: "a-task".into(),
7078                    role: AgentRole::Coder,
7079                    description: "implement a".into(),
7080                    depends_on: Vec::new(),
7081                },
7082                CoordinationTask {
7083                    id: "mid-task".into(),
7084                    role: AgentRole::Optimizer,
7085                    description: "polish after both".into(),
7086                    depends_on: vec!["z-task".into(), "a-task".into()],
7087                },
7088            ],
7089            timeout_ms: 5_000,
7090            max_retries: 0,
7091        });
7092
7093        assert_eq!(result.completed_tasks, vec!["a-task", "z-task", "mid-task"]);
7094        assert!(result.failed_tasks.is_empty());
7095    }
7096
7097    #[test]
7098    fn coordination_retries_stop_at_max_retries() {
7099        let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
7100            root_goal: "retry then stop".into(),
7101            primitive: CoordinationPrimitive::Sequential,
7102            tasks: vec![CoordinationTask {
7103                id: "coder".into(),
7104                role: AgentRole::Coder,
7105                description: "force-fail this task".into(),
7106                depends_on: Vec::new(),
7107            }],
7108            timeout_ms: 5_000,
7109            max_retries: 1,
7110        });
7111
7112        assert!(result.completed_tasks.is_empty());
7113        assert_eq!(result.failed_tasks, vec!["coder"]);
7114        assert_eq!(
7115            result
7116                .messages
7117                .iter()
7118                .filter(|message| message.task_id == "coder" && message.content.contains("failed"))
7119                .count(),
7120            2
7121        );
7122    }
7123
7124    #[test]
7125    fn coordination_conditional_mode_skips_downstream_tasks_on_failure() {
7126        let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
7127            root_goal: "skip blocked follow-up work".into(),
7128            primitive: CoordinationPrimitive::Conditional,
7129            tasks: vec![
7130                CoordinationTask {
7131                    id: "coder".into(),
7132                    role: AgentRole::Coder,
7133                    description: "force-fail the implementation".into(),
7134                    depends_on: Vec::new(),
7135                },
7136                CoordinationTask {
7137                    id: "optimizer".into(),
7138                    role: AgentRole::Optimizer,
7139                    description: "only optimize a successful implementation".into(),
7140                    depends_on: vec!["coder".into()],
7141                },
7142            ],
7143            timeout_ms: 5_000,
7144            max_retries: 0,
7145        });
7146
7147        assert!(result.completed_tasks.is_empty());
7148        assert_eq!(result.failed_tasks, vec!["coder"]);
7149        assert!(result.messages.iter().any(|message| {
7150            message.task_id == "optimizer"
7151                && message
7152                    .content
7153                    .contains("skipped due to failed dependency chain")
7154        }));
7155        assert!(!result
7156            .failed_tasks
7157            .iter()
7158            .any(|task_id| task_id == "optimizer"));
7159    }
7160
7161    #[tokio::test]
7162    async fn command_validator_aggregates_stage_reports() {
7163        let workspace = temp_workspace("validator");
7164        let receipt = SandboxReceipt {
7165            mutation_id: "m".into(),
7166            workdir: workspace,
7167            applied: true,
7168            changed_files: Vec::new(),
7169            patch_hash: "hash".into(),
7170            stdout_log: std::env::temp_dir().join("stdout.log"),
7171            stderr_log: std::env::temp_dir().join("stderr.log"),
7172        };
7173        let validator = CommandValidator::new(SandboxPolicy {
7174            allowed_programs: vec!["git".into()],
7175            max_duration_ms: 1_000,
7176            max_output_bytes: 1024,
7177            denied_env_prefixes: Vec::new(),
7178        });
7179        let report = validator
7180            .run(
7181                &receipt,
7182                &ValidationPlan {
7183                    profile: "test".into(),
7184                    stages: vec![ValidationStage::Command {
7185                        program: "git".into(),
7186                        args: vec!["--version".into()],
7187                        timeout_ms: 1_000,
7188                    }],
7189                },
7190            )
7191            .await
7192            .unwrap();
7193        assert_eq!(report.stages.len(), 1);
7194    }
7195
7196    #[tokio::test]
7197    async fn capture_successful_mutation_appends_capsule() {
7198        let (evo, store) = build_test_evo("capture", "run-1", command_validator());
7199        let capsule = evo
7200            .capture_successful_mutation(&"run-1".into(), sample_mutation())
7201            .await
7202            .unwrap();
7203        let events = store.scan(1).unwrap();
7204        assert!(events
7205            .iter()
7206            .any(|stored| matches!(stored.event, EvolutionEvent::CapsuleCommitted { .. })));
7207        assert!(!capsule.id.is_empty());
7208    }
7209
7210    #[tokio::test]
7211    async fn replay_hit_records_capsule_reused() {
7212        let (evo, store) = build_test_evo("replay", "run-2", command_validator());
7213        let capsule = evo
7214            .capture_successful_mutation(&"run-2".into(), sample_mutation())
7215            .await
7216            .unwrap();
7217        let replay_run_id = "run-replay".to_string();
7218        let decision = evo
7219            .replay_or_fallback_for_run(&replay_run_id, replay_input("missing readme"))
7220            .await
7221            .unwrap();
7222        assert!(decision.used_capsule);
7223        assert_eq!(decision.capsule_id, Some(capsule.id));
7224        assert!(!decision.detect_evidence.task_class_id.is_empty());
7225        assert!(!decision.detect_evidence.matched_signals.is_empty());
7226        assert!(decision.detect_evidence.mismatch_reasons.is_empty());
7227        assert!(!decision.select_evidence.candidates.is_empty());
7228        assert!(!decision.select_evidence.exact_match_lookup);
7229        assert_eq!(
7230            decision.select_evidence.selected_capsule_id.as_deref(),
7231            decision.capsule_id.as_deref()
7232        );
7233        assert!(store.scan(1).unwrap().iter().any(|stored| matches!(
7234            &stored.event,
7235            EvolutionEvent::CapsuleReused {
7236                run_id,
7237                replay_run_id: Some(current_replay_run_id),
7238                ..
7239            } if run_id == "run-2" && current_replay_run_id == &replay_run_id
7240        )));
7241    }
7242
7243    #[tokio::test]
7244    async fn legacy_replay_executor_api_preserves_original_capsule_run_id() {
7245        let capture_run_id = "run-legacy-capture".to_string();
7246        let (evo, store) = build_test_evo("replay-legacy", &capture_run_id, command_validator());
7247        let capsule = evo
7248            .capture_successful_mutation(&capture_run_id, sample_mutation())
7249            .await
7250            .unwrap();
7251        let executor = StoreReplayExecutor {
7252            sandbox: evo.sandbox.clone(),
7253            validator: evo.validator.clone(),
7254            store: evo.store.clone(),
7255            selector: evo.selector.clone(),
7256            governor: evo.governor.clone(),
7257            economics: Some(evo.economics.clone()),
7258            remote_publishers: Some(evo.remote_publishers.clone()),
7259            stake_policy: evo.stake_policy.clone(),
7260        };
7261
7262        let decision = executor
7263            .try_replay(
7264                &replay_input("missing readme"),
7265                &evo.sandbox_policy,
7266                &evo.validation_plan,
7267            )
7268            .await
7269            .unwrap();
7270
7271        assert!(decision.used_capsule);
7272        assert_eq!(decision.capsule_id, Some(capsule.id));
7273        assert!(store.scan(1).unwrap().iter().any(|stored| matches!(
7274            &stored.event,
7275            EvolutionEvent::CapsuleReused {
7276                run_id,
7277                replay_run_id: None,
7278                ..
7279            } if run_id == &capture_run_id
7280        )));
7281    }
7282
7283    #[tokio::test]
7284    async fn metrics_snapshot_tracks_replay_promotion_and_revocation_signals() {
7285        let (evo, _) = build_test_evo("metrics", "run-metrics", command_validator());
7286        let capsule = evo
7287            .capture_successful_mutation(&"run-metrics".into(), sample_mutation())
7288            .await
7289            .unwrap();
7290        let decision = evo
7291            .replay_or_fallback(replay_input("missing readme"))
7292            .await
7293            .unwrap();
7294        assert!(decision.used_capsule);
7295
7296        evo.revoke_assets(&RevokeNotice {
7297            sender_id: "node-metrics".into(),
7298            asset_ids: vec![capsule.id.clone()],
7299            reason: "manual test revoke".into(),
7300        })
7301        .unwrap();
7302
7303        let snapshot = evo.metrics_snapshot().unwrap();
7304        assert_eq!(snapshot.replay_attempts_total, 1);
7305        assert_eq!(snapshot.replay_success_total, 1);
7306        assert_eq!(snapshot.replay_success_rate, 1.0);
7307        assert_eq!(snapshot.confidence_revalidations_total, 0);
7308        assert_eq!(snapshot.replay_reasoning_avoided_total, 1);
7309        assert_eq!(
7310            snapshot.reasoning_avoided_tokens_total,
7311            decision.economics_evidence.reasoning_avoided_tokens
7312        );
7313        assert_eq!(snapshot.replay_fallback_cost_total, 0);
7314        assert_eq!(snapshot.replay_roi, 1.0);
7315        assert_eq!(snapshot.replay_task_classes.len(), 1);
7316        assert_eq!(snapshot.replay_task_classes[0].replay_success_total, 1);
7317        assert_eq!(snapshot.replay_task_classes[0].replay_failure_total, 0);
7318        assert_eq!(
7319            snapshot.replay_task_classes[0].reasoning_steps_avoided_total,
7320            1
7321        );
7322        assert_eq!(
7323            snapshot.replay_task_classes[0].replay_fallback_cost_total,
7324            0
7325        );
7326        assert_eq!(snapshot.replay_task_classes[0].replay_roi, 1.0);
7327        assert!(snapshot.replay_sources.is_empty());
7328        assert_eq!(snapshot.confidence_revalidations_total, 0);
7329        assert_eq!(snapshot.mutation_declared_total, 1);
7330        assert_eq!(snapshot.promoted_mutations_total, 1);
7331        assert_eq!(snapshot.promotion_ratio, 1.0);
7332        assert_eq!(snapshot.gene_revocations_total, 1);
7333        assert_eq!(snapshot.mutation_velocity_last_hour, 1);
7334        assert_eq!(snapshot.revoke_frequency_last_hour, 1);
7335        assert_eq!(snapshot.promoted_genes, 0);
7336        assert_eq!(snapshot.promoted_capsules, 0);
7337
7338        let rendered = evo.render_metrics_prometheus().unwrap();
7339        assert!(rendered.contains("oris_evolution_replay_reasoning_avoided_total 1"));
7340        assert!(rendered.contains("oris_evolution_reasoning_avoided_tokens_total"));
7341        assert!(rendered.contains("oris_evolution_replay_fallback_cost_total"));
7342        assert!(rendered.contains("oris_evolution_replay_roi 1.000000"));
7343        assert!(rendered.contains("oris_evolution_replay_utilization_by_task_class_total"));
7344        assert!(rendered.contains("oris_evolution_replay_reasoning_avoided_by_task_class_total"));
7345        assert!(rendered.contains("oris_evolution_replay_success_rate 1.000000"));
7346        assert!(rendered.contains("oris_evolution_confidence_revalidations_total 0"));
7347        assert!(rendered.contains("oris_evolution_promotion_ratio 1.000000"));
7348        assert!(rendered.contains("oris_evolution_revoke_frequency_last_hour 1"));
7349        assert!(rendered.contains("oris_evolution_mutation_velocity_last_hour 1"));
7350        assert!(rendered.contains("oris_evolution_health 1"));
7351    }
7352
7353    #[tokio::test]
7354    async fn replay_roi_release_gate_summary_aggregates_task_class_and_remote_source() {
7355        let (evo, _) = build_test_evo("roi-summary", "run-roi-summary", command_validator());
7356        let envelope = remote_publish_envelope(
7357            "node-roi",
7358            "run-remote-roi",
7359            "gene-roi",
7360            "capsule-roi",
7361            "mutation-roi",
7362            "roi-signal",
7363            "ROI.md",
7364            "# roi",
7365        );
7366        evo.import_remote_envelope(&envelope).unwrap();
7367
7368        let miss = evo
7369            .replay_or_fallback(replay_input("entropy-hash-12345-no-overlap"))
7370            .await
7371            .unwrap();
7372        assert!(!miss.used_capsule);
7373        assert!(miss.fallback_to_planner);
7374        assert!(miss.select_evidence.candidates.is_empty());
7375        assert!(miss
7376            .detect_evidence
7377            .mismatch_reasons
7378            .iter()
7379            .any(|reason| reason == "no_candidate_after_select"));
7380
7381        let hit = evo
7382            .replay_or_fallback(replay_input("roi-signal"))
7383            .await
7384            .unwrap();
7385        assert!(hit.used_capsule);
7386        assert!(!hit.select_evidence.candidates.is_empty());
7387        assert_eq!(
7388            hit.select_evidence.selected_capsule_id.as_deref(),
7389            hit.capsule_id.as_deref()
7390        );
7391
7392        let summary = evo.replay_roi_release_gate_summary(60 * 60).unwrap();
7393        assert_eq!(summary.replay_attempts_total, 2);
7394        assert_eq!(summary.replay_success_total, 1);
7395        assert_eq!(summary.replay_failure_total, 1);
7396        assert!(summary.reasoning_avoided_tokens_total > 0);
7397        assert!(summary.replay_fallback_cost_total > 0);
7398        assert!(summary
7399            .replay_task_classes
7400            .iter()
7401            .any(|entry| { entry.replay_success_total == 1 && entry.replay_failure_total == 0 }));
7402        assert!(summary.replay_sources.iter().any(|source| {
7403            source.source_sender_id == "node-roi" && source.replay_success_total == 1
7404        }));
7405
7406        let rendered = evo
7407            .render_replay_roi_release_gate_summary_json(60 * 60)
7408            .unwrap();
7409        assert!(rendered.contains("\"replay_attempts_total\": 2"));
7410        assert!(rendered.contains("\"source_sender_id\": \"node-roi\""));
7411    }
7412
7413    #[tokio::test]
7414    async fn replay_roi_release_gate_summary_contract_exposes_core_metrics_and_fail_closed_defaults(
7415    ) {
7416        let (evo, _) = build_test_evo("roi-contract", "run-roi-contract", command_validator());
7417        let envelope = remote_publish_envelope(
7418            "node-contract",
7419            "run-remote-contract",
7420            "gene-contract",
7421            "capsule-contract",
7422            "mutation-contract",
7423            "contract-signal",
7424            "CONTRACT.md",
7425            "# contract",
7426        );
7427        evo.import_remote_envelope(&envelope).unwrap();
7428
7429        let miss = evo
7430            .replay_or_fallback(replay_input("entropy-hash-contract-no-overlap"))
7431            .await
7432            .unwrap();
7433        assert!(!miss.used_capsule);
7434        assert!(miss.fallback_to_planner);
7435
7436        let hit = evo
7437            .replay_or_fallback(replay_input("contract-signal"))
7438            .await
7439            .unwrap();
7440        assert!(hit.used_capsule);
7441
7442        let summary = evo.replay_roi_release_gate_summary(60 * 60).unwrap();
7443        let contract = evo
7444            .replay_roi_release_gate_contract(60 * 60, ReplayRoiReleaseGateThresholds::default())
7445            .unwrap();
7446
7447        assert_eq!(contract.input.replay_attempts_total, 2);
7448        assert_eq!(contract.input.replay_success_total, 1);
7449        assert_eq!(contract.input.replay_failure_total, 1);
7450        assert_eq!(
7451            contract.input.reasoning_avoided_tokens,
7452            summary.reasoning_avoided_tokens_total
7453        );
7454        assert_eq!(
7455            contract.input.replay_fallback_cost_total,
7456            summary.replay_fallback_cost_total
7457        );
7458        assert!((contract.input.replay_hit_rate - 0.5).abs() < f64::EPSILON);
7459        assert!((contract.input.false_replay_rate - 0.5).abs() < f64::EPSILON);
7460        assert!((contract.input.replay_roi - summary.replay_roi).abs() < f64::EPSILON);
7461        assert!(contract.input.replay_safety);
7462        assert_eq!(
7463            contract.input.aggregation_dimensions,
7464            REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
7465                .iter()
7466                .map(|dimension| (*dimension).to_string())
7467                .collect::<Vec<_>>()
7468        );
7469        assert_eq!(
7470            contract.input.thresholds,
7471            ReplayRoiReleaseGateThresholds::default()
7472        );
7473        assert_eq!(
7474            contract.input.fail_closed_policy,
7475            ReplayRoiReleaseGateFailClosedPolicy::default()
7476        );
7477        assert_eq!(
7478            contract.output.status,
7479            ReplayRoiReleaseGateStatus::FailClosed
7480        );
7481        assert!(contract
7482            .output
7483            .failed_checks
7484            .iter()
7485            .any(|check| check == "min_replay_attempts_below_threshold"));
7486        assert!(contract
7487            .output
7488            .failed_checks
7489            .iter()
7490            .any(|check| check == "replay_hit_rate_below_threshold"));
7491        assert!(contract
7492            .output
7493            .failed_checks
7494            .iter()
7495            .any(|check| check == "false_replay_rate_above_threshold"));
7496        assert!(contract
7497            .output
7498            .evidence_refs
7499            .iter()
7500            .any(|evidence| evidence == "replay_roi_release_gate_summary"));
7501        assert!(contract.output.summary.contains("release gate fail_closed"));
7502    }
7503
7504    #[tokio::test]
7505    async fn replay_roi_release_gate_summary_contract_accepts_custom_thresholds_and_json() {
7506        let (evo, _) = build_test_evo(
7507            "roi-contract-thresholds",
7508            "run-roi-contract-thresholds",
7509            command_validator(),
7510        );
7511        let thresholds = ReplayRoiReleaseGateThresholds {
7512            min_replay_attempts: 8,
7513            min_replay_hit_rate: 0.75,
7514            max_false_replay_rate: 0.10,
7515            min_reasoning_avoided_tokens: 600,
7516            min_replay_roi: 0.30,
7517            require_replay_safety: true,
7518        };
7519        let contract = evo
7520            .replay_roi_release_gate_contract(60 * 60, thresholds.clone())
7521            .unwrap();
7522        assert_eq!(contract.input.thresholds, thresholds.clone());
7523        assert_eq!(contract.input.replay_attempts_total, 0);
7524        assert_eq!(contract.input.replay_hit_rate, 0.0);
7525        assert_eq!(contract.input.false_replay_rate, 0.0);
7526        assert!(!contract.input.replay_safety_signal.has_replay_activity);
7527        assert!(!contract.input.replay_safety);
7528        assert_eq!(
7529            contract.output.status,
7530            ReplayRoiReleaseGateStatus::Indeterminate
7531        );
7532        assert!(contract
7533            .output
7534            .failed_checks
7535            .iter()
7536            .any(|check| check == "missing_replay_attempts"));
7537        assert!(contract
7538            .output
7539            .summary
7540            .contains("indeterminate (fail-closed)"));
7541
7542        let rendered = evo
7543            .render_replay_roi_release_gate_contract_json(60 * 60, thresholds)
7544            .unwrap();
7545        assert!(rendered.contains("\"min_replay_attempts\": 8"));
7546        assert!(rendered.contains("\"min_replay_hit_rate\": 0.75"));
7547        assert!(rendered.contains("\"status\": \"indeterminate\""));
7548    }
7549
7550    #[tokio::test]
7551    async fn replay_roi_release_gate_summary_window_boundary_filters_old_events() {
7552        let (evo, _) = build_test_evo("roi-window", "run-roi-window", command_validator());
7553        let envelope = remote_publish_envelope(
7554            "node-window",
7555            "run-remote-window",
7556            "gene-window",
7557            "capsule-window",
7558            "mutation-window",
7559            "window-signal",
7560            "WINDOW.md",
7561            "# window",
7562        );
7563        evo.import_remote_envelope(&envelope).unwrap();
7564
7565        let miss = evo
7566            .replay_or_fallback(replay_input("window-no-match-signal"))
7567            .await
7568            .unwrap();
7569        assert!(!miss.used_capsule);
7570        assert!(miss.fallback_to_planner);
7571
7572        let first_hit = evo
7573            .replay_or_fallback(replay_input("window-signal"))
7574            .await
7575            .unwrap();
7576        assert!(first_hit.used_capsule);
7577
7578        std::thread::sleep(std::time::Duration::from_secs(2));
7579
7580        let second_hit = evo
7581            .replay_or_fallback(replay_input("window-signal"))
7582            .await
7583            .unwrap();
7584        assert!(second_hit.used_capsule);
7585
7586        let narrow = evo.replay_roi_release_gate_summary(1).unwrap();
7587        assert_eq!(narrow.replay_attempts_total, 1);
7588        assert_eq!(narrow.replay_success_total, 1);
7589        assert_eq!(narrow.replay_failure_total, 0);
7590
7591        let all = evo.replay_roi_release_gate_summary(0).unwrap();
7592        assert_eq!(all.replay_attempts_total, 3);
7593        assert_eq!(all.replay_success_total, 2);
7594        assert_eq!(all.replay_failure_total, 1);
7595    }
7596
7597    fn fixed_release_gate_pass_fixture() -> ReplayRoiReleaseGateInputContract {
7598        ReplayRoiReleaseGateInputContract {
7599            generated_at: "2026-03-13T00:00:00Z".to_string(),
7600            window_seconds: 86_400,
7601            aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
7602                .iter()
7603                .map(|dimension| (*dimension).to_string())
7604                .collect(),
7605            replay_attempts_total: 4,
7606            replay_success_total: 3,
7607            replay_failure_total: 1,
7608            replay_hit_rate: 0.75,
7609            false_replay_rate: 0.25,
7610            reasoning_avoided_tokens: 480,
7611            replay_fallback_cost_total: 64,
7612            replay_roi: compute_replay_roi(480, 64),
7613            replay_safety: true,
7614            replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
7615                fail_closed_default: true,
7616                rollback_ready: true,
7617                audit_trail_complete: true,
7618                has_replay_activity: true,
7619            },
7620            thresholds: ReplayRoiReleaseGateThresholds::default(),
7621            fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
7622        }
7623    }
7624
7625    fn fixed_release_gate_fail_fixture() -> ReplayRoiReleaseGateInputContract {
7626        ReplayRoiReleaseGateInputContract {
7627            generated_at: "2026-03-13T00:00:00Z".to_string(),
7628            window_seconds: 86_400,
7629            aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
7630                .iter()
7631                .map(|dimension| (*dimension).to_string())
7632                .collect(),
7633            replay_attempts_total: 10,
7634            replay_success_total: 4,
7635            replay_failure_total: 6,
7636            replay_hit_rate: 0.4,
7637            false_replay_rate: 0.6,
7638            reasoning_avoided_tokens: 80,
7639            replay_fallback_cost_total: 400,
7640            replay_roi: compute_replay_roi(80, 400),
7641            replay_safety: false,
7642            replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
7643                fail_closed_default: true,
7644                rollback_ready: true,
7645                audit_trail_complete: true,
7646                has_replay_activity: true,
7647            },
7648            thresholds: ReplayRoiReleaseGateThresholds::default(),
7649            fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
7650        }
7651    }
7652
7653    fn fixed_release_gate_borderline_fixture() -> ReplayRoiReleaseGateInputContract {
7654        ReplayRoiReleaseGateInputContract {
7655            generated_at: "2026-03-13T00:00:00Z".to_string(),
7656            window_seconds: 3_600,
7657            aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
7658                .iter()
7659                .map(|dimension| (*dimension).to_string())
7660                .collect(),
7661            replay_attempts_total: 4,
7662            replay_success_total: 3,
7663            replay_failure_total: 1,
7664            replay_hit_rate: 0.75,
7665            false_replay_rate: 0.25,
7666            reasoning_avoided_tokens: 192,
7667            replay_fallback_cost_total: 173,
7668            replay_roi: 0.05,
7669            replay_safety: true,
7670            replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
7671                fail_closed_default: true,
7672                rollback_ready: true,
7673                audit_trail_complete: true,
7674                has_replay_activity: true,
7675            },
7676            thresholds: ReplayRoiReleaseGateThresholds {
7677                min_replay_attempts: 4,
7678                min_replay_hit_rate: 0.75,
7679                max_false_replay_rate: 0.25,
7680                min_reasoning_avoided_tokens: 192,
7681                min_replay_roi: 0.05,
7682                require_replay_safety: true,
7683            },
7684            fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
7685        }
7686    }
7687
7688    #[test]
7689    fn replay_roi_release_gate_summary_fixed_fixtures_cover_pass_fail_and_borderline() {
7690        let pass =
7691            evaluate_replay_roi_release_gate_contract_input(&fixed_release_gate_pass_fixture());
7692        let fail =
7693            evaluate_replay_roi_release_gate_contract_input(&fixed_release_gate_fail_fixture());
7694        let borderline = evaluate_replay_roi_release_gate_contract_input(
7695            &fixed_release_gate_borderline_fixture(),
7696        );
7697
7698        assert_eq!(pass.status, ReplayRoiReleaseGateStatus::Pass);
7699        assert!(pass.failed_checks.is_empty());
7700        assert_eq!(fail.status, ReplayRoiReleaseGateStatus::FailClosed);
7701        assert!(!fail.failed_checks.is_empty());
7702        assert_eq!(borderline.status, ReplayRoiReleaseGateStatus::Pass);
7703        assert!(borderline.failed_checks.is_empty());
7704    }
7705
7706    #[test]
7707    fn replay_roi_release_gate_summary_machine_readable_output_is_stable_and_sorted() {
7708        let output =
7709            evaluate_replay_roi_release_gate_contract_input(&fixed_release_gate_fail_fixture());
7710
7711        assert_eq!(
7712            output.failed_checks,
7713            vec![
7714                "false_replay_rate_above_threshold".to_string(),
7715                "reasoning_avoided_tokens_below_threshold".to_string(),
7716                "replay_hit_rate_below_threshold".to_string(),
7717                "replay_roi_below_threshold".to_string(),
7718                "replay_safety_required".to_string(),
7719            ]
7720        );
7721        assert_eq!(
7722            output.evidence_refs,
7723            vec![
7724                "generated_at:2026-03-13T00:00:00Z".to_string(),
7725                "metric:false_replay_rate".to_string(),
7726                "metric:reasoning_avoided_tokens".to_string(),
7727                "metric:replay_hit_rate".to_string(),
7728                "metric:replay_roi".to_string(),
7729                "metric:replay_safety".to_string(),
7730                "replay_roi_release_gate_summary".to_string(),
7731                "threshold:max_false_replay_rate".to_string(),
7732                "threshold:min_reasoning_avoided_tokens".to_string(),
7733                "threshold:min_replay_hit_rate".to_string(),
7734                "threshold:min_replay_roi".to_string(),
7735                "threshold:require_replay_safety".to_string(),
7736                "window_seconds:86400".to_string(),
7737            ]
7738        );
7739
7740        let rendered = serde_json::to_string(&output).unwrap();
7741        assert!(rendered.starts_with("{\"status\":\"fail_closed\",\"failed_checks\":"));
7742        assert_eq!(rendered, serde_json::to_string(&output).unwrap());
7743    }
7744
7745    #[test]
7746    fn replay_roi_release_gate_summary_evaluator_passes_with_threshold_compliance() {
7747        let input = ReplayRoiReleaseGateInputContract {
7748            generated_at: Utc::now().to_rfc3339(),
7749            window_seconds: 86_400,
7750            aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
7751                .iter()
7752                .map(|dimension| (*dimension).to_string())
7753                .collect(),
7754            replay_attempts_total: 10,
7755            replay_success_total: 9,
7756            replay_failure_total: 1,
7757            replay_hit_rate: 0.9,
7758            false_replay_rate: 0.1,
7759            reasoning_avoided_tokens: 960,
7760            replay_fallback_cost_total: 64,
7761            replay_roi: compute_replay_roi(960, 64),
7762            replay_safety: true,
7763            replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
7764                fail_closed_default: true,
7765                rollback_ready: true,
7766                audit_trail_complete: true,
7767                has_replay_activity: true,
7768            },
7769            thresholds: ReplayRoiReleaseGateThresholds::default(),
7770            fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
7771        };
7772
7773        let output = evaluate_replay_roi_release_gate_contract_input(&input);
7774        assert_eq!(output.status, ReplayRoiReleaseGateStatus::Pass);
7775        assert!(output.failed_checks.is_empty());
7776        assert!(output.summary.contains("release gate pass"));
7777    }
7778
7779    #[test]
7780    fn replay_roi_release_gate_summary_evaluator_fail_closed_on_threshold_violations() {
7781        let input = ReplayRoiReleaseGateInputContract {
7782            generated_at: Utc::now().to_rfc3339(),
7783            window_seconds: 86_400,
7784            aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
7785                .iter()
7786                .map(|dimension| (*dimension).to_string())
7787                .collect(),
7788            replay_attempts_total: 10,
7789            replay_success_total: 4,
7790            replay_failure_total: 6,
7791            replay_hit_rate: 0.4,
7792            false_replay_rate: 0.6,
7793            reasoning_avoided_tokens: 80,
7794            replay_fallback_cost_total: 400,
7795            replay_roi: compute_replay_roi(80, 400),
7796            replay_safety: false,
7797            replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
7798                fail_closed_default: true,
7799                rollback_ready: true,
7800                audit_trail_complete: true,
7801                has_replay_activity: true,
7802            },
7803            thresholds: ReplayRoiReleaseGateThresholds::default(),
7804            fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
7805        };
7806
7807        let output = evaluate_replay_roi_release_gate_contract_input(&input);
7808        assert_eq!(output.status, ReplayRoiReleaseGateStatus::FailClosed);
7809        assert!(output
7810            .failed_checks
7811            .iter()
7812            .any(|check| check == "replay_hit_rate_below_threshold"));
7813        assert!(output
7814            .failed_checks
7815            .iter()
7816            .any(|check| check == "false_replay_rate_above_threshold"));
7817        assert!(output
7818            .failed_checks
7819            .iter()
7820            .any(|check| check == "replay_roi_below_threshold"));
7821        assert!(output.summary.contains("release gate fail_closed"));
7822    }
7823
7824    #[test]
7825    fn replay_roi_release_gate_summary_evaluator_marks_missing_data_indeterminate() {
7826        let input = ReplayRoiReleaseGateInputContract {
7827            generated_at: String::new(),
7828            window_seconds: 86_400,
7829            aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
7830                .iter()
7831                .map(|dimension| (*dimension).to_string())
7832                .collect(),
7833            replay_attempts_total: 0,
7834            replay_success_total: 0,
7835            replay_failure_total: 0,
7836            replay_hit_rate: 0.0,
7837            false_replay_rate: 0.0,
7838            reasoning_avoided_tokens: 0,
7839            replay_fallback_cost_total: 0,
7840            replay_roi: 0.0,
7841            replay_safety: false,
7842            replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
7843                fail_closed_default: true,
7844                rollback_ready: true,
7845                audit_trail_complete: true,
7846                has_replay_activity: false,
7847            },
7848            thresholds: ReplayRoiReleaseGateThresholds::default(),
7849            fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
7850        };
7851
7852        let output = evaluate_replay_roi_release_gate_contract_input(&input);
7853        assert_eq!(output.status, ReplayRoiReleaseGateStatus::Indeterminate);
7854        assert!(output
7855            .failed_checks
7856            .iter()
7857            .any(|check| check == "missing_generated_at"));
7858        assert!(output
7859            .failed_checks
7860            .iter()
7861            .any(|check| check == "missing_replay_attempts"));
7862        assert!(output
7863            .summary
7864            .contains("release gate indeterminate (fail-closed)"));
7865    }
7866
7867    #[test]
7868    fn stale_replay_targets_require_confidence_revalidation() {
7869        let now = Utc::now();
7870        let projection = EvolutionProjection {
7871            genes: vec![Gene {
7872                id: "gene-stale".into(),
7873                signals: vec!["missing readme".into()],
7874                strategy: vec!["README.md".into()],
7875                validation: vec!["test".into()],
7876                state: AssetState::Promoted,
7877            }],
7878            capsules: vec![Capsule {
7879                id: "capsule-stale".into(),
7880                gene_id: "gene-stale".into(),
7881                mutation_id: "mutation-stale".into(),
7882                run_id: "run-stale".into(),
7883                diff_hash: "hash".into(),
7884                confidence: 0.8,
7885                env: replay_input("missing readme").env,
7886                outcome: Outcome {
7887                    success: true,
7888                    validation_profile: "test".into(),
7889                    validation_duration_ms: 1,
7890                    changed_files: vec!["README.md".into()],
7891                    validator_hash: "validator".into(),
7892                    lines_changed: 1,
7893                    replay_verified: false,
7894                },
7895                state: AssetState::Promoted,
7896            }],
7897            reuse_counts: BTreeMap::from([("gene-stale".into(), 1)]),
7898            attempt_counts: BTreeMap::from([("gene-stale".into(), 1)]),
7899            last_updated_at: BTreeMap::from([(
7900                "gene-stale".into(),
7901                (now - Duration::hours(48)).to_rfc3339(),
7902            )]),
7903            spec_ids_by_gene: BTreeMap::new(),
7904        };
7905
7906        let targets = stale_replay_revalidation_targets(&projection, now);
7907
7908        assert_eq!(targets.len(), 1);
7909        assert_eq!(targets[0].gene_id, "gene-stale");
7910        assert_eq!(targets[0].capsule_ids, vec!["capsule-stale".to_string()]);
7911        assert!(targets[0].decayed_confidence < MIN_REPLAY_CONFIDENCE);
7912    }
7913
7914    #[tokio::test]
7915    async fn remote_replay_prefers_closest_environment_match() {
7916        let (evo, _) = build_test_evo("remote-env", "run-remote-env", command_validator());
7917        let input = replay_input("env-signal");
7918
7919        let envelope_a = remote_publish_envelope_with_env(
7920            "node-a",
7921            "run-remote-a",
7922            "gene-a",
7923            "capsule-a",
7924            "mutation-a",
7925            "env-signal",
7926            "A.md",
7927            "# from a",
7928            input.env.clone(),
7929        );
7930        let envelope_b = remote_publish_envelope_with_env(
7931            "node-b",
7932            "run-remote-b",
7933            "gene-b",
7934            "capsule-b",
7935            "mutation-b",
7936            "env-signal",
7937            "B.md",
7938            "# from b",
7939            EnvFingerprint {
7940                rustc_version: "old-rustc".into(),
7941                cargo_lock_hash: "other-lock".into(),
7942                target_triple: "aarch64-apple-darwin".into(),
7943                os: "linux".into(),
7944            },
7945        );
7946
7947        evo.import_remote_envelope(&envelope_a).unwrap();
7948        evo.import_remote_envelope(&envelope_b).unwrap();
7949
7950        let decision = evo.replay_or_fallback(input).await.unwrap();
7951
7952        assert!(decision.used_capsule);
7953        assert_eq!(decision.capsule_id, Some("capsule-a".into()));
7954        assert!(!decision.fallback_to_planner);
7955    }
7956
7957    #[test]
7958    fn remote_cold_start_scoring_caps_distinct_query_coverage() {
7959        let (evo, _) = build_test_evo("remote-score", "run-remote-score", command_validator());
7960        let input = replay_input("missing readme");
7961
7962        let exact = remote_publish_envelope_with_signals(
7963            "node-exact",
7964            "run-remote-exact",
7965            "gene-exact",
7966            "capsule-exact",
7967            "mutation-exact",
7968            vec!["missing readme".into()],
7969            vec!["missing readme".into()],
7970            "EXACT.md",
7971            "# exact",
7972            input.env.clone(),
7973        );
7974        let overlapping = remote_publish_envelope_with_signals(
7975            "node-overlap",
7976            "run-remote-overlap",
7977            "gene-overlap",
7978            "capsule-overlap",
7979            "mutation-overlap",
7980            vec!["missing readme".into()],
7981            vec!["missing".into(), "readme".into()],
7982            "OVERLAP.md",
7983            "# overlap",
7984            input.env.clone(),
7985        );
7986
7987        evo.import_remote_envelope(&exact).unwrap();
7988        evo.import_remote_envelope(&overlapping).unwrap();
7989
7990        let candidates = quarantined_remote_exact_match_candidates(evo.store.as_ref(), &input);
7991        let exact_candidate = candidates
7992            .iter()
7993            .find(|candidate| candidate.gene.id == "gene-exact")
7994            .unwrap();
7995        let overlap_candidate = candidates
7996            .iter()
7997            .find(|candidate| candidate.gene.id == "gene-overlap")
7998            .unwrap();
7999
8000        assert_eq!(exact_candidate.score, 1.0);
8001        assert_eq!(overlap_candidate.score, 1.0);
8002        assert!(candidates.iter().all(|candidate| candidate.score <= 1.0));
8003    }
8004
8005    #[test]
8006    fn exact_match_candidates_respect_spec_linked_events() {
8007        let (evo, _) = build_test_evo(
8008            "spec-linked-filter",
8009            "run-spec-linked-filter",
8010            command_validator(),
8011        );
8012        let mut input = replay_input("missing readme");
8013        input.spec_id = Some("spec-readme".into());
8014
8015        let mut mutation = sample_mutation();
8016        mutation.intent.id = "mutation-spec-linked".into();
8017        mutation.intent.spec_id = None;
8018        let gene = Gene {
8019            id: "gene-spec-linked".into(),
8020            signals: vec!["missing readme".into()],
8021            strategy: vec!["README.md".into()],
8022            validation: vec!["test".into()],
8023            state: AssetState::Promoted,
8024        };
8025        let capsule = Capsule {
8026            id: "capsule-spec-linked".into(),
8027            gene_id: gene.id.clone(),
8028            mutation_id: mutation.intent.id.clone(),
8029            run_id: "run-spec-linked".into(),
8030            diff_hash: mutation.artifact.content_hash.clone(),
8031            confidence: 0.9,
8032            env: input.env.clone(),
8033            outcome: Outcome {
8034                success: true,
8035                validation_profile: "test".into(),
8036                validation_duration_ms: 1,
8037                changed_files: vec!["README.md".into()],
8038                validator_hash: "validator-hash".into(),
8039                lines_changed: 1,
8040                replay_verified: false,
8041            },
8042            state: AssetState::Promoted,
8043        };
8044
8045        evo.store
8046            .append_event(EvolutionEvent::MutationDeclared { mutation })
8047            .unwrap();
8048        evo.store
8049            .append_event(EvolutionEvent::GeneProjected { gene })
8050            .unwrap();
8051        evo.store
8052            .append_event(EvolutionEvent::CapsuleCommitted { capsule })
8053            .unwrap();
8054        evo.store
8055            .append_event(EvolutionEvent::SpecLinked {
8056                mutation_id: "mutation-spec-linked".into(),
8057                spec_id: "spec-readme".into(),
8058            })
8059            .unwrap();
8060
8061        let candidates = exact_match_candidates(evo.store.as_ref(), &input);
8062        assert_eq!(candidates.len(), 1);
8063        assert_eq!(candidates[0].gene.id, "gene-spec-linked");
8064    }
8065
8066    #[tokio::test]
8067    async fn remote_capsule_advances_from_quarantine_to_shadow_then_promoted() {
8068        let (evo, store) = build_test_evo(
8069            "remote-quarantine",
8070            "run-remote-quarantine",
8071            command_validator(),
8072        );
8073        let envelope = remote_publish_envelope(
8074            "node-remote",
8075            "run-remote-quarantine",
8076            "gene-remote",
8077            "capsule-remote",
8078            "mutation-remote",
8079            "remote-signal",
8080            "REMOTE.md",
8081            "# from remote",
8082        );
8083
8084        evo.import_remote_envelope(&envelope).unwrap();
8085
8086        let before_replay = store.rebuild_projection().unwrap();
8087        let imported_gene = before_replay
8088            .genes
8089            .iter()
8090            .find(|gene| gene.id == "gene-remote")
8091            .unwrap();
8092        let imported_capsule = before_replay
8093            .capsules
8094            .iter()
8095            .find(|capsule| capsule.id == "capsule-remote")
8096            .unwrap();
8097        assert_eq!(imported_gene.state, AssetState::Quarantined);
8098        assert_eq!(imported_capsule.state, AssetState::Quarantined);
8099        let exported_before_replay =
8100            export_promoted_assets_from_store(store.as_ref(), "node-local").unwrap();
8101        assert!(exported_before_replay.assets.is_empty());
8102
8103        let first_decision = evo
8104            .replay_or_fallback(replay_input("remote-signal"))
8105            .await
8106            .unwrap();
8107
8108        assert!(first_decision.used_capsule);
8109        assert_eq!(first_decision.capsule_id, Some("capsule-remote".into()));
8110
8111        let after_first_replay = store.rebuild_projection().unwrap();
8112        let shadow_gene = after_first_replay
8113            .genes
8114            .iter()
8115            .find(|gene| gene.id == "gene-remote")
8116            .unwrap();
8117        let shadow_capsule = after_first_replay
8118            .capsules
8119            .iter()
8120            .find(|capsule| capsule.id == "capsule-remote")
8121            .unwrap();
8122        assert_eq!(shadow_gene.state, AssetState::ShadowValidated);
8123        assert_eq!(shadow_capsule.state, AssetState::ShadowValidated);
8124        let exported_after_first_replay =
8125            export_promoted_assets_from_store(store.as_ref(), "node-local").unwrap();
8126        assert!(exported_after_first_replay.assets.is_empty());
8127
8128        let second_decision = evo
8129            .replay_or_fallback(replay_input("remote-signal"))
8130            .await
8131            .unwrap();
8132        assert!(second_decision.used_capsule);
8133        assert_eq!(second_decision.capsule_id, Some("capsule-remote".into()));
8134
8135        let after_second_replay = store.rebuild_projection().unwrap();
8136        let promoted_gene = after_second_replay
8137            .genes
8138            .iter()
8139            .find(|gene| gene.id == "gene-remote")
8140            .unwrap();
8141        let promoted_capsule = after_second_replay
8142            .capsules
8143            .iter()
8144            .find(|capsule| capsule.id == "capsule-remote")
8145            .unwrap();
8146        assert_eq!(promoted_gene.state, AssetState::Promoted);
8147        assert_eq!(promoted_capsule.state, AssetState::Promoted);
8148        let exported_after_second_replay =
8149            export_promoted_assets_from_store(store.as_ref(), "node-local").unwrap();
8150        assert_eq!(exported_after_second_replay.assets.len(), 3);
8151        assert!(exported_after_second_replay
8152            .assets
8153            .iter()
8154            .any(|asset| matches!(
8155                asset,
8156                NetworkAsset::EvolutionEvent {
8157                    event: EvolutionEvent::MutationDeclared { .. }
8158                }
8159            )));
8160    }
8161
8162    #[tokio::test]
8163    async fn publish_local_assets_include_mutation_payload_for_remote_replay() {
8164        let (source, source_store) = build_test_evo(
8165            "remote-publish-export",
8166            "run-remote-publish-export",
8167            command_validator(),
8168        );
8169        source
8170            .capture_successful_mutation(&"run-remote-publish-export".into(), sample_mutation())
8171            .await
8172            .unwrap();
8173        let envelope = EvolutionNetworkNode::new(source_store.clone())
8174            .publish_local_assets("node-source")
8175            .unwrap();
8176        assert!(envelope.assets.iter().any(|asset| matches!(
8177            asset,
8178            NetworkAsset::EvolutionEvent {
8179                event: EvolutionEvent::MutationDeclared { mutation }
8180            } if mutation.intent.id == "mutation-1"
8181        )));
8182
8183        let (remote, _) = build_test_evo(
8184            "remote-publish-import",
8185            "run-remote-publish-import",
8186            command_validator(),
8187        );
8188        remote.import_remote_envelope(&envelope).unwrap();
8189
8190        let decision = remote
8191            .replay_or_fallback(replay_input("missing readme"))
8192            .await
8193            .unwrap();
8194
8195        assert!(decision.used_capsule);
8196        assert!(!decision.fallback_to_planner);
8197    }
8198
8199    #[tokio::test]
8200    async fn import_remote_envelope_records_manifest_validation_event() {
8201        let (source, source_store) = build_test_evo(
8202            "remote-manifest-success-source",
8203            "run-remote-manifest-success-source",
8204            command_validator(),
8205        );
8206        source
8207            .capture_successful_mutation(
8208                &"run-remote-manifest-success-source".into(),
8209                sample_mutation(),
8210            )
8211            .await
8212            .unwrap();
8213        let envelope = EvolutionNetworkNode::new(source_store.clone())
8214            .publish_local_assets("node-source")
8215            .unwrap();
8216
8217        let (remote, remote_store) = build_test_evo(
8218            "remote-manifest-success-remote",
8219            "run-remote-manifest-success-remote",
8220            command_validator(),
8221        );
8222        remote.import_remote_envelope(&envelope).unwrap();
8223
8224        let events = remote_store.scan(1).unwrap();
8225        assert!(events.iter().any(|stored| matches!(
8226            &stored.event,
8227            EvolutionEvent::ManifestValidated {
8228                accepted: true,
8229                reason,
8230                sender_id: Some(sender_id),
8231                publisher: Some(publisher),
8232                asset_ids,
8233            } if reason == "manifest validated"
8234                && sender_id == "node-source"
8235                && publisher == "node-source"
8236                && !asset_ids.is_empty()
8237        )));
8238    }
8239
8240    #[test]
8241    fn import_remote_envelope_rejects_invalid_manifest_and_records_audit_event() {
8242        let (remote, remote_store) = build_test_evo(
8243            "remote-manifest-invalid",
8244            "run-remote-manifest-invalid",
8245            command_validator(),
8246        );
8247        let mut envelope = remote_publish_envelope(
8248            "node-remote",
8249            "run-remote-manifest-invalid",
8250            "gene-remote",
8251            "capsule-remote",
8252            "mutation-remote",
8253            "manifest-signal",
8254            "MANIFEST.md",
8255            "# drift",
8256        );
8257        if let Some(manifest) = envelope.manifest.as_mut() {
8258            manifest.asset_hash = "tampered-hash".to_string();
8259        }
8260        envelope.content_hash = envelope.compute_content_hash();
8261
8262        let error = remote.import_remote_envelope(&envelope).unwrap_err();
8263        assert!(error.to_string().contains("manifest"));
8264
8265        let events = remote_store.scan(1).unwrap();
8266        assert!(events.iter().any(|stored| matches!(
8267            &stored.event,
8268            EvolutionEvent::ManifestValidated {
8269                accepted: false,
8270                reason,
8271                sender_id: Some(sender_id),
8272                publisher: Some(publisher),
8273                asset_ids,
8274            } if reason.contains("manifest asset_hash mismatch")
8275                && sender_id == "node-remote"
8276                && publisher == "node-remote"
8277                && !asset_ids.is_empty()
8278        )));
8279    }
8280
8281    #[tokio::test]
8282    async fn fetch_assets_include_mutation_payload_for_remote_replay() {
8283        let (evo, store) = build_test_evo(
8284            "remote-fetch-export",
8285            "run-remote-fetch",
8286            command_validator(),
8287        );
8288        evo.capture_successful_mutation(&"run-remote-fetch".into(), sample_mutation())
8289            .await
8290            .unwrap();
8291
8292        let response = EvolutionNetworkNode::new(store.clone())
8293            .fetch_assets(
8294                "node-source",
8295                &FetchQuery {
8296                    sender_id: "node-client".into(),
8297                    signals: vec!["missing readme".into()],
8298                    since_cursor: None,
8299                    resume_token: None,
8300                },
8301            )
8302            .unwrap();
8303
8304        assert!(response.assets.iter().any(|asset| matches!(
8305            asset,
8306            NetworkAsset::EvolutionEvent {
8307                event: EvolutionEvent::MutationDeclared { mutation }
8308            } if mutation.intent.id == "mutation-1"
8309        )));
8310        assert!(response
8311            .assets
8312            .iter()
8313            .any(|asset| matches!(asset, NetworkAsset::Gene { .. })));
8314        assert!(response
8315            .assets
8316            .iter()
8317            .any(|asset| matches!(asset, NetworkAsset::Capsule { .. })));
8318    }
8319
8320    #[test]
8321    fn fetch_assets_delta_sync_supports_since_cursor_and_resume_token() {
8322        let store_root =
8323            std::env::temp_dir().join(format!("oris-evokernel-fetch-delta-store-{}", next_id("t")));
8324        if store_root.exists() {
8325            fs::remove_dir_all(&store_root).unwrap();
8326        }
8327        let store: Arc<dyn EvolutionStore> =
8328            Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
8329        let node = EvolutionNetworkNode::new(store.clone());
8330        node.record_reported_experience(
8331            "delta-agent",
8332            "gene-delta-a",
8333            vec!["delta.signal".into()],
8334            vec![
8335                "task_class=delta.signal".into(),
8336                "task_label=delta replay".into(),
8337            ],
8338            vec!["a2a.tasks.report".into()],
8339        )
8340        .unwrap();
8341
8342        let first = node
8343            .fetch_assets(
8344                "execution-api",
8345                &FetchQuery {
8346                    sender_id: "delta-agent".into(),
8347                    signals: vec!["delta.signal".into()],
8348                    since_cursor: None,
8349                    resume_token: None,
8350                },
8351            )
8352            .unwrap();
8353        let first_cursor = first.next_cursor.clone().expect("first next_cursor");
8354        let first_token = first.resume_token.clone().expect("first resume_token");
8355        assert!(first.assets.iter().any(
8356            |asset| matches!(asset, NetworkAsset::Gene { gene } if gene.id == "gene-delta-a")
8357        ));
8358
8359        let restarted = EvolutionNetworkNode::new(store.clone());
8360        restarted
8361            .record_reported_experience(
8362                "delta-agent",
8363                "gene-delta-b",
8364                vec!["delta.signal".into()],
8365                vec![
8366                    "task_class=delta.signal".into(),
8367                    "task_label=delta replay".into(),
8368                ],
8369                vec!["a2a.tasks.report".into()],
8370            )
8371            .unwrap();
8372
8373        let from_token = restarted
8374            .fetch_assets(
8375                "execution-api",
8376                &FetchQuery {
8377                    sender_id: "delta-agent".into(),
8378                    signals: vec!["delta.signal".into()],
8379                    since_cursor: None,
8380                    resume_token: Some(first_token),
8381                },
8382            )
8383            .unwrap();
8384        assert!(from_token.assets.iter().any(
8385            |asset| matches!(asset, NetworkAsset::Gene { gene } if gene.id == "gene-delta-b")
8386        ));
8387        assert!(!from_token.assets.iter().any(
8388            |asset| matches!(asset, NetworkAsset::Gene { gene } if gene.id == "gene-delta-a")
8389        ));
8390        assert_eq!(
8391            from_token.sync_audit.requested_cursor,
8392            Some(first_cursor.clone())
8393        );
8394        assert!(from_token.sync_audit.applied_count >= 1);
8395
8396        let from_cursor = restarted
8397            .fetch_assets(
8398                "execution-api",
8399                &FetchQuery {
8400                    sender_id: "delta-agent".into(),
8401                    signals: vec!["delta.signal".into()],
8402                    since_cursor: Some(first_cursor),
8403                    resume_token: None,
8404                },
8405            )
8406            .unwrap();
8407        assert!(from_cursor.assets.iter().any(
8408            |asset| matches!(asset, NetworkAsset::Gene { gene } if gene.id == "gene-delta-b")
8409        ));
8410    }
8411
8412    #[test]
8413    fn partial_remote_import_keeps_publisher_for_already_imported_assets() {
8414        let store_root = std::env::temp_dir().join(format!(
8415            "oris-evokernel-remote-partial-store-{}",
8416            std::process::id()
8417        ));
8418        if store_root.exists() {
8419            fs::remove_dir_all(&store_root).unwrap();
8420        }
8421        let store: Arc<dyn EvolutionStore> = Arc::new(FailOnAppendStore::new(store_root, 5));
8422        let evo = build_test_evo_with_store(
8423            "remote-partial",
8424            "run-remote-partial",
8425            command_validator(),
8426            store.clone(),
8427        );
8428        let envelope = remote_publish_envelope(
8429            "node-partial",
8430            "run-remote-partial",
8431            "gene-partial",
8432            "capsule-partial",
8433            "mutation-partial",
8434            "partial-signal",
8435            "PARTIAL.md",
8436            "# partial",
8437        );
8438
8439        let result = evo.import_remote_envelope(&envelope);
8440
8441        assert!(matches!(result, Err(EvoKernelError::Store(_))));
8442        let projection = store.rebuild_projection().unwrap();
8443        assert!(projection
8444            .genes
8445            .iter()
8446            .any(|gene| gene.id == "gene-partial"));
8447        assert!(projection.capsules.is_empty());
8448        let publishers = evo.remote_publishers.lock().unwrap();
8449        assert_eq!(
8450            publishers.get("gene-partial").map(String::as_str),
8451            Some("node-partial")
8452        );
8453    }
8454
8455    #[test]
8456    fn retry_remote_import_after_partial_failure_only_imports_missing_assets() {
8457        let store_root = std::env::temp_dir().join(format!(
8458            "oris-evokernel-remote-partial-retry-store-{}",
8459            next_id("t")
8460        ));
8461        if store_root.exists() {
8462            fs::remove_dir_all(&store_root).unwrap();
8463        }
8464        let store: Arc<dyn EvolutionStore> = Arc::new(FailOnAppendStore::new(store_root, 5));
8465        let evo = build_test_evo_with_store(
8466            "remote-partial-retry",
8467            "run-remote-partial-retry",
8468            command_validator(),
8469            store.clone(),
8470        );
8471        let envelope = remote_publish_envelope(
8472            "node-partial",
8473            "run-remote-partial-retry",
8474            "gene-partial-retry",
8475            "capsule-partial-retry",
8476            "mutation-partial-retry",
8477            "partial-retry-signal",
8478            "PARTIAL_RETRY.md",
8479            "# partial retry",
8480        );
8481
8482        let first = evo.import_remote_envelope(&envelope);
8483        assert!(matches!(first, Err(EvoKernelError::Store(_))));
8484
8485        let retry = evo.import_remote_envelope(&envelope).unwrap();
8486
8487        assert_eq!(retry.imported_asset_ids, vec!["capsule-partial-retry"]);
8488        let projection = store.rebuild_projection().unwrap();
8489        let gene = projection
8490            .genes
8491            .iter()
8492            .find(|gene| gene.id == "gene-partial-retry")
8493            .unwrap();
8494        assert_eq!(gene.state, AssetState::Quarantined);
8495        let capsule = projection
8496            .capsules
8497            .iter()
8498            .find(|capsule| capsule.id == "capsule-partial-retry")
8499            .unwrap();
8500        assert_eq!(capsule.state, AssetState::Quarantined);
8501        assert_eq!(projection.attempt_counts["gene-partial-retry"], 1);
8502
8503        let events = store.scan(1).unwrap();
8504        assert_eq!(
8505            events
8506                .iter()
8507                .filter(|stored| {
8508                    matches!(
8509                        &stored.event,
8510                        EvolutionEvent::MutationDeclared { mutation }
8511                            if mutation.intent.id == "mutation-partial-retry"
8512                    )
8513                })
8514                .count(),
8515            1
8516        );
8517        assert_eq!(
8518            events
8519                .iter()
8520                .filter(|stored| {
8521                    matches!(
8522                        &stored.event,
8523                        EvolutionEvent::GeneProjected { gene } if gene.id == "gene-partial-retry"
8524                    )
8525                })
8526                .count(),
8527            1
8528        );
8529        assert_eq!(
8530            events
8531                .iter()
8532                .filter(|stored| {
8533                    matches!(
8534                        &stored.event,
8535                        EvolutionEvent::CapsuleCommitted { capsule }
8536                            if capsule.id == "capsule-partial-retry"
8537                    )
8538                })
8539                .count(),
8540            1
8541        );
8542    }
8543
8544    #[tokio::test]
8545    async fn duplicate_remote_import_does_not_requarantine_locally_validated_assets() {
8546        let (evo, store) = build_test_evo(
8547            "remote-idempotent",
8548            "run-remote-idempotent",
8549            command_validator(),
8550        );
8551        let envelope = remote_publish_envelope(
8552            "node-idempotent",
8553            "run-remote-idempotent",
8554            "gene-idempotent",
8555            "capsule-idempotent",
8556            "mutation-idempotent",
8557            "idempotent-signal",
8558            "IDEMPOTENT.md",
8559            "# idempotent",
8560        );
8561
8562        let first = evo.import_remote_envelope(&envelope).unwrap();
8563        assert_eq!(
8564            first.imported_asset_ids,
8565            vec!["gene-idempotent", "capsule-idempotent"]
8566        );
8567
8568        let decision = evo
8569            .replay_or_fallback(replay_input("idempotent-signal"))
8570            .await
8571            .unwrap();
8572        assert!(decision.used_capsule);
8573        assert_eq!(decision.capsule_id, Some("capsule-idempotent".into()));
8574
8575        let projection_before = store.rebuild_projection().unwrap();
8576        let attempts_before = projection_before.attempt_counts["gene-idempotent"];
8577        let gene_before = projection_before
8578            .genes
8579            .iter()
8580            .find(|gene| gene.id == "gene-idempotent")
8581            .unwrap();
8582        assert_eq!(gene_before.state, AssetState::ShadowValidated);
8583        let capsule_before = projection_before
8584            .capsules
8585            .iter()
8586            .find(|capsule| capsule.id == "capsule-idempotent")
8587            .unwrap();
8588        assert_eq!(capsule_before.state, AssetState::ShadowValidated);
8589
8590        let second = evo.import_remote_envelope(&envelope).unwrap();
8591        assert!(second.imported_asset_ids.is_empty());
8592
8593        let projection_after = store.rebuild_projection().unwrap();
8594        assert_eq!(
8595            projection_after.attempt_counts["gene-idempotent"],
8596            attempts_before
8597        );
8598        let gene_after = projection_after
8599            .genes
8600            .iter()
8601            .find(|gene| gene.id == "gene-idempotent")
8602            .unwrap();
8603        assert_eq!(gene_after.state, AssetState::ShadowValidated);
8604        let capsule_after = projection_after
8605            .capsules
8606            .iter()
8607            .find(|capsule| capsule.id == "capsule-idempotent")
8608            .unwrap();
8609        assert_eq!(capsule_after.state, AssetState::ShadowValidated);
8610
8611        let third_decision = evo
8612            .replay_or_fallback(replay_input("idempotent-signal"))
8613            .await
8614            .unwrap();
8615        assert!(third_decision.used_capsule);
8616        assert_eq!(third_decision.capsule_id, Some("capsule-idempotent".into()));
8617
8618        let projection_promoted = store.rebuild_projection().unwrap();
8619        let promoted_gene = projection_promoted
8620            .genes
8621            .iter()
8622            .find(|gene| gene.id == "gene-idempotent")
8623            .unwrap();
8624        let promoted_capsule = projection_promoted
8625            .capsules
8626            .iter()
8627            .find(|capsule| capsule.id == "capsule-idempotent")
8628            .unwrap();
8629        assert_eq!(promoted_gene.state, AssetState::Promoted);
8630        assert_eq!(promoted_capsule.state, AssetState::Promoted);
8631
8632        let events = store.scan(1).unwrap();
8633        assert_eq!(
8634            events
8635                .iter()
8636                .filter(|stored| {
8637                    matches!(
8638                        &stored.event,
8639                        EvolutionEvent::MutationDeclared { mutation }
8640                            if mutation.intent.id == "mutation-idempotent"
8641                    )
8642                })
8643                .count(),
8644            1
8645        );
8646        assert_eq!(
8647            events
8648                .iter()
8649                .filter(|stored| {
8650                    matches!(
8651                        &stored.event,
8652                        EvolutionEvent::GeneProjected { gene } if gene.id == "gene-idempotent"
8653                    )
8654                })
8655                .count(),
8656            1
8657        );
8658        assert_eq!(
8659            events
8660                .iter()
8661                .filter(|stored| {
8662                    matches!(
8663                        &stored.event,
8664                        EvolutionEvent::CapsuleCommitted { capsule }
8665                            if capsule.id == "capsule-idempotent"
8666                    )
8667                })
8668                .count(),
8669            1
8670        );
8671
8672        assert_eq!(first.sync_audit.scanned_count, envelope.assets.len());
8673        assert_eq!(first.sync_audit.failed_count, 0);
8674        assert_eq!(second.sync_audit.applied_count, 0);
8675        assert_eq!(second.sync_audit.skipped_count, envelope.assets.len());
8676        assert!(second.resume_token.is_some());
8677    }
8678
8679    #[tokio::test]
8680    async fn insufficient_evu_blocks_publish_but_not_local_replay() {
8681        let (evo, _) = build_test_evo("stake-gate", "run-stake", command_validator());
8682        let capsule = evo
8683            .capture_successful_mutation(&"run-stake".into(), sample_mutation())
8684            .await
8685            .unwrap();
8686        let publish = evo.export_promoted_assets("node-local");
8687        assert!(matches!(publish, Err(EvoKernelError::Validation(_))));
8688
8689        let decision = evo
8690            .replay_or_fallback(replay_input("missing readme"))
8691            .await
8692            .unwrap();
8693        assert!(decision.used_capsule);
8694        assert_eq!(decision.capsule_id, Some(capsule.id));
8695    }
8696
8697    #[tokio::test]
8698    async fn second_replay_validation_failure_revokes_gene_immediately() {
8699        let (capturer, store) = build_test_evo("revoke-replay", "run-capture", command_validator());
8700        let capsule = capturer
8701            .capture_successful_mutation(&"run-capture".into(), sample_mutation())
8702            .await
8703            .unwrap();
8704
8705        let failing_validator: Arc<dyn Validator> = Arc::new(FixedValidator { success: false });
8706        let failing_replay = build_test_evo_with_store(
8707            "revoke-replay",
8708            "run-replay-fail",
8709            failing_validator,
8710            store.clone(),
8711        );
8712
8713        let first = failing_replay
8714            .replay_or_fallback(replay_input("missing readme"))
8715            .await
8716            .unwrap();
8717        let second = failing_replay
8718            .replay_or_fallback(replay_input("missing readme"))
8719            .await
8720            .unwrap();
8721
8722        assert!(!first.used_capsule);
8723        assert!(first.fallback_to_planner);
8724        assert!(!second.used_capsule);
8725        assert!(second.fallback_to_planner);
8726
8727        let projection = store.rebuild_projection().unwrap();
8728        let gene = projection
8729            .genes
8730            .iter()
8731            .find(|gene| gene.id == capsule.gene_id)
8732            .unwrap();
8733        assert_eq!(gene.state, AssetState::Promoted);
8734        let committed_capsule = projection
8735            .capsules
8736            .iter()
8737            .find(|current| current.id == capsule.id)
8738            .unwrap();
8739        assert_eq!(committed_capsule.state, AssetState::Promoted);
8740
8741        let events = store.scan(1).unwrap();
8742        assert_eq!(
8743            events
8744                .iter()
8745                .filter(|stored| {
8746                    matches!(
8747                        &stored.event,
8748                        EvolutionEvent::ValidationFailed {
8749                            gene_id: Some(gene_id),
8750                            ..
8751                        } if gene_id == &capsule.gene_id
8752                    )
8753                })
8754                .count(),
8755            1
8756        );
8757        assert!(!events.iter().any(|stored| {
8758            matches!(
8759                &stored.event,
8760                EvolutionEvent::GeneRevoked { gene_id, .. } if gene_id == &capsule.gene_id
8761            )
8762        }));
8763
8764        let recovered = build_test_evo_with_store(
8765            "revoke-replay",
8766            "run-replay-check",
8767            command_validator(),
8768            store.clone(),
8769        );
8770        let after_revoke = recovered
8771            .replay_or_fallback(replay_input("missing readme"))
8772            .await
8773            .unwrap();
8774        assert!(!after_revoke.used_capsule);
8775        assert!(after_revoke.fallback_to_planner);
8776        assert!(after_revoke.reason.contains("below replay threshold"));
8777    }
8778
8779    #[tokio::test]
8780    async fn remote_reuse_success_rewards_publisher_and_biases_selection() {
8781        let ledger = Arc::new(Mutex::new(EvuLedger {
8782            accounts: vec![],
8783            reputations: vec![
8784                oris_economics::ReputationRecord {
8785                    node_id: "node-a".into(),
8786                    publish_success_rate: 0.4,
8787                    validator_accuracy: 0.4,
8788                    reuse_impact: 0,
8789                },
8790                oris_economics::ReputationRecord {
8791                    node_id: "node-b".into(),
8792                    publish_success_rate: 0.95,
8793                    validator_accuracy: 0.95,
8794                    reuse_impact: 8,
8795                },
8796            ],
8797        }));
8798        let (evo, _) = build_test_evo("remote-success", "run-remote", command_validator());
8799        let evo = evo.with_economics(ledger.clone());
8800
8801        let envelope_a = remote_publish_envelope(
8802            "node-a",
8803            "run-remote-a",
8804            "gene-a",
8805            "capsule-a",
8806            "mutation-a",
8807            "shared-signal",
8808            "A.md",
8809            "# from a",
8810        );
8811        let envelope_b = remote_publish_envelope(
8812            "node-b",
8813            "run-remote-b",
8814            "gene-b",
8815            "capsule-b",
8816            "mutation-b",
8817            "shared-signal",
8818            "B.md",
8819            "# from b",
8820        );
8821
8822        evo.import_remote_envelope(&envelope_a).unwrap();
8823        evo.import_remote_envelope(&envelope_b).unwrap();
8824
8825        let decision = evo
8826            .replay_or_fallback(replay_input("shared-signal"))
8827            .await
8828            .unwrap();
8829
8830        assert!(decision.used_capsule);
8831        assert_eq!(decision.capsule_id, Some("capsule-b".into()));
8832        let locked = ledger.lock().unwrap();
8833        let rewarded = locked
8834            .accounts
8835            .iter()
8836            .find(|item| item.node_id == "node-b")
8837            .unwrap();
8838        assert_eq!(rewarded.balance, evo.stake_policy.reuse_reward);
8839        assert!(
8840            locked.selector_reputation_bias()["node-b"]
8841                > locked.selector_reputation_bias()["node-a"]
8842        );
8843    }
8844
8845    #[tokio::test]
8846    async fn remote_reuse_settlement_tracks_selected_capsule_publisher_for_shared_gene() {
8847        let ledger = Arc::new(Mutex::new(EvuLedger::default()));
8848        let (evo, _) = build_test_evo(
8849            "remote-shared-publisher",
8850            "run-remote-shared-publisher",
8851            command_validator(),
8852        );
8853        let evo = evo.with_economics(ledger.clone());
8854        let input = replay_input("shared-signal");
8855        let preferred = remote_publish_envelope_with_env(
8856            "node-a",
8857            "run-remote-a",
8858            "gene-shared",
8859            "capsule-preferred",
8860            "mutation-preferred",
8861            "shared-signal",
8862            "A.md",
8863            "# from a",
8864            input.env.clone(),
8865        );
8866        let fallback = remote_publish_envelope_with_env(
8867            "node-b",
8868            "run-remote-b",
8869            "gene-shared",
8870            "capsule-fallback",
8871            "mutation-fallback",
8872            "shared-signal",
8873            "B.md",
8874            "# from b",
8875            EnvFingerprint {
8876                rustc_version: "old-rustc".into(),
8877                cargo_lock_hash: "other-lock".into(),
8878                target_triple: "aarch64-apple-darwin".into(),
8879                os: "linux".into(),
8880            },
8881        );
8882
8883        evo.import_remote_envelope(&preferred).unwrap();
8884        evo.import_remote_envelope(&fallback).unwrap();
8885
8886        let decision = evo.replay_or_fallback(input).await.unwrap();
8887
8888        assert!(decision.used_capsule);
8889        assert_eq!(decision.capsule_id, Some("capsule-preferred".into()));
8890        let locked = ledger.lock().unwrap();
8891        let rewarded = locked
8892            .accounts
8893            .iter()
8894            .find(|item| item.node_id == "node-a")
8895            .unwrap();
8896        assert_eq!(rewarded.balance, evo.stake_policy.reuse_reward);
8897        assert!(locked.accounts.iter().all(|item| item.node_id != "node-b"));
8898    }
8899
8900    #[test]
8901    fn select_candidates_surfaces_ranked_remote_cold_start_candidates() {
8902        let ledger = Arc::new(Mutex::new(EvuLedger {
8903            accounts: vec![],
8904            reputations: vec![
8905                oris_economics::ReputationRecord {
8906                    node_id: "node-a".into(),
8907                    publish_success_rate: 0.4,
8908                    validator_accuracy: 0.4,
8909                    reuse_impact: 0,
8910                },
8911                oris_economics::ReputationRecord {
8912                    node_id: "node-b".into(),
8913                    publish_success_rate: 0.95,
8914                    validator_accuracy: 0.95,
8915                    reuse_impact: 8,
8916                },
8917            ],
8918        }));
8919        let (evo, _) = build_test_evo("remote-select", "run-remote-select", command_validator());
8920        let evo = evo.with_economics(ledger);
8921
8922        let envelope_a = remote_publish_envelope(
8923            "node-a",
8924            "run-remote-a",
8925            "gene-a",
8926            "capsule-a",
8927            "mutation-a",
8928            "shared-signal",
8929            "A.md",
8930            "# from a",
8931        );
8932        let envelope_b = remote_publish_envelope(
8933            "node-b",
8934            "run-remote-b",
8935            "gene-b",
8936            "capsule-b",
8937            "mutation-b",
8938            "shared-signal",
8939            "B.md",
8940            "# from b",
8941        );
8942
8943        evo.import_remote_envelope(&envelope_a).unwrap();
8944        evo.import_remote_envelope(&envelope_b).unwrap();
8945
8946        let candidates = evo.select_candidates(&replay_input("shared-signal"));
8947
8948        assert_eq!(candidates.len(), 1);
8949        assert_eq!(candidates[0].gene.id, "gene-b");
8950        assert_eq!(candidates[0].capsules[0].id, "capsule-b");
8951    }
8952
8953    #[tokio::test]
8954    async fn remote_reuse_publisher_bias_survives_restart() {
8955        let ledger = Arc::new(Mutex::new(EvuLedger {
8956            accounts: vec![],
8957            reputations: vec![
8958                oris_economics::ReputationRecord {
8959                    node_id: "node-a".into(),
8960                    publish_success_rate: 0.4,
8961                    validator_accuracy: 0.4,
8962                    reuse_impact: 0,
8963                },
8964                oris_economics::ReputationRecord {
8965                    node_id: "node-b".into(),
8966                    publish_success_rate: 0.95,
8967                    validator_accuracy: 0.95,
8968                    reuse_impact: 8,
8969                },
8970            ],
8971        }));
8972        let store_root = std::env::temp_dir().join(format!(
8973            "oris-evokernel-remote-restart-store-{}",
8974            next_id("t")
8975        ));
8976        if store_root.exists() {
8977            fs::remove_dir_all(&store_root).unwrap();
8978        }
8979        let store: Arc<dyn EvolutionStore> =
8980            Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
8981        let evo = build_test_evo_with_store(
8982            "remote-success-restart-source",
8983            "run-remote-restart-source",
8984            command_validator(),
8985            store.clone(),
8986        )
8987        .with_economics(ledger.clone());
8988
8989        let envelope_a = remote_publish_envelope(
8990            "node-a",
8991            "run-remote-a",
8992            "gene-a",
8993            "capsule-a",
8994            "mutation-a",
8995            "shared-signal",
8996            "A.md",
8997            "# from a",
8998        );
8999        let envelope_b = remote_publish_envelope(
9000            "node-b",
9001            "run-remote-b",
9002            "gene-b",
9003            "capsule-b",
9004            "mutation-b",
9005            "shared-signal",
9006            "B.md",
9007            "# from b",
9008        );
9009
9010        evo.import_remote_envelope(&envelope_a).unwrap();
9011        evo.import_remote_envelope(&envelope_b).unwrap();
9012
9013        let recovered = build_test_evo_with_store(
9014            "remote-success-restart-recovered",
9015            "run-remote-restart-recovered",
9016            command_validator(),
9017            store.clone(),
9018        )
9019        .with_economics(ledger.clone());
9020
9021        let decision = recovered
9022            .replay_or_fallback(replay_input("shared-signal"))
9023            .await
9024            .unwrap();
9025
9026        assert!(decision.used_capsule);
9027        assert_eq!(decision.capsule_id, Some("capsule-b".into()));
9028        let locked = ledger.lock().unwrap();
9029        let rewarded = locked
9030            .accounts
9031            .iter()
9032            .find(|item| item.node_id == "node-b")
9033            .unwrap();
9034        assert_eq!(rewarded.balance, recovered.stake_policy.reuse_reward);
9035    }
9036
9037    #[tokio::test]
9038    async fn remote_reuse_failure_penalizes_remote_reputation() {
9039        let ledger = Arc::new(Mutex::new(EvuLedger::default()));
9040        let failing_validator: Arc<dyn Validator> = Arc::new(FixedValidator { success: false });
9041        let (evo, _) = build_test_evo("remote-failure", "run-failure", failing_validator);
9042        let evo = evo.with_economics(ledger.clone());
9043
9044        let envelope = remote_publish_envelope(
9045            "node-remote",
9046            "run-remote-failed",
9047            "gene-remote",
9048            "capsule-remote",
9049            "mutation-remote",
9050            "failure-signal",
9051            "FAILED.md",
9052            "# from remote",
9053        );
9054        evo.import_remote_envelope(&envelope).unwrap();
9055
9056        let decision = evo
9057            .replay_or_fallback(replay_input("failure-signal"))
9058            .await
9059            .unwrap();
9060
9061        assert!(!decision.used_capsule);
9062        assert!(decision.fallback_to_planner);
9063
9064        let signal = evo.economics_signal("node-remote").unwrap();
9065        assert_eq!(signal.available_evu, 0);
9066        assert!(signal.publish_success_rate < 0.5);
9067        assert!(signal.validator_accuracy < 0.5);
9068    }
9069
9070    #[test]
9071    fn ensure_builtin_experience_assets_is_idempotent_and_fetchable() {
9072        let store_root = std::env::temp_dir().join(format!(
9073            "oris-evokernel-builtin-experience-store-{}",
9074            next_id("t")
9075        ));
9076        if store_root.exists() {
9077            fs::remove_dir_all(&store_root).unwrap();
9078        }
9079        let store: Arc<dyn EvolutionStore> =
9080            Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
9081        let node = EvolutionNetworkNode::new(store.clone());
9082
9083        let first = node
9084            .ensure_builtin_experience_assets("runtime-bootstrap")
9085            .unwrap();
9086        assert!(!first.imported_asset_ids.is_empty());
9087
9088        let second = node
9089            .ensure_builtin_experience_assets("runtime-bootstrap")
9090            .unwrap();
9091        assert!(second.imported_asset_ids.is_empty());
9092
9093        let fetch = node
9094            .fetch_assets(
9095                "execution-api",
9096                &FetchQuery {
9097                    sender_id: "compat-agent".into(),
9098                    signals: vec!["error".into()],
9099                    since_cursor: None,
9100                    resume_token: None,
9101                },
9102            )
9103            .unwrap();
9104
9105        let mut has_builtin_evomap = false;
9106        for asset in fetch.assets {
9107            if let NetworkAsset::Gene { gene } = asset {
9108                if strategy_metadata_value(&gene.strategy, "asset_origin").as_deref()
9109                    == Some("builtin_evomap")
9110                    && gene.state == AssetState::Promoted
9111                {
9112                    has_builtin_evomap = true;
9113                    break;
9114                }
9115            }
9116        }
9117        assert!(has_builtin_evomap);
9118    }
9119
9120    #[test]
9121    fn reported_experience_retention_keeps_latest_three_and_preserves_builtin_assets() {
9122        let store_root = std::env::temp_dir().join(format!(
9123            "oris-evokernel-reported-retention-store-{}",
9124            next_id("t")
9125        ));
9126        if store_root.exists() {
9127            fs::remove_dir_all(&store_root).unwrap();
9128        }
9129        let store: Arc<dyn EvolutionStore> =
9130            Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
9131        let node = EvolutionNetworkNode::new(store.clone());
9132
9133        node.ensure_builtin_experience_assets("runtime-bootstrap")
9134            .unwrap();
9135
9136        for idx in 0..4 {
9137            node.record_reported_experience(
9138                "reporter-a",
9139                format!("reported-docs-rewrite-v{}", idx + 1),
9140                vec!["docs.rewrite".into(), format!("task-{}", idx + 1)],
9141                vec![
9142                    "task_class=docs.rewrite".into(),
9143                    format!("task_label=Docs rewrite v{}", idx + 1),
9144                    format!("summary=reported replay {}", idx + 1),
9145                ],
9146                vec!["a2a.tasks.report".into()],
9147            )
9148            .unwrap();
9149        }
9150
9151        let (_, projection) = store.scan_projection().unwrap();
9152        let reported_promoted = projection
9153            .genes
9154            .iter()
9155            .filter(|gene| {
9156                gene.state == AssetState::Promoted
9157                    && strategy_metadata_value(&gene.strategy, "asset_origin").as_deref()
9158                        == Some("reported_experience")
9159                    && strategy_metadata_value(&gene.strategy, "task_class").as_deref()
9160                        == Some("docs.rewrite")
9161            })
9162            .count();
9163        let reported_revoked = projection
9164            .genes
9165            .iter()
9166            .filter(|gene| {
9167                gene.state == AssetState::Revoked
9168                    && strategy_metadata_value(&gene.strategy, "asset_origin").as_deref()
9169                        == Some("reported_experience")
9170                    && strategy_metadata_value(&gene.strategy, "task_class").as_deref()
9171                        == Some("docs.rewrite")
9172            })
9173            .count();
9174        let builtin_promoted = projection
9175            .genes
9176            .iter()
9177            .filter(|gene| {
9178                gene.state == AssetState::Promoted
9179                    && matches!(
9180                        strategy_metadata_value(&gene.strategy, "asset_origin").as_deref(),
9181                        Some("builtin") | Some("builtin_evomap")
9182                    )
9183            })
9184            .count();
9185
9186        assert_eq!(reported_promoted, 3);
9187        assert_eq!(reported_revoked, 1);
9188        assert!(builtin_promoted >= 1);
9189
9190        let fetch = node
9191            .fetch_assets(
9192                "execution-api",
9193                &FetchQuery {
9194                    sender_id: "consumer-b".into(),
9195                    signals: vec!["docs.rewrite".into()],
9196                    since_cursor: None,
9197                    resume_token: None,
9198                },
9199            )
9200            .unwrap();
9201        let docs_genes = fetch
9202            .assets
9203            .into_iter()
9204            .filter_map(|asset| match asset {
9205                NetworkAsset::Gene { gene } => Some(gene),
9206                _ => None,
9207            })
9208            .filter(|gene| {
9209                strategy_metadata_value(&gene.strategy, "task_class").as_deref()
9210                    == Some("docs.rewrite")
9211            })
9212            .collect::<Vec<_>>();
9213        assert!(docs_genes.len() >= 3);
9214    }
9215}