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