Skip to main content

oris_evokernel/
core.rs

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