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