Skip to main content

oris_evokernel/
core.rs

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