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