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