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