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