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