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