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