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 max_memory_bytes: None,
8955 max_cpu_secs: None,
8956 use_process_group: false,
8957 }
8958 }
8959
8960 fn command_validator() -> Arc<dyn Validator> {
8961 Arc::new(CommandValidator::new(base_sandbox_policy()))
8962 }
8963
8964 fn replay_input(signal: &str) -> SelectorInput {
8965 let rustc_version = std::process::Command::new("rustc")
8966 .arg("--version")
8967 .output()
8968 .ok()
8969 .filter(|output| output.status.success())
8970 .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
8971 .unwrap_or_else(|| "rustc unknown".into());
8972 SelectorInput {
8973 signals: vec![signal.into()],
8974 env: EnvFingerprint {
8975 rustc_version,
8976 cargo_lock_hash: compute_artifact_hash("# lock\n"),
8977 target_triple: format!(
8978 "{}-unknown-{}",
8979 std::env::consts::ARCH,
8980 std::env::consts::OS
8981 ),
8982 os: std::env::consts::OS.into(),
8983 },
8984 spec_id: None,
8985 limit: 1,
8986 }
8987 }
8988
8989 fn build_test_evo_with_store(
8990 name: &str,
8991 run_id: &str,
8992 validator: Arc<dyn Validator>,
8993 store: Arc<dyn EvolutionStore>,
8994 ) -> EvoKernel<TestState> {
8995 let workspace = temp_workspace(name);
8996 let sandbox: Arc<dyn Sandbox> = Arc::new(oris_sandbox::LocalProcessSandbox::new(
8997 run_id,
8998 &workspace,
8999 std::env::temp_dir(),
9000 ));
9001 EvoKernel::new(test_kernel(), sandbox, validator, store)
9002 .with_governor(Arc::new(DefaultGovernor::new(
9003 oris_governor::GovernorConfig {
9004 promote_after_successes: 1,
9005 ..Default::default()
9006 },
9007 )))
9008 .with_validation_plan(lightweight_plan())
9009 .with_sandbox_policy(base_sandbox_policy())
9010 }
9011
9012 fn build_test_evo(
9013 name: &str,
9014 run_id: &str,
9015 validator: Arc<dyn Validator>,
9016 ) -> (EvoKernel<TestState>, Arc<dyn EvolutionStore>) {
9017 let store_root = std::env::temp_dir().join(format!(
9018 "oris-evokernel-{name}-store-{}",
9019 std::process::id()
9020 ));
9021 if store_root.exists() {
9022 fs::remove_dir_all(&store_root).unwrap();
9023 }
9024 let store: Arc<dyn EvolutionStore> =
9025 Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
9026 let evo = build_test_evo_with_store(name, run_id, validator, store.clone());
9027 (evo, store)
9028 }
9029
9030 fn remote_publish_envelope(
9031 sender_id: &str,
9032 run_id: &str,
9033 gene_id: &str,
9034 capsule_id: &str,
9035 mutation_id: &str,
9036 signal: &str,
9037 file_name: &str,
9038 line: &str,
9039 ) -> EvolutionEnvelope {
9040 remote_publish_envelope_with_env(
9041 sender_id,
9042 run_id,
9043 gene_id,
9044 capsule_id,
9045 mutation_id,
9046 signal,
9047 file_name,
9048 line,
9049 replay_input(signal).env,
9050 )
9051 }
9052
9053 fn remote_publish_envelope_with_env(
9054 sender_id: &str,
9055 run_id: &str,
9056 gene_id: &str,
9057 capsule_id: &str,
9058 mutation_id: &str,
9059 signal: &str,
9060 file_name: &str,
9061 line: &str,
9062 env: EnvFingerprint,
9063 ) -> EvolutionEnvelope {
9064 let mutation = prepare_mutation(
9065 MutationIntent {
9066 id: mutation_id.into(),
9067 intent: format!("add {file_name}"),
9068 target: MutationTarget::Paths {
9069 allow: vec![file_name.into()],
9070 },
9071 expected_effect: "replay should still validate".into(),
9072 risk: RiskLevel::Low,
9073 signals: vec![signal.into()],
9074 spec_id: None,
9075 },
9076 format!(
9077 "\
9078diff --git a/{file_name} b/{file_name}
9079new file mode 100644
9080index 0000000..1111111
9081--- /dev/null
9082+++ b/{file_name}
9083@@ -0,0 +1 @@
9084+{line}
9085"
9086 ),
9087 Some("HEAD".into()),
9088 );
9089 let gene = Gene {
9090 id: gene_id.into(),
9091 signals: vec![signal.into()],
9092 strategy: vec![file_name.into()],
9093 validation: vec!["test".into()],
9094 state: AssetState::Promoted,
9095 task_class_id: None,
9096 };
9097 let capsule = Capsule {
9098 id: capsule_id.into(),
9099 gene_id: gene_id.into(),
9100 mutation_id: mutation_id.into(),
9101 run_id: run_id.into(),
9102 diff_hash: mutation.artifact.content_hash.clone(),
9103 confidence: 0.9,
9104 env,
9105 outcome: Outcome {
9106 success: true,
9107 validation_profile: "test".into(),
9108 validation_duration_ms: 1,
9109 changed_files: vec![file_name.into()],
9110 validator_hash: "validator-hash".into(),
9111 lines_changed: 1,
9112 replay_verified: false,
9113 },
9114 state: AssetState::Promoted,
9115 };
9116 EvolutionEnvelope::publish(
9117 sender_id,
9118 vec![
9119 NetworkAsset::EvolutionEvent {
9120 event: EvolutionEvent::MutationDeclared { mutation },
9121 },
9122 NetworkAsset::Gene { gene: gene.clone() },
9123 NetworkAsset::Capsule {
9124 capsule: capsule.clone(),
9125 },
9126 NetworkAsset::EvolutionEvent {
9127 event: EvolutionEvent::CapsuleReleased {
9128 capsule_id: capsule.id.clone(),
9129 state: AssetState::Promoted,
9130 },
9131 },
9132 ],
9133 )
9134 }
9135
9136 fn remote_publish_envelope_with_signals(
9137 sender_id: &str,
9138 run_id: &str,
9139 gene_id: &str,
9140 capsule_id: &str,
9141 mutation_id: &str,
9142 mutation_signals: Vec<String>,
9143 gene_signals: Vec<String>,
9144 file_name: &str,
9145 line: &str,
9146 env: EnvFingerprint,
9147 ) -> EvolutionEnvelope {
9148 let mutation = prepare_mutation(
9149 MutationIntent {
9150 id: mutation_id.into(),
9151 intent: format!("add {file_name}"),
9152 target: MutationTarget::Paths {
9153 allow: vec![file_name.into()],
9154 },
9155 expected_effect: "replay should still validate".into(),
9156 risk: RiskLevel::Low,
9157 signals: mutation_signals,
9158 spec_id: None,
9159 },
9160 format!(
9161 "\
9162diff --git a/{file_name} b/{file_name}
9163new file mode 100644
9164index 0000000..1111111
9165--- /dev/null
9166+++ b/{file_name}
9167@@ -0,0 +1 @@
9168+{line}
9169"
9170 ),
9171 Some("HEAD".into()),
9172 );
9173 let gene = Gene {
9174 id: gene_id.into(),
9175 signals: gene_signals,
9176 strategy: vec![file_name.into()],
9177 validation: vec!["test".into()],
9178 state: AssetState::Promoted,
9179 task_class_id: None,
9180 };
9181 let capsule = Capsule {
9182 id: capsule_id.into(),
9183 gene_id: gene_id.into(),
9184 mutation_id: mutation_id.into(),
9185 run_id: run_id.into(),
9186 diff_hash: mutation.artifact.content_hash.clone(),
9187 confidence: 0.9,
9188 env,
9189 outcome: Outcome {
9190 success: true,
9191 validation_profile: "test".into(),
9192 validation_duration_ms: 1,
9193 changed_files: vec![file_name.into()],
9194 validator_hash: "validator-hash".into(),
9195 lines_changed: 1,
9196 replay_verified: false,
9197 },
9198 state: AssetState::Promoted,
9199 };
9200 EvolutionEnvelope::publish(
9201 sender_id,
9202 vec![
9203 NetworkAsset::EvolutionEvent {
9204 event: EvolutionEvent::MutationDeclared { mutation },
9205 },
9206 NetworkAsset::Gene { gene: gene.clone() },
9207 NetworkAsset::Capsule {
9208 capsule: capsule.clone(),
9209 },
9210 NetworkAsset::EvolutionEvent {
9211 event: EvolutionEvent::CapsuleReleased {
9212 capsule_id: capsule.id.clone(),
9213 state: AssetState::Promoted,
9214 },
9215 },
9216 ],
9217 )
9218 }
9219
9220 struct FixedValidator {
9221 success: bool,
9222 }
9223
9224 #[async_trait]
9225 impl Validator for FixedValidator {
9226 async fn run(
9227 &self,
9228 _receipt: &SandboxReceipt,
9229 plan: &ValidationPlan,
9230 ) -> Result<ValidationReport, ValidationError> {
9231 Ok(ValidationReport {
9232 success: self.success,
9233 duration_ms: 1,
9234 stages: Vec::new(),
9235 logs: if self.success {
9236 format!("{} ok", plan.profile)
9237 } else {
9238 format!("{} failed", plan.profile)
9239 },
9240 })
9241 }
9242 }
9243
9244 struct FailOnAppendStore {
9245 inner: JsonlEvolutionStore,
9246 fail_on_call: usize,
9247 call_count: Mutex<usize>,
9248 }
9249
9250 impl FailOnAppendStore {
9251 fn new(root_dir: std::path::PathBuf, fail_on_call: usize) -> Self {
9252 Self {
9253 inner: JsonlEvolutionStore::new(root_dir),
9254 fail_on_call,
9255 call_count: Mutex::new(0),
9256 }
9257 }
9258 }
9259
9260 impl EvolutionStore for FailOnAppendStore {
9261 fn append_event(&self, event: EvolutionEvent) -> Result<u64, EvolutionError> {
9262 let mut call_count = self
9263 .call_count
9264 .lock()
9265 .map_err(|_| EvolutionError::Io("test store lock poisoned".into()))?;
9266 *call_count += 1;
9267 if *call_count == self.fail_on_call {
9268 return Err(EvolutionError::Io("injected append failure".into()));
9269 }
9270 self.inner.append_event(event)
9271 }
9272
9273 fn scan(&self, from_seq: u64) -> Result<Vec<StoredEvolutionEvent>, EvolutionError> {
9274 self.inner.scan(from_seq)
9275 }
9276
9277 fn rebuild_projection(&self) -> Result<EvolutionProjection, EvolutionError> {
9278 self.inner.rebuild_projection()
9279 }
9280 }
9281
9282 #[test]
9283 fn coordination_planner_to_coder_handoff_is_deterministic() {
9284 let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
9285 root_goal: "ship feature".into(),
9286 primitive: CoordinationPrimitive::Sequential,
9287 tasks: vec![
9288 CoordinationTask {
9289 id: "planner".into(),
9290 role: AgentRole::Planner,
9291 description: "split the work".into(),
9292 depends_on: Vec::new(),
9293 },
9294 CoordinationTask {
9295 id: "coder".into(),
9296 role: AgentRole::Coder,
9297 description: "implement the patch".into(),
9298 depends_on: vec!["planner".into()],
9299 },
9300 ],
9301 timeout_ms: 5_000,
9302 max_retries: 0,
9303 });
9304
9305 assert_eq!(result.completed_tasks, vec!["planner", "coder"]);
9306 assert!(result.failed_tasks.is_empty());
9307 assert!(result.messages.iter().any(|message| {
9308 message.from_role == AgentRole::Planner
9309 && message.to_role == AgentRole::Coder
9310 && message.task_id == "coder"
9311 }));
9312 }
9313
9314 #[test]
9315 fn coordination_repair_runs_only_after_coder_failure() {
9316 let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
9317 root_goal: "fix broken implementation".into(),
9318 primitive: CoordinationPrimitive::Sequential,
9319 tasks: vec![
9320 CoordinationTask {
9321 id: "coder".into(),
9322 role: AgentRole::Coder,
9323 description: "force-fail initial implementation".into(),
9324 depends_on: Vec::new(),
9325 },
9326 CoordinationTask {
9327 id: "repair".into(),
9328 role: AgentRole::Repair,
9329 description: "patch the failed implementation".into(),
9330 depends_on: vec!["coder".into()],
9331 },
9332 ],
9333 timeout_ms: 5_000,
9334 max_retries: 0,
9335 });
9336
9337 assert_eq!(result.completed_tasks, vec!["repair"]);
9338 assert_eq!(result.failed_tasks, vec!["coder"]);
9339 assert!(result.messages.iter().any(|message| {
9340 message.from_role == AgentRole::Coder
9341 && message.to_role == AgentRole::Repair
9342 && message.task_id == "repair"
9343 }));
9344 }
9345
9346 #[test]
9347 fn coordination_optimizer_runs_after_successful_implementation_step() {
9348 let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
9349 root_goal: "ship optimized patch".into(),
9350 primitive: CoordinationPrimitive::Sequential,
9351 tasks: vec![
9352 CoordinationTask {
9353 id: "coder".into(),
9354 role: AgentRole::Coder,
9355 description: "implement a working patch".into(),
9356 depends_on: Vec::new(),
9357 },
9358 CoordinationTask {
9359 id: "optimizer".into(),
9360 role: AgentRole::Optimizer,
9361 description: "tighten the implementation".into(),
9362 depends_on: vec!["coder".into()],
9363 },
9364 ],
9365 timeout_ms: 5_000,
9366 max_retries: 0,
9367 });
9368
9369 assert_eq!(result.completed_tasks, vec!["coder", "optimizer"]);
9370 assert!(result.failed_tasks.is_empty());
9371 }
9372
9373 #[test]
9374 fn coordination_parallel_waves_preserve_sorted_merge_order() {
9375 let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
9376 root_goal: "parallelize safe tasks".into(),
9377 primitive: CoordinationPrimitive::Parallel,
9378 tasks: vec![
9379 CoordinationTask {
9380 id: "z-task".into(),
9381 role: AgentRole::Planner,
9382 description: "analyze z".into(),
9383 depends_on: Vec::new(),
9384 },
9385 CoordinationTask {
9386 id: "a-task".into(),
9387 role: AgentRole::Coder,
9388 description: "implement a".into(),
9389 depends_on: Vec::new(),
9390 },
9391 CoordinationTask {
9392 id: "mid-task".into(),
9393 role: AgentRole::Optimizer,
9394 description: "polish after both".into(),
9395 depends_on: vec!["z-task".into(), "a-task".into()],
9396 },
9397 ],
9398 timeout_ms: 5_000,
9399 max_retries: 0,
9400 });
9401
9402 assert_eq!(result.completed_tasks, vec!["a-task", "z-task", "mid-task"]);
9403 assert!(result.failed_tasks.is_empty());
9404 }
9405
9406 #[test]
9407 fn coordination_retries_stop_at_max_retries() {
9408 let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
9409 root_goal: "retry then stop".into(),
9410 primitive: CoordinationPrimitive::Sequential,
9411 tasks: vec![CoordinationTask {
9412 id: "coder".into(),
9413 role: AgentRole::Coder,
9414 description: "force-fail this task".into(),
9415 depends_on: Vec::new(),
9416 }],
9417 timeout_ms: 5_000,
9418 max_retries: 1,
9419 });
9420
9421 assert!(result.completed_tasks.is_empty());
9422 assert_eq!(result.failed_tasks, vec!["coder"]);
9423 assert_eq!(
9424 result
9425 .messages
9426 .iter()
9427 .filter(|message| message.task_id == "coder" && message.content.contains("failed"))
9428 .count(),
9429 2
9430 );
9431 }
9432
9433 #[test]
9434 fn coordination_conditional_mode_skips_downstream_tasks_on_failure() {
9435 let result = MultiAgentCoordinator::new().coordinate(CoordinationPlan {
9436 root_goal: "skip blocked follow-up work".into(),
9437 primitive: CoordinationPrimitive::Conditional,
9438 tasks: vec![
9439 CoordinationTask {
9440 id: "coder".into(),
9441 role: AgentRole::Coder,
9442 description: "force-fail the implementation".into(),
9443 depends_on: Vec::new(),
9444 },
9445 CoordinationTask {
9446 id: "optimizer".into(),
9447 role: AgentRole::Optimizer,
9448 description: "only optimize a successful implementation".into(),
9449 depends_on: vec!["coder".into()],
9450 },
9451 ],
9452 timeout_ms: 5_000,
9453 max_retries: 0,
9454 });
9455
9456 assert!(result.completed_tasks.is_empty());
9457 assert_eq!(result.failed_tasks, vec!["coder"]);
9458 assert!(result.messages.iter().any(|message| {
9459 message.task_id == "optimizer"
9460 && message
9461 .content
9462 .contains("skipped due to failed dependency chain")
9463 }));
9464 assert!(!result
9465 .failed_tasks
9466 .iter()
9467 .any(|task_id| task_id == "optimizer"));
9468 }
9469
9470 #[tokio::test]
9471 async fn command_validator_aggregates_stage_reports() {
9472 let workspace = temp_workspace("validator");
9473 let receipt = SandboxReceipt {
9474 mutation_id: "m".into(),
9475 workdir: workspace,
9476 applied: true,
9477 changed_files: Vec::new(),
9478 patch_hash: "hash".into(),
9479 stdout_log: std::env::temp_dir().join("stdout.log"),
9480 stderr_log: std::env::temp_dir().join("stderr.log"),
9481 };
9482 let validator = CommandValidator::new(SandboxPolicy {
9483 allowed_programs: vec!["git".into()],
9484 max_duration_ms: 1_000,
9485 max_output_bytes: 1024,
9486 denied_env_prefixes: Vec::new(),
9487 max_memory_bytes: None,
9488 max_cpu_secs: None,
9489 use_process_group: false,
9490 });
9491 let report = validator
9492 .run(
9493 &receipt,
9494 &ValidationPlan {
9495 profile: "test".into(),
9496 stages: vec![ValidationStage::Command {
9497 program: "git".into(),
9498 args: vec!["--version".into()],
9499 timeout_ms: 1_000,
9500 }],
9501 },
9502 )
9503 .await
9504 .unwrap();
9505 assert_eq!(report.stages.len(), 1);
9506 }
9507
9508 #[tokio::test]
9509 async fn capture_successful_mutation_appends_capsule() {
9510 let (evo, store) = build_test_evo("capture", "run-1", command_validator());
9511 let capsule = evo
9512 .capture_successful_mutation(&"run-1".into(), sample_mutation())
9513 .await
9514 .unwrap();
9515 let events = store.scan(1).unwrap();
9516 assert!(events
9517 .iter()
9518 .any(|stored| matches!(stored.event, EvolutionEvent::CapsuleCommitted { .. })));
9519 assert!(!capsule.id.is_empty());
9520 }
9521
9522 #[tokio::test]
9523 async fn replay_hit_records_capsule_reused() {
9524 let (evo, store) = build_test_evo("replay", "run-2", command_validator());
9525 let capsule = evo
9526 .capture_successful_mutation(&"run-2".into(), sample_mutation())
9527 .await
9528 .unwrap();
9529 let replay_run_id = "run-replay".to_string();
9530 let decision = evo
9531 .replay_or_fallback_for_run(&replay_run_id, replay_input("missing readme"))
9532 .await
9533 .unwrap();
9534 assert!(decision.used_capsule);
9535 assert_eq!(decision.capsule_id, Some(capsule.id));
9536 assert!(!decision.detect_evidence.task_class_id.is_empty());
9537 assert!(!decision.detect_evidence.matched_signals.is_empty());
9538 assert!(decision.detect_evidence.mismatch_reasons.is_empty());
9539 assert!(!decision.select_evidence.candidates.is_empty());
9540 assert!(!decision.select_evidence.exact_match_lookup);
9541 assert_eq!(
9542 decision.select_evidence.selected_capsule_id.as_deref(),
9543 decision.capsule_id.as_deref()
9544 );
9545 assert!(store.scan(1).unwrap().iter().any(|stored| matches!(
9546 &stored.event,
9547 EvolutionEvent::CapsuleReused {
9548 run_id,
9549 replay_run_id: Some(current_replay_run_id),
9550 ..
9551 } if run_id == "run-2" && current_replay_run_id == &replay_run_id
9552 )));
9553 }
9554
9555 #[tokio::test]
9556 async fn legacy_replay_executor_api_preserves_original_capsule_run_id() {
9557 let capture_run_id = "run-legacy-capture".to_string();
9558 let (evo, store) = build_test_evo("replay-legacy", &capture_run_id, command_validator());
9559 let capsule = evo
9560 .capture_successful_mutation(&capture_run_id, sample_mutation())
9561 .await
9562 .unwrap();
9563 let executor = StoreReplayExecutor {
9564 sandbox: evo.sandbox.clone(),
9565 validator: evo.validator.clone(),
9566 store: evo.store.clone(),
9567 selector: evo.selector.clone(),
9568 governor: evo.governor.clone(),
9569 economics: Some(evo.economics.clone()),
9570 remote_publishers: Some(evo.remote_publishers.clone()),
9571 stake_policy: evo.stake_policy.clone(),
9572 };
9573
9574 let decision = executor
9575 .try_replay(
9576 &replay_input("missing readme"),
9577 &evo.sandbox_policy,
9578 &evo.validation_plan,
9579 )
9580 .await
9581 .unwrap();
9582
9583 assert!(decision.used_capsule);
9584 assert_eq!(decision.capsule_id, Some(capsule.id));
9585 assert!(store.scan(1).unwrap().iter().any(|stored| matches!(
9586 &stored.event,
9587 EvolutionEvent::CapsuleReused {
9588 run_id,
9589 replay_run_id: None,
9590 ..
9591 } if run_id == &capture_run_id
9592 )));
9593 }
9594
9595 #[tokio::test]
9596 async fn metrics_snapshot_tracks_replay_promotion_and_revocation_signals() {
9597 let (evo, _) = build_test_evo("metrics", "run-metrics", command_validator());
9598 let capsule = evo
9599 .capture_successful_mutation(&"run-metrics".into(), sample_mutation())
9600 .await
9601 .unwrap();
9602 let decision = evo
9603 .replay_or_fallback(replay_input("missing readme"))
9604 .await
9605 .unwrap();
9606 assert!(decision.used_capsule);
9607
9608 evo.revoke_assets(&RevokeNotice {
9609 sender_id: "node-metrics".into(),
9610 asset_ids: vec![capsule.id.clone()],
9611 reason: "manual test revoke".into(),
9612 })
9613 .unwrap();
9614
9615 let snapshot = evo.metrics_snapshot().unwrap();
9616 assert_eq!(snapshot.replay_attempts_total, 1);
9617 assert_eq!(snapshot.replay_success_total, 1);
9618 assert_eq!(snapshot.replay_success_rate, 1.0);
9619 assert_eq!(snapshot.confidence_revalidations_total, 0);
9620 assert_eq!(snapshot.replay_reasoning_avoided_total, 1);
9621 assert_eq!(
9622 snapshot.reasoning_avoided_tokens_total,
9623 decision.economics_evidence.reasoning_avoided_tokens
9624 );
9625 assert_eq!(snapshot.replay_fallback_cost_total, 0);
9626 assert_eq!(snapshot.replay_roi, 1.0);
9627 assert_eq!(snapshot.replay_task_classes.len(), 1);
9628 assert_eq!(snapshot.replay_task_classes[0].replay_success_total, 1);
9629 assert_eq!(snapshot.replay_task_classes[0].replay_failure_total, 0);
9630 assert_eq!(
9631 snapshot.replay_task_classes[0].reasoning_steps_avoided_total,
9632 1
9633 );
9634 assert_eq!(
9635 snapshot.replay_task_classes[0].replay_fallback_cost_total,
9636 0
9637 );
9638 assert_eq!(snapshot.replay_task_classes[0].replay_roi, 1.0);
9639 assert!(snapshot.replay_sources.is_empty());
9640 assert_eq!(snapshot.confidence_revalidations_total, 0);
9641 assert_eq!(snapshot.mutation_declared_total, 1);
9642 assert_eq!(snapshot.promoted_mutations_total, 1);
9643 assert_eq!(snapshot.promotion_ratio, 1.0);
9644 assert_eq!(snapshot.gene_revocations_total, 1);
9645 assert_eq!(snapshot.mutation_velocity_last_hour, 1);
9646 assert_eq!(snapshot.revoke_frequency_last_hour, 1);
9647 assert_eq!(snapshot.promoted_genes, 0);
9648 assert_eq!(snapshot.promoted_capsules, 0);
9649
9650 let rendered = evo.render_metrics_prometheus().unwrap();
9651 assert!(rendered.contains("oris_evolution_replay_reasoning_avoided_total 1"));
9652 assert!(rendered.contains("oris_evolution_reasoning_avoided_tokens_total"));
9653 assert!(rendered.contains("oris_evolution_replay_fallback_cost_total"));
9654 assert!(rendered.contains("oris_evolution_replay_roi 1.000000"));
9655 assert!(rendered.contains("oris_evolution_replay_utilization_by_task_class_total"));
9656 assert!(rendered.contains("oris_evolution_replay_reasoning_avoided_by_task_class_total"));
9657 assert!(rendered.contains("oris_evolution_replay_success_rate 1.000000"));
9658 assert!(rendered.contains("oris_evolution_confidence_revalidations_total 0"));
9659 assert!(rendered.contains("oris_evolution_promotion_ratio 1.000000"));
9660 assert!(rendered.contains("oris_evolution_revoke_frequency_last_hour 1"));
9661 assert!(rendered.contains("oris_evolution_mutation_velocity_last_hour 1"));
9662 assert!(rendered.contains("oris_evolution_health 1"));
9663 }
9664
9665 #[tokio::test]
9666 async fn replay_roi_release_gate_summary_matches_metrics_snapshot_for_legacy_replay_history() {
9667 let (evo, _) = build_test_evo("roi-legacy", "run-roi-legacy", command_validator());
9668 let capsule = evo
9669 .capture_successful_mutation(&"run-roi-legacy".into(), sample_mutation())
9670 .await
9671 .unwrap();
9672
9673 evo.store
9674 .append_event(EvolutionEvent::CapsuleReused {
9675 capsule_id: capsule.id.clone(),
9676 gene_id: capsule.gene_id.clone(),
9677 run_id: capsule.run_id.clone(),
9678 replay_run_id: Some("run-roi-legacy-replay".into()),
9679 })
9680 .unwrap();
9681 evo.store
9682 .append_event(EvolutionEvent::ValidationFailed {
9683 mutation_id: "legacy-replay-failure".into(),
9684 report: ValidationSnapshot {
9685 success: false,
9686 profile: "test".into(),
9687 duration_ms: 1,
9688 summary: "legacy replay validation failed".into(),
9689 },
9690 gene_id: Some(capsule.gene_id.clone()),
9691 })
9692 .unwrap();
9693
9694 let metrics = evo.metrics_snapshot().unwrap();
9695 let summary = evo.replay_roi_release_gate_summary(0).unwrap();
9696 let task_class = &metrics.replay_task_classes[0];
9697
9698 assert_eq!(metrics.replay_attempts_total, 2);
9699 assert_eq!(metrics.replay_success_total, 1);
9700 assert_eq!(summary.replay_attempts_total, metrics.replay_attempts_total);
9701 assert_eq!(summary.replay_success_total, metrics.replay_success_total);
9702 assert_eq!(
9703 summary.replay_failure_total,
9704 metrics.replay_attempts_total - metrics.replay_success_total
9705 );
9706 assert_eq!(
9707 summary.reasoning_avoided_tokens_total,
9708 metrics.reasoning_avoided_tokens_total
9709 );
9710 assert_eq!(
9711 summary.replay_fallback_cost_total,
9712 metrics.replay_fallback_cost_total
9713 );
9714 assert_eq!(summary.replay_roi, metrics.replay_roi);
9715 assert_eq!(summary.replay_task_classes.len(), 1);
9716 assert_eq!(
9717 summary.replay_task_classes[0].task_class_id,
9718 task_class.task_class_id
9719 );
9720 assert_eq!(
9721 summary.replay_task_classes[0].replay_success_total,
9722 task_class.replay_success_total
9723 );
9724 assert_eq!(
9725 summary.replay_task_classes[0].replay_failure_total,
9726 task_class.replay_failure_total
9727 );
9728 assert_eq!(
9729 summary.replay_task_classes[0].reasoning_avoided_tokens_total,
9730 task_class.reasoning_avoided_tokens_total
9731 );
9732 assert_eq!(
9733 summary.replay_task_classes[0].replay_fallback_cost_total,
9734 task_class.replay_fallback_cost_total
9735 );
9736 }
9737
9738 #[tokio::test]
9739 async fn replay_roi_release_gate_summary_aggregates_task_class_and_remote_source() {
9740 let (evo, _) = build_test_evo("roi-summary", "run-roi-summary", command_validator());
9741 let envelope = remote_publish_envelope(
9742 "node-roi",
9743 "run-remote-roi",
9744 "gene-roi",
9745 "capsule-roi",
9746 "mutation-roi",
9747 "roi-signal",
9748 "ROI.md",
9749 "# roi",
9750 );
9751 evo.import_remote_envelope(&envelope).unwrap();
9752
9753 let miss = evo
9754 .replay_or_fallback(replay_input("entropy-hash-12345-no-overlap"))
9755 .await
9756 .unwrap();
9757 assert!(!miss.used_capsule);
9758 assert!(miss.fallback_to_planner);
9759 assert!(miss.select_evidence.candidates.is_empty());
9760 assert!(miss
9761 .detect_evidence
9762 .mismatch_reasons
9763 .iter()
9764 .any(|reason| reason == "no_candidate_after_select"));
9765
9766 let hit = evo
9767 .replay_or_fallback(replay_input("roi-signal"))
9768 .await
9769 .unwrap();
9770 assert!(hit.used_capsule);
9771 assert!(!hit.select_evidence.candidates.is_empty());
9772 assert_eq!(
9773 hit.select_evidence.selected_capsule_id.as_deref(),
9774 hit.capsule_id.as_deref()
9775 );
9776
9777 let summary = evo.replay_roi_release_gate_summary(60 * 60).unwrap();
9778 assert_eq!(summary.replay_attempts_total, 2);
9779 assert_eq!(summary.replay_success_total, 1);
9780 assert_eq!(summary.replay_failure_total, 1);
9781 assert!(summary.reasoning_avoided_tokens_total > 0);
9782 assert!(summary.replay_fallback_cost_total > 0);
9783 assert!(summary
9784 .replay_task_classes
9785 .iter()
9786 .any(|entry| { entry.replay_success_total == 1 && entry.replay_failure_total == 0 }));
9787 assert!(summary.replay_sources.iter().any(|source| {
9788 source.source_sender_id == "node-roi" && source.replay_success_total == 1
9789 }));
9790
9791 let rendered = evo
9792 .render_replay_roi_release_gate_summary_json(60 * 60)
9793 .unwrap();
9794 assert!(rendered.contains("\"replay_attempts_total\": 2"));
9795 assert!(rendered.contains("\"source_sender_id\": \"node-roi\""));
9796 }
9797
9798 #[tokio::test]
9799 async fn replay_roi_release_gate_summary_contract_exposes_core_metrics_and_fail_closed_defaults(
9800 ) {
9801 let (evo, _) = build_test_evo("roi-contract", "run-roi-contract", command_validator());
9802 let envelope = remote_publish_envelope(
9803 "node-contract",
9804 "run-remote-contract",
9805 "gene-contract",
9806 "capsule-contract",
9807 "mutation-contract",
9808 "contract-signal",
9809 "CONTRACT.md",
9810 "# contract",
9811 );
9812 evo.import_remote_envelope(&envelope).unwrap();
9813
9814 let miss = evo
9815 .replay_or_fallback(replay_input("entropy-hash-contract-no-overlap"))
9816 .await
9817 .unwrap();
9818 assert!(!miss.used_capsule);
9819 assert!(miss.fallback_to_planner);
9820
9821 let hit = evo
9822 .replay_or_fallback(replay_input("contract-signal"))
9823 .await
9824 .unwrap();
9825 assert!(hit.used_capsule);
9826
9827 let summary = evo.replay_roi_release_gate_summary(60 * 60).unwrap();
9828 let contract = evo
9829 .replay_roi_release_gate_contract(60 * 60, ReplayRoiReleaseGateThresholds::default())
9830 .unwrap();
9831
9832 assert_eq!(contract.input.replay_attempts_total, 2);
9833 assert_eq!(contract.input.replay_success_total, 1);
9834 assert_eq!(contract.input.replay_failure_total, 1);
9835 assert_eq!(
9836 contract.input.reasoning_avoided_tokens,
9837 summary.reasoning_avoided_tokens_total
9838 );
9839 assert_eq!(
9840 contract.input.replay_fallback_cost_total,
9841 summary.replay_fallback_cost_total
9842 );
9843 assert!((contract.input.replay_hit_rate - 0.5).abs() < f64::EPSILON);
9844 assert!((contract.input.false_replay_rate - 0.5).abs() < f64::EPSILON);
9845 assert!((contract.input.replay_roi - summary.replay_roi).abs() < f64::EPSILON);
9846 assert!(contract.input.replay_safety);
9847 assert_eq!(
9848 contract.input.aggregation_dimensions,
9849 REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
9850 .iter()
9851 .map(|dimension| (*dimension).to_string())
9852 .collect::<Vec<_>>()
9853 );
9854 assert_eq!(
9855 contract.input.thresholds,
9856 ReplayRoiReleaseGateThresholds::default()
9857 );
9858 assert_eq!(
9859 contract.input.fail_closed_policy,
9860 ReplayRoiReleaseGateFailClosedPolicy::default()
9861 );
9862 assert_eq!(
9863 contract.output.status,
9864 ReplayRoiReleaseGateStatus::FailClosed
9865 );
9866 assert!(contract
9867 .output
9868 .failed_checks
9869 .iter()
9870 .any(|check| check == "min_replay_attempts_below_threshold"));
9871 assert!(contract
9872 .output
9873 .failed_checks
9874 .iter()
9875 .any(|check| check == "replay_hit_rate_below_threshold"));
9876 assert!(contract
9877 .output
9878 .failed_checks
9879 .iter()
9880 .any(|check| check == "false_replay_rate_above_threshold"));
9881 assert!(contract
9882 .output
9883 .evidence_refs
9884 .iter()
9885 .any(|evidence| evidence == "replay_roi_release_gate_summary"));
9886 assert!(contract.output.summary.contains("release gate fail_closed"));
9887 }
9888
9889 #[tokio::test]
9890 async fn replay_roi_release_gate_summary_contract_accepts_custom_thresholds_and_json() {
9891 let (evo, _) = build_test_evo(
9892 "roi-contract-thresholds",
9893 "run-roi-contract-thresholds",
9894 command_validator(),
9895 );
9896 let thresholds = ReplayRoiReleaseGateThresholds {
9897 min_replay_attempts: 8,
9898 min_replay_hit_rate: 0.75,
9899 max_false_replay_rate: 0.10,
9900 min_reasoning_avoided_tokens: 600,
9901 min_replay_roi: 0.30,
9902 require_replay_safety: true,
9903 };
9904 let contract = evo
9905 .replay_roi_release_gate_contract(60 * 60, thresholds.clone())
9906 .unwrap();
9907 assert_eq!(contract.input.thresholds, thresholds.clone());
9908 assert_eq!(contract.input.replay_attempts_total, 0);
9909 assert_eq!(contract.input.replay_hit_rate, 0.0);
9910 assert_eq!(contract.input.false_replay_rate, 0.0);
9911 assert!(!contract.input.replay_safety_signal.has_replay_activity);
9912 assert!(!contract.input.replay_safety);
9913 assert_eq!(
9914 contract.output.status,
9915 ReplayRoiReleaseGateStatus::Indeterminate
9916 );
9917 assert!(contract
9918 .output
9919 .failed_checks
9920 .iter()
9921 .any(|check| check == "missing_replay_attempts"));
9922 assert!(contract
9923 .output
9924 .summary
9925 .contains("indeterminate (fail-closed)"));
9926
9927 let rendered = evo
9928 .render_replay_roi_release_gate_contract_json(60 * 60, thresholds)
9929 .unwrap();
9930 assert!(rendered.contains("\"min_replay_attempts\": 8"));
9931 assert!(rendered.contains("\"min_replay_hit_rate\": 0.75"));
9932 assert!(rendered.contains("\"status\": \"indeterminate\""));
9933 }
9934
9935 #[tokio::test]
9936 async fn replay_roi_release_gate_summary_window_boundary_filters_old_events() {
9937 let (evo, _) = build_test_evo("roi-window", "run-roi-window", command_validator());
9938 let envelope = remote_publish_envelope(
9939 "node-window",
9940 "run-remote-window",
9941 "gene-window",
9942 "capsule-window",
9943 "mutation-window",
9944 "window-signal",
9945 "WINDOW.md",
9946 "# window",
9947 );
9948 evo.import_remote_envelope(&envelope).unwrap();
9949
9950 let miss = evo
9951 .replay_or_fallback(replay_input("window-no-match-signal"))
9952 .await
9953 .unwrap();
9954 assert!(!miss.used_capsule);
9955 assert!(miss.fallback_to_planner);
9956
9957 let first_hit = evo
9958 .replay_or_fallback(replay_input("window-signal"))
9959 .await
9960 .unwrap();
9961 assert!(first_hit.used_capsule);
9962
9963 std::thread::sleep(std::time::Duration::from_secs(2));
9964
9965 let second_hit = evo
9966 .replay_or_fallback(replay_input("window-signal"))
9967 .await
9968 .unwrap();
9969 assert!(second_hit.used_capsule);
9970
9971 let narrow = evo.replay_roi_release_gate_summary(1).unwrap();
9972 assert_eq!(narrow.replay_attempts_total, 1);
9973 assert_eq!(narrow.replay_success_total, 1);
9974 assert_eq!(narrow.replay_failure_total, 0);
9975
9976 let all = evo.replay_roi_release_gate_summary(0).unwrap();
9977 assert_eq!(all.replay_attempts_total, 3);
9978 assert_eq!(all.replay_success_total, 2);
9979 assert_eq!(all.replay_failure_total, 1);
9980 }
9981
9982 fn fixed_release_gate_pass_fixture() -> ReplayRoiReleaseGateInputContract {
9983 ReplayRoiReleaseGateInputContract {
9984 generated_at: "2026-03-13T00:00:00Z".to_string(),
9985 window_seconds: 86_400,
9986 aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
9987 .iter()
9988 .map(|dimension| (*dimension).to_string())
9989 .collect(),
9990 replay_attempts_total: 4,
9991 replay_success_total: 3,
9992 replay_failure_total: 1,
9993 replay_hit_rate: 0.75,
9994 false_replay_rate: 0.25,
9995 reasoning_avoided_tokens: 480,
9996 replay_fallback_cost_total: 64,
9997 replay_roi: compute_replay_roi(480, 64),
9998 replay_safety: true,
9999 replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
10000 fail_closed_default: true,
10001 rollback_ready: true,
10002 audit_trail_complete: true,
10003 has_replay_activity: true,
10004 },
10005 thresholds: ReplayRoiReleaseGateThresholds::default(),
10006 fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
10007 }
10008 }
10009
10010 fn fixed_release_gate_fail_fixture() -> ReplayRoiReleaseGateInputContract {
10011 ReplayRoiReleaseGateInputContract {
10012 generated_at: "2026-03-13T00:00:00Z".to_string(),
10013 window_seconds: 86_400,
10014 aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
10015 .iter()
10016 .map(|dimension| (*dimension).to_string())
10017 .collect(),
10018 replay_attempts_total: 10,
10019 replay_success_total: 4,
10020 replay_failure_total: 6,
10021 replay_hit_rate: 0.4,
10022 false_replay_rate: 0.6,
10023 reasoning_avoided_tokens: 80,
10024 replay_fallback_cost_total: 400,
10025 replay_roi: compute_replay_roi(80, 400),
10026 replay_safety: false,
10027 replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
10028 fail_closed_default: true,
10029 rollback_ready: true,
10030 audit_trail_complete: true,
10031 has_replay_activity: true,
10032 },
10033 thresholds: ReplayRoiReleaseGateThresholds::default(),
10034 fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
10035 }
10036 }
10037
10038 fn fixed_release_gate_borderline_fixture() -> ReplayRoiReleaseGateInputContract {
10039 ReplayRoiReleaseGateInputContract {
10040 generated_at: "2026-03-13T00:00:00Z".to_string(),
10041 window_seconds: 3_600,
10042 aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
10043 .iter()
10044 .map(|dimension| (*dimension).to_string())
10045 .collect(),
10046 replay_attempts_total: 4,
10047 replay_success_total: 3,
10048 replay_failure_total: 1,
10049 replay_hit_rate: 0.75,
10050 false_replay_rate: 0.25,
10051 reasoning_avoided_tokens: 192,
10052 replay_fallback_cost_total: 173,
10053 replay_roi: 0.05,
10054 replay_safety: true,
10055 replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
10056 fail_closed_default: true,
10057 rollback_ready: true,
10058 audit_trail_complete: true,
10059 has_replay_activity: true,
10060 },
10061 thresholds: ReplayRoiReleaseGateThresholds {
10062 min_replay_attempts: 4,
10063 min_replay_hit_rate: 0.75,
10064 max_false_replay_rate: 0.25,
10065 min_reasoning_avoided_tokens: 192,
10066 min_replay_roi: 0.05,
10067 require_replay_safety: true,
10068 },
10069 fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
10070 }
10071 }
10072
10073 #[test]
10074 fn replay_roi_release_gate_summary_fixed_fixtures_cover_pass_fail_and_borderline() {
10075 let pass =
10076 evaluate_replay_roi_release_gate_contract_input(&fixed_release_gate_pass_fixture());
10077 let fail =
10078 evaluate_replay_roi_release_gate_contract_input(&fixed_release_gate_fail_fixture());
10079 let borderline = evaluate_replay_roi_release_gate_contract_input(
10080 &fixed_release_gate_borderline_fixture(),
10081 );
10082
10083 assert_eq!(pass.status, ReplayRoiReleaseGateStatus::Pass);
10084 assert!(pass.failed_checks.is_empty());
10085 assert_eq!(fail.status, ReplayRoiReleaseGateStatus::FailClosed);
10086 assert!(!fail.failed_checks.is_empty());
10087 assert_eq!(borderline.status, ReplayRoiReleaseGateStatus::Pass);
10088 assert!(borderline.failed_checks.is_empty());
10089 }
10090
10091 #[test]
10092 fn replay_roi_release_gate_summary_machine_readable_output_is_stable_and_sorted() {
10093 let output =
10094 evaluate_replay_roi_release_gate_contract_input(&fixed_release_gate_fail_fixture());
10095
10096 assert_eq!(
10097 output.failed_checks,
10098 vec![
10099 "false_replay_rate_above_threshold".to_string(),
10100 "reasoning_avoided_tokens_below_threshold".to_string(),
10101 "replay_hit_rate_below_threshold".to_string(),
10102 "replay_roi_below_threshold".to_string(),
10103 "replay_safety_required".to_string(),
10104 ]
10105 );
10106 assert_eq!(
10107 output.evidence_refs,
10108 vec![
10109 "generated_at:2026-03-13T00:00:00Z".to_string(),
10110 "metric:false_replay_rate".to_string(),
10111 "metric:reasoning_avoided_tokens".to_string(),
10112 "metric:replay_hit_rate".to_string(),
10113 "metric:replay_roi".to_string(),
10114 "metric:replay_safety".to_string(),
10115 "replay_roi_release_gate_summary".to_string(),
10116 "threshold:max_false_replay_rate".to_string(),
10117 "threshold:min_reasoning_avoided_tokens".to_string(),
10118 "threshold:min_replay_hit_rate".to_string(),
10119 "threshold:min_replay_roi".to_string(),
10120 "threshold:require_replay_safety".to_string(),
10121 "window_seconds:86400".to_string(),
10122 ]
10123 );
10124
10125 let rendered = serde_json::to_string(&output).unwrap();
10126 assert!(rendered.starts_with("{\"status\":\"fail_closed\",\"failed_checks\":"));
10127 assert_eq!(rendered, serde_json::to_string(&output).unwrap());
10128 }
10129
10130 #[test]
10131 fn replay_roi_release_gate_summary_evaluator_passes_with_threshold_compliance() {
10132 let input = ReplayRoiReleaseGateInputContract {
10133 generated_at: Utc::now().to_rfc3339(),
10134 window_seconds: 86_400,
10135 aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
10136 .iter()
10137 .map(|dimension| (*dimension).to_string())
10138 .collect(),
10139 replay_attempts_total: 10,
10140 replay_success_total: 9,
10141 replay_failure_total: 1,
10142 replay_hit_rate: 0.9,
10143 false_replay_rate: 0.1,
10144 reasoning_avoided_tokens: 960,
10145 replay_fallback_cost_total: 64,
10146 replay_roi: compute_replay_roi(960, 64),
10147 replay_safety: true,
10148 replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
10149 fail_closed_default: true,
10150 rollback_ready: true,
10151 audit_trail_complete: true,
10152 has_replay_activity: true,
10153 },
10154 thresholds: ReplayRoiReleaseGateThresholds::default(),
10155 fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
10156 };
10157
10158 let output = evaluate_replay_roi_release_gate_contract_input(&input);
10159 assert_eq!(output.status, ReplayRoiReleaseGateStatus::Pass);
10160 assert!(output.failed_checks.is_empty());
10161 assert!(output.summary.contains("release gate pass"));
10162 }
10163
10164 #[test]
10165 fn replay_roi_release_gate_summary_evaluator_fail_closed_on_threshold_violations() {
10166 let input = ReplayRoiReleaseGateInputContract {
10167 generated_at: Utc::now().to_rfc3339(),
10168 window_seconds: 86_400,
10169 aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
10170 .iter()
10171 .map(|dimension| (*dimension).to_string())
10172 .collect(),
10173 replay_attempts_total: 10,
10174 replay_success_total: 4,
10175 replay_failure_total: 6,
10176 replay_hit_rate: 0.4,
10177 false_replay_rate: 0.6,
10178 reasoning_avoided_tokens: 80,
10179 replay_fallback_cost_total: 400,
10180 replay_roi: compute_replay_roi(80, 400),
10181 replay_safety: false,
10182 replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
10183 fail_closed_default: true,
10184 rollback_ready: true,
10185 audit_trail_complete: true,
10186 has_replay_activity: true,
10187 },
10188 thresholds: ReplayRoiReleaseGateThresholds::default(),
10189 fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
10190 };
10191
10192 let output = evaluate_replay_roi_release_gate_contract_input(&input);
10193 assert_eq!(output.status, ReplayRoiReleaseGateStatus::FailClosed);
10194 assert!(output
10195 .failed_checks
10196 .iter()
10197 .any(|check| check == "replay_hit_rate_below_threshold"));
10198 assert!(output
10199 .failed_checks
10200 .iter()
10201 .any(|check| check == "false_replay_rate_above_threshold"));
10202 assert!(output
10203 .failed_checks
10204 .iter()
10205 .any(|check| check == "replay_roi_below_threshold"));
10206 assert!(output.summary.contains("release gate fail_closed"));
10207 }
10208
10209 #[test]
10210 fn replay_roi_release_gate_summary_evaluator_marks_missing_data_indeterminate() {
10211 let input = ReplayRoiReleaseGateInputContract {
10212 generated_at: String::new(),
10213 window_seconds: 86_400,
10214 aggregation_dimensions: REPLAY_RELEASE_GATE_AGGREGATION_DIMENSIONS
10215 .iter()
10216 .map(|dimension| (*dimension).to_string())
10217 .collect(),
10218 replay_attempts_total: 0,
10219 replay_success_total: 0,
10220 replay_failure_total: 0,
10221 replay_hit_rate: 0.0,
10222 false_replay_rate: 0.0,
10223 reasoning_avoided_tokens: 0,
10224 replay_fallback_cost_total: 0,
10225 replay_roi: 0.0,
10226 replay_safety: false,
10227 replay_safety_signal: ReplayRoiReleaseGateSafetySignal {
10228 fail_closed_default: true,
10229 rollback_ready: true,
10230 audit_trail_complete: true,
10231 has_replay_activity: false,
10232 },
10233 thresholds: ReplayRoiReleaseGateThresholds::default(),
10234 fail_closed_policy: ReplayRoiReleaseGateFailClosedPolicy::default(),
10235 };
10236
10237 let output = evaluate_replay_roi_release_gate_contract_input(&input);
10238 assert_eq!(output.status, ReplayRoiReleaseGateStatus::Indeterminate);
10239 assert!(output
10240 .failed_checks
10241 .iter()
10242 .any(|check| check == "missing_generated_at"));
10243 assert!(output
10244 .failed_checks
10245 .iter()
10246 .any(|check| check == "missing_replay_attempts"));
10247 assert!(output
10248 .summary
10249 .contains("release gate indeterminate (fail-closed)"));
10250 }
10251
10252 #[test]
10253 fn stale_replay_targets_require_confidence_revalidation() {
10254 let now = Utc::now();
10255 let projection = EvolutionProjection {
10256 genes: vec![Gene {
10257 id: "gene-stale".into(),
10258 signals: vec!["missing readme".into()],
10259 strategy: vec!["README.md".into()],
10260 validation: vec!["test".into()],
10261 state: AssetState::Promoted,
10262 task_class_id: None,
10263 }],
10264 capsules: vec![Capsule {
10265 id: "capsule-stale".into(),
10266 gene_id: "gene-stale".into(),
10267 mutation_id: "mutation-stale".into(),
10268 run_id: "run-stale".into(),
10269 diff_hash: "hash".into(),
10270 confidence: 0.8,
10271 env: replay_input("missing readme").env,
10272 outcome: Outcome {
10273 success: true,
10274 validation_profile: "test".into(),
10275 validation_duration_ms: 1,
10276 changed_files: vec!["README.md".into()],
10277 validator_hash: "validator".into(),
10278 lines_changed: 1,
10279 replay_verified: false,
10280 },
10281 state: AssetState::Promoted,
10282 }],
10283 reuse_counts: BTreeMap::from([("gene-stale".into(), 1)]),
10284 attempt_counts: BTreeMap::from([("gene-stale".into(), 1)]),
10285 last_updated_at: BTreeMap::from([(
10286 "gene-stale".into(),
10287 (now - Duration::hours(48)).to_rfc3339(),
10288 )]),
10289 spec_ids_by_gene: BTreeMap::new(),
10290 };
10291
10292 let targets = stale_replay_revalidation_targets(&projection, now);
10293
10294 assert_eq!(targets.len(), 1);
10295 assert_eq!(targets[0].gene_id, "gene-stale");
10296 assert_eq!(targets[0].capsule_ids, vec!["capsule-stale".to_string()]);
10297 assert!(targets[0].decayed_confidence < MIN_REPLAY_CONFIDENCE);
10298 }
10299
10300 #[tokio::test]
10301 async fn remote_replay_prefers_closest_environment_match() {
10302 let (evo, _) = build_test_evo("remote-env", "run-remote-env", command_validator());
10303 let input = replay_input("env-signal");
10304
10305 let envelope_a = remote_publish_envelope_with_env(
10306 "node-a",
10307 "run-remote-a",
10308 "gene-a",
10309 "capsule-a",
10310 "mutation-a",
10311 "env-signal",
10312 "A.md",
10313 "# from a",
10314 input.env.clone(),
10315 );
10316 let envelope_b = remote_publish_envelope_with_env(
10317 "node-b",
10318 "run-remote-b",
10319 "gene-b",
10320 "capsule-b",
10321 "mutation-b",
10322 "env-signal",
10323 "B.md",
10324 "# from b",
10325 EnvFingerprint {
10326 rustc_version: "old-rustc".into(),
10327 cargo_lock_hash: "other-lock".into(),
10328 target_triple: "aarch64-apple-darwin".into(),
10329 os: "linux".into(),
10330 },
10331 );
10332
10333 evo.import_remote_envelope(&envelope_a).unwrap();
10334 evo.import_remote_envelope(&envelope_b).unwrap();
10335
10336 let decision = evo.replay_or_fallback(input).await.unwrap();
10337
10338 assert!(decision.used_capsule);
10339 assert_eq!(decision.capsule_id, Some("capsule-a".into()));
10340 assert!(!decision.fallback_to_planner);
10341 }
10342
10343 #[test]
10344 fn remote_cold_start_scoring_caps_distinct_query_coverage() {
10345 let (evo, _) = build_test_evo("remote-score", "run-remote-score", command_validator());
10346 let input = replay_input("missing readme");
10347
10348 let exact = remote_publish_envelope_with_signals(
10349 "node-exact",
10350 "run-remote-exact",
10351 "gene-exact",
10352 "capsule-exact",
10353 "mutation-exact",
10354 vec!["missing readme".into()],
10355 vec!["missing readme".into()],
10356 "EXACT.md",
10357 "# exact",
10358 input.env.clone(),
10359 );
10360 let overlapping = remote_publish_envelope_with_signals(
10361 "node-overlap",
10362 "run-remote-overlap",
10363 "gene-overlap",
10364 "capsule-overlap",
10365 "mutation-overlap",
10366 vec!["missing readme".into()],
10367 vec!["missing".into(), "readme".into()],
10368 "OVERLAP.md",
10369 "# overlap",
10370 input.env.clone(),
10371 );
10372
10373 evo.import_remote_envelope(&exact).unwrap();
10374 evo.import_remote_envelope(&overlapping).unwrap();
10375
10376 let candidates = quarantined_remote_exact_match_candidates(evo.store.as_ref(), &input);
10377 let exact_candidate = candidates
10378 .iter()
10379 .find(|candidate| candidate.gene.id == "gene-exact")
10380 .unwrap();
10381 let overlap_candidate = candidates
10382 .iter()
10383 .find(|candidate| candidate.gene.id == "gene-overlap")
10384 .unwrap();
10385
10386 assert_eq!(exact_candidate.score, 1.0);
10387 assert_eq!(overlap_candidate.score, 1.0);
10388 assert!(candidates.iter().all(|candidate| candidate.score <= 1.0));
10389 }
10390
10391 #[test]
10392 fn exact_match_candidates_respect_spec_linked_events() {
10393 let (evo, _) = build_test_evo(
10394 "spec-linked-filter",
10395 "run-spec-linked-filter",
10396 command_validator(),
10397 );
10398 let mut input = replay_input("missing readme");
10399 input.spec_id = Some("spec-readme".into());
10400
10401 let mut mutation = sample_mutation();
10402 mutation.intent.id = "mutation-spec-linked".into();
10403 mutation.intent.spec_id = None;
10404 let gene = Gene {
10405 id: "gene-spec-linked".into(),
10406 signals: vec!["missing readme".into()],
10407 strategy: vec!["README.md".into()],
10408 validation: vec!["test".into()],
10409 state: AssetState::Promoted,
10410 task_class_id: None,
10411 };
10412 let capsule = Capsule {
10413 id: "capsule-spec-linked".into(),
10414 gene_id: gene.id.clone(),
10415 mutation_id: mutation.intent.id.clone(),
10416 run_id: "run-spec-linked".into(),
10417 diff_hash: mutation.artifact.content_hash.clone(),
10418 confidence: 0.9,
10419 env: input.env.clone(),
10420 outcome: Outcome {
10421 success: true,
10422 validation_profile: "test".into(),
10423 validation_duration_ms: 1,
10424 changed_files: vec!["README.md".into()],
10425 validator_hash: "validator-hash".into(),
10426 lines_changed: 1,
10427 replay_verified: false,
10428 },
10429 state: AssetState::Promoted,
10430 };
10431
10432 evo.store
10433 .append_event(EvolutionEvent::MutationDeclared { mutation })
10434 .unwrap();
10435 evo.store
10436 .append_event(EvolutionEvent::GeneProjected { gene })
10437 .unwrap();
10438 evo.store
10439 .append_event(EvolutionEvent::CapsuleCommitted { capsule })
10440 .unwrap();
10441 evo.store
10442 .append_event(EvolutionEvent::SpecLinked {
10443 mutation_id: "mutation-spec-linked".into(),
10444 spec_id: "spec-readme".into(),
10445 })
10446 .unwrap();
10447
10448 let candidates = exact_match_candidates(evo.store.as_ref(), &input);
10449 assert_eq!(candidates.len(), 1);
10450 assert_eq!(candidates[0].gene.id, "gene-spec-linked");
10451 }
10452
10453 #[tokio::test]
10454 async fn remote_capsule_advances_from_quarantine_to_shadow_then_promoted() {
10455 let (evo, store) = build_test_evo(
10456 "remote-quarantine",
10457 "run-remote-quarantine",
10458 command_validator(),
10459 );
10460 let envelope = remote_publish_envelope(
10461 "node-remote",
10462 "run-remote-quarantine",
10463 "gene-remote",
10464 "capsule-remote",
10465 "mutation-remote",
10466 "remote-signal",
10467 "REMOTE.md",
10468 "# from remote",
10469 );
10470
10471 evo.import_remote_envelope(&envelope).unwrap();
10472
10473 let before_replay = store.rebuild_projection().unwrap();
10474 let imported_gene = before_replay
10475 .genes
10476 .iter()
10477 .find(|gene| gene.id == "gene-remote")
10478 .unwrap();
10479 let imported_capsule = before_replay
10480 .capsules
10481 .iter()
10482 .find(|capsule| capsule.id == "capsule-remote")
10483 .unwrap();
10484 assert_eq!(imported_gene.state, AssetState::Quarantined);
10485 assert_eq!(imported_capsule.state, AssetState::Quarantined);
10486 let exported_before_replay =
10487 export_promoted_assets_from_store(store.as_ref(), "node-local").unwrap();
10488 assert!(exported_before_replay.assets.is_empty());
10489
10490 let first_decision = evo
10491 .replay_or_fallback(replay_input("remote-signal"))
10492 .await
10493 .unwrap();
10494
10495 assert!(first_decision.used_capsule);
10496 assert_eq!(first_decision.capsule_id, Some("capsule-remote".into()));
10497
10498 let after_first_replay = store.rebuild_projection().unwrap();
10499 let shadow_gene = after_first_replay
10500 .genes
10501 .iter()
10502 .find(|gene| gene.id == "gene-remote")
10503 .unwrap();
10504 let shadow_capsule = after_first_replay
10505 .capsules
10506 .iter()
10507 .find(|capsule| capsule.id == "capsule-remote")
10508 .unwrap();
10509 assert_eq!(shadow_gene.state, AssetState::ShadowValidated);
10510 assert_eq!(shadow_capsule.state, AssetState::ShadowValidated);
10511 let exported_after_first_replay =
10512 export_promoted_assets_from_store(store.as_ref(), "node-local").unwrap();
10513 assert!(exported_after_first_replay.assets.is_empty());
10514
10515 let second_decision = evo
10516 .replay_or_fallback(replay_input("remote-signal"))
10517 .await
10518 .unwrap();
10519 assert!(second_decision.used_capsule);
10520 assert_eq!(second_decision.capsule_id, Some("capsule-remote".into()));
10521
10522 let after_second_replay = store.rebuild_projection().unwrap();
10523 let promoted_gene = after_second_replay
10524 .genes
10525 .iter()
10526 .find(|gene| gene.id == "gene-remote")
10527 .unwrap();
10528 let promoted_capsule = after_second_replay
10529 .capsules
10530 .iter()
10531 .find(|capsule| capsule.id == "capsule-remote")
10532 .unwrap();
10533 assert_eq!(promoted_gene.state, AssetState::Promoted);
10534 assert_eq!(promoted_capsule.state, AssetState::Promoted);
10535 let exported_after_second_replay =
10536 export_promoted_assets_from_store(store.as_ref(), "node-local").unwrap();
10537 assert_eq!(exported_after_second_replay.assets.len(), 3);
10538 assert!(exported_after_second_replay
10539 .assets
10540 .iter()
10541 .any(|asset| matches!(
10542 asset,
10543 NetworkAsset::EvolutionEvent {
10544 event: EvolutionEvent::MutationDeclared { .. }
10545 }
10546 )));
10547 }
10548
10549 #[tokio::test]
10550 async fn publish_local_assets_include_mutation_payload_for_remote_replay() {
10551 let (source, source_store) = build_test_evo(
10552 "remote-publish-export",
10553 "run-remote-publish-export",
10554 command_validator(),
10555 );
10556 source
10557 .capture_successful_mutation(&"run-remote-publish-export".into(), sample_mutation())
10558 .await
10559 .unwrap();
10560 let envelope = EvolutionNetworkNode::new(source_store.clone())
10561 .publish_local_assets("node-source")
10562 .unwrap();
10563 assert!(envelope.assets.iter().any(|asset| matches!(
10564 asset,
10565 NetworkAsset::EvolutionEvent {
10566 event: EvolutionEvent::MutationDeclared { mutation }
10567 } if mutation.intent.id == "mutation-1"
10568 )));
10569
10570 let (remote, _) = build_test_evo(
10571 "remote-publish-import",
10572 "run-remote-publish-import",
10573 command_validator(),
10574 );
10575 remote.import_remote_envelope(&envelope).unwrap();
10576
10577 let decision = remote
10578 .replay_or_fallback(replay_input("missing readme"))
10579 .await
10580 .unwrap();
10581
10582 assert!(decision.used_capsule);
10583 assert!(!decision.fallback_to_planner);
10584 }
10585
10586 #[tokio::test]
10587 async fn import_remote_envelope_records_manifest_validation_event() {
10588 let (source, source_store) = build_test_evo(
10589 "remote-manifest-success-source",
10590 "run-remote-manifest-success-source",
10591 command_validator(),
10592 );
10593 source
10594 .capture_successful_mutation(
10595 &"run-remote-manifest-success-source".into(),
10596 sample_mutation(),
10597 )
10598 .await
10599 .unwrap();
10600 let envelope = EvolutionNetworkNode::new(source_store.clone())
10601 .publish_local_assets("node-source")
10602 .unwrap();
10603
10604 let (remote, remote_store) = build_test_evo(
10605 "remote-manifest-success-remote",
10606 "run-remote-manifest-success-remote",
10607 command_validator(),
10608 );
10609 remote.import_remote_envelope(&envelope).unwrap();
10610
10611 let events = remote_store.scan(1).unwrap();
10612 assert!(events.iter().any(|stored| matches!(
10613 &stored.event,
10614 EvolutionEvent::ManifestValidated {
10615 accepted: true,
10616 reason,
10617 sender_id: Some(sender_id),
10618 publisher: Some(publisher),
10619 asset_ids,
10620 } if reason == "manifest validated"
10621 && sender_id == "node-source"
10622 && publisher == "node-source"
10623 && !asset_ids.is_empty()
10624 )));
10625 }
10626
10627 #[test]
10628 fn import_remote_envelope_rejects_invalid_manifest_and_records_audit_event() {
10629 let (remote, remote_store) = build_test_evo(
10630 "remote-manifest-invalid",
10631 "run-remote-manifest-invalid",
10632 command_validator(),
10633 );
10634 let mut envelope = remote_publish_envelope(
10635 "node-remote",
10636 "run-remote-manifest-invalid",
10637 "gene-remote",
10638 "capsule-remote",
10639 "mutation-remote",
10640 "manifest-signal",
10641 "MANIFEST.md",
10642 "# drift",
10643 );
10644 if let Some(manifest) = envelope.manifest.as_mut() {
10645 manifest.asset_hash = "tampered-hash".to_string();
10646 }
10647 envelope.content_hash = envelope.compute_content_hash();
10648
10649 let error = remote.import_remote_envelope(&envelope).unwrap_err();
10650 assert!(error.to_string().contains("manifest"));
10651
10652 let events = remote_store.scan(1).unwrap();
10653 assert!(events.iter().any(|stored| matches!(
10654 &stored.event,
10655 EvolutionEvent::ManifestValidated {
10656 accepted: false,
10657 reason,
10658 sender_id: Some(sender_id),
10659 publisher: Some(publisher),
10660 asset_ids,
10661 } if reason.contains("manifest asset_hash mismatch")
10662 && sender_id == "node-remote"
10663 && publisher == "node-remote"
10664 && !asset_ids.is_empty()
10665 )));
10666 }
10667
10668 #[tokio::test]
10669 async fn fetch_assets_include_mutation_payload_for_remote_replay() {
10670 let (evo, store) = build_test_evo(
10671 "remote-fetch-export",
10672 "run-remote-fetch",
10673 command_validator(),
10674 );
10675 evo.capture_successful_mutation(&"run-remote-fetch".into(), sample_mutation())
10676 .await
10677 .unwrap();
10678
10679 let response = EvolutionNetworkNode::new(store.clone())
10680 .fetch_assets(
10681 "node-source",
10682 &FetchQuery {
10683 sender_id: "node-client".into(),
10684 signals: vec!["missing readme".into()],
10685 since_cursor: None,
10686 resume_token: None,
10687 },
10688 )
10689 .unwrap();
10690
10691 assert!(response.assets.iter().any(|asset| matches!(
10692 asset,
10693 NetworkAsset::EvolutionEvent {
10694 event: EvolutionEvent::MutationDeclared { mutation }
10695 } if mutation.intent.id == "mutation-1"
10696 )));
10697 assert!(response
10698 .assets
10699 .iter()
10700 .any(|asset| matches!(asset, NetworkAsset::Gene { .. })));
10701 assert!(response
10702 .assets
10703 .iter()
10704 .any(|asset| matches!(asset, NetworkAsset::Capsule { .. })));
10705 }
10706
10707 #[test]
10708 fn fetch_assets_delta_sync_supports_since_cursor_and_resume_token() {
10709 let store_root =
10710 std::env::temp_dir().join(format!("oris-evokernel-fetch-delta-store-{}", next_id("t")));
10711 if store_root.exists() {
10712 fs::remove_dir_all(&store_root).unwrap();
10713 }
10714 let store: Arc<dyn EvolutionStore> =
10715 Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
10716 let node = EvolutionNetworkNode::new(store.clone());
10717 node.record_reported_experience(
10718 "delta-agent",
10719 "gene-delta-a",
10720 vec!["delta.signal".into()],
10721 vec![
10722 "task_class=delta.signal".into(),
10723 "task_label=delta replay".into(),
10724 ],
10725 vec!["a2a.tasks.report".into()],
10726 )
10727 .unwrap();
10728
10729 let first = node
10730 .fetch_assets(
10731 "execution-api",
10732 &FetchQuery {
10733 sender_id: "delta-agent".into(),
10734 signals: vec!["delta.signal".into()],
10735 since_cursor: None,
10736 resume_token: None,
10737 },
10738 )
10739 .unwrap();
10740 let first_cursor = first.next_cursor.clone().expect("first next_cursor");
10741 let first_token = first.resume_token.clone().expect("first resume_token");
10742 assert!(first.assets.iter().any(
10743 |asset| matches!(asset, NetworkAsset::Gene { gene } if gene.id == "gene-delta-a")
10744 ));
10745
10746 let restarted = EvolutionNetworkNode::new(store.clone());
10747 restarted
10748 .record_reported_experience(
10749 "delta-agent",
10750 "gene-delta-b",
10751 vec!["delta.signal".into()],
10752 vec![
10753 "task_class=delta.signal".into(),
10754 "task_label=delta replay".into(),
10755 ],
10756 vec!["a2a.tasks.report".into()],
10757 )
10758 .unwrap();
10759
10760 let from_token = restarted
10761 .fetch_assets(
10762 "execution-api",
10763 &FetchQuery {
10764 sender_id: "delta-agent".into(),
10765 signals: vec!["delta.signal".into()],
10766 since_cursor: None,
10767 resume_token: Some(first_token),
10768 },
10769 )
10770 .unwrap();
10771 assert!(from_token.assets.iter().any(
10772 |asset| matches!(asset, NetworkAsset::Gene { gene } if gene.id == "gene-delta-b")
10773 ));
10774 assert!(!from_token.assets.iter().any(
10775 |asset| matches!(asset, NetworkAsset::Gene { gene } if gene.id == "gene-delta-a")
10776 ));
10777 assert_eq!(
10778 from_token.sync_audit.requested_cursor,
10779 Some(first_cursor.clone())
10780 );
10781 assert!(from_token.sync_audit.applied_count >= 1);
10782
10783 let from_cursor = restarted
10784 .fetch_assets(
10785 "execution-api",
10786 &FetchQuery {
10787 sender_id: "delta-agent".into(),
10788 signals: vec!["delta.signal".into()],
10789 since_cursor: Some(first_cursor),
10790 resume_token: None,
10791 },
10792 )
10793 .unwrap();
10794 assert!(from_cursor.assets.iter().any(
10795 |asset| matches!(asset, NetworkAsset::Gene { gene } if gene.id == "gene-delta-b")
10796 ));
10797 }
10798
10799 #[test]
10800 fn partial_remote_import_keeps_publisher_for_already_imported_assets() {
10801 let store_root = std::env::temp_dir().join(format!(
10802 "oris-evokernel-remote-partial-store-{}",
10803 std::process::id()
10804 ));
10805 if store_root.exists() {
10806 fs::remove_dir_all(&store_root).unwrap();
10807 }
10808 let store: Arc<dyn EvolutionStore> = Arc::new(FailOnAppendStore::new(store_root, 5));
10809 let evo = build_test_evo_with_store(
10810 "remote-partial",
10811 "run-remote-partial",
10812 command_validator(),
10813 store.clone(),
10814 );
10815 let envelope = remote_publish_envelope(
10816 "node-partial",
10817 "run-remote-partial",
10818 "gene-partial",
10819 "capsule-partial",
10820 "mutation-partial",
10821 "partial-signal",
10822 "PARTIAL.md",
10823 "# partial",
10824 );
10825
10826 let result = evo.import_remote_envelope(&envelope);
10827
10828 assert!(matches!(result, Err(EvoKernelError::Store(_))));
10829 let projection = store.rebuild_projection().unwrap();
10830 assert!(projection
10831 .genes
10832 .iter()
10833 .any(|gene| gene.id == "gene-partial"));
10834 assert!(projection.capsules.is_empty());
10835 let publishers = evo.remote_publishers.lock().unwrap();
10836 assert_eq!(
10837 publishers.get("gene-partial").map(String::as_str),
10838 Some("node-partial")
10839 );
10840 }
10841
10842 #[test]
10843 fn retry_remote_import_after_partial_failure_only_imports_missing_assets() {
10844 let store_root = std::env::temp_dir().join(format!(
10845 "oris-evokernel-remote-partial-retry-store-{}",
10846 next_id("t")
10847 ));
10848 if store_root.exists() {
10849 fs::remove_dir_all(&store_root).unwrap();
10850 }
10851 let store: Arc<dyn EvolutionStore> = Arc::new(FailOnAppendStore::new(store_root, 5));
10852 let evo = build_test_evo_with_store(
10853 "remote-partial-retry",
10854 "run-remote-partial-retry",
10855 command_validator(),
10856 store.clone(),
10857 );
10858 let envelope = remote_publish_envelope(
10859 "node-partial",
10860 "run-remote-partial-retry",
10861 "gene-partial-retry",
10862 "capsule-partial-retry",
10863 "mutation-partial-retry",
10864 "partial-retry-signal",
10865 "PARTIAL_RETRY.md",
10866 "# partial retry",
10867 );
10868
10869 let first = evo.import_remote_envelope(&envelope);
10870 assert!(matches!(first, Err(EvoKernelError::Store(_))));
10871
10872 let retry = evo.import_remote_envelope(&envelope).unwrap();
10873
10874 assert_eq!(retry.imported_asset_ids, vec!["capsule-partial-retry"]);
10875 let projection = store.rebuild_projection().unwrap();
10876 let gene = projection
10877 .genes
10878 .iter()
10879 .find(|gene| gene.id == "gene-partial-retry")
10880 .unwrap();
10881 assert_eq!(gene.state, AssetState::Quarantined);
10882 let capsule = projection
10883 .capsules
10884 .iter()
10885 .find(|capsule| capsule.id == "capsule-partial-retry")
10886 .unwrap();
10887 assert_eq!(capsule.state, AssetState::Quarantined);
10888 assert_eq!(projection.attempt_counts["gene-partial-retry"], 1);
10889
10890 let events = store.scan(1).unwrap();
10891 assert_eq!(
10892 events
10893 .iter()
10894 .filter(|stored| {
10895 matches!(
10896 &stored.event,
10897 EvolutionEvent::MutationDeclared { mutation }
10898 if mutation.intent.id == "mutation-partial-retry"
10899 )
10900 })
10901 .count(),
10902 1
10903 );
10904 assert_eq!(
10905 events
10906 .iter()
10907 .filter(|stored| {
10908 matches!(
10909 &stored.event,
10910 EvolutionEvent::GeneProjected { gene } if gene.id == "gene-partial-retry"
10911 )
10912 })
10913 .count(),
10914 1
10915 );
10916 assert_eq!(
10917 events
10918 .iter()
10919 .filter(|stored| {
10920 matches!(
10921 &stored.event,
10922 EvolutionEvent::CapsuleCommitted { capsule }
10923 if capsule.id == "capsule-partial-retry"
10924 )
10925 })
10926 .count(),
10927 1
10928 );
10929 }
10930
10931 #[tokio::test]
10932 async fn duplicate_remote_import_does_not_requarantine_locally_validated_assets() {
10933 let (evo, store) = build_test_evo(
10934 "remote-idempotent",
10935 "run-remote-idempotent",
10936 command_validator(),
10937 );
10938 let envelope = remote_publish_envelope(
10939 "node-idempotent",
10940 "run-remote-idempotent",
10941 "gene-idempotent",
10942 "capsule-idempotent",
10943 "mutation-idempotent",
10944 "idempotent-signal",
10945 "IDEMPOTENT.md",
10946 "# idempotent",
10947 );
10948
10949 let first = evo.import_remote_envelope(&envelope).unwrap();
10950 assert_eq!(
10951 first.imported_asset_ids,
10952 vec!["gene-idempotent", "capsule-idempotent"]
10953 );
10954
10955 let decision = evo
10956 .replay_or_fallback(replay_input("idempotent-signal"))
10957 .await
10958 .unwrap();
10959 assert!(decision.used_capsule);
10960 assert_eq!(decision.capsule_id, Some("capsule-idempotent".into()));
10961
10962 let projection_before = store.rebuild_projection().unwrap();
10963 let attempts_before = projection_before.attempt_counts["gene-idempotent"];
10964 let gene_before = projection_before
10965 .genes
10966 .iter()
10967 .find(|gene| gene.id == "gene-idempotent")
10968 .unwrap();
10969 assert_eq!(gene_before.state, AssetState::ShadowValidated);
10970 let capsule_before = projection_before
10971 .capsules
10972 .iter()
10973 .find(|capsule| capsule.id == "capsule-idempotent")
10974 .unwrap();
10975 assert_eq!(capsule_before.state, AssetState::ShadowValidated);
10976
10977 let second = evo.import_remote_envelope(&envelope).unwrap();
10978 assert!(second.imported_asset_ids.is_empty());
10979
10980 let projection_after = store.rebuild_projection().unwrap();
10981 assert_eq!(
10982 projection_after.attempt_counts["gene-idempotent"],
10983 attempts_before
10984 );
10985 let gene_after = projection_after
10986 .genes
10987 .iter()
10988 .find(|gene| gene.id == "gene-idempotent")
10989 .unwrap();
10990 assert_eq!(gene_after.state, AssetState::ShadowValidated);
10991 let capsule_after = projection_after
10992 .capsules
10993 .iter()
10994 .find(|capsule| capsule.id == "capsule-idempotent")
10995 .unwrap();
10996 assert_eq!(capsule_after.state, AssetState::ShadowValidated);
10997
10998 let third_decision = evo
10999 .replay_or_fallback(replay_input("idempotent-signal"))
11000 .await
11001 .unwrap();
11002 assert!(third_decision.used_capsule);
11003 assert_eq!(third_decision.capsule_id, Some("capsule-idempotent".into()));
11004
11005 let projection_promoted = store.rebuild_projection().unwrap();
11006 let promoted_gene = projection_promoted
11007 .genes
11008 .iter()
11009 .find(|gene| gene.id == "gene-idempotent")
11010 .unwrap();
11011 let promoted_capsule = projection_promoted
11012 .capsules
11013 .iter()
11014 .find(|capsule| capsule.id == "capsule-idempotent")
11015 .unwrap();
11016 assert_eq!(promoted_gene.state, AssetState::Promoted);
11017 assert_eq!(promoted_capsule.state, AssetState::Promoted);
11018
11019 let events = store.scan(1).unwrap();
11020 assert_eq!(
11021 events
11022 .iter()
11023 .filter(|stored| {
11024 matches!(
11025 &stored.event,
11026 EvolutionEvent::MutationDeclared { mutation }
11027 if mutation.intent.id == "mutation-idempotent"
11028 )
11029 })
11030 .count(),
11031 1
11032 );
11033 assert_eq!(
11034 events
11035 .iter()
11036 .filter(|stored| {
11037 matches!(
11038 &stored.event,
11039 EvolutionEvent::GeneProjected { gene } if gene.id == "gene-idempotent"
11040 )
11041 })
11042 .count(),
11043 1
11044 );
11045 assert_eq!(
11046 events
11047 .iter()
11048 .filter(|stored| {
11049 matches!(
11050 &stored.event,
11051 EvolutionEvent::CapsuleCommitted { capsule }
11052 if capsule.id == "capsule-idempotent"
11053 )
11054 })
11055 .count(),
11056 1
11057 );
11058
11059 assert_eq!(first.sync_audit.scanned_count, envelope.assets.len());
11060 assert_eq!(first.sync_audit.failed_count, 0);
11061 assert_eq!(second.sync_audit.applied_count, 0);
11062 assert_eq!(second.sync_audit.skipped_count, envelope.assets.len());
11063 assert!(second.resume_token.is_some());
11064 }
11065
11066 #[tokio::test]
11067 async fn insufficient_evu_blocks_publish_but_not_local_replay() {
11068 let (evo, _) = build_test_evo("stake-gate", "run-stake", command_validator());
11069 let capsule = evo
11070 .capture_successful_mutation(&"run-stake".into(), sample_mutation())
11071 .await
11072 .unwrap();
11073 let publish = evo.export_promoted_assets("node-local");
11074 assert!(matches!(publish, Err(EvoKernelError::Validation(_))));
11075
11076 let decision = evo
11077 .replay_or_fallback(replay_input("missing readme"))
11078 .await
11079 .unwrap();
11080 assert!(decision.used_capsule);
11081 assert_eq!(decision.capsule_id, Some(capsule.id));
11082 }
11083
11084 #[tokio::test]
11085 async fn second_replay_validation_failure_revokes_gene_immediately() {
11086 let (capturer, store) = build_test_evo("revoke-replay", "run-capture", command_validator());
11087 let capsule = capturer
11088 .capture_successful_mutation(&"run-capture".into(), sample_mutation())
11089 .await
11090 .unwrap();
11091
11092 let failing_validator: Arc<dyn Validator> = Arc::new(FixedValidator { success: false });
11093 let failing_replay = build_test_evo_with_store(
11094 "revoke-replay",
11095 "run-replay-fail",
11096 failing_validator,
11097 store.clone(),
11098 );
11099
11100 let first = failing_replay
11101 .replay_or_fallback(replay_input("missing readme"))
11102 .await
11103 .unwrap();
11104 let second = failing_replay
11105 .replay_or_fallback(replay_input("missing readme"))
11106 .await
11107 .unwrap();
11108
11109 assert!(!first.used_capsule);
11110 assert!(first.fallback_to_planner);
11111 assert!(!second.used_capsule);
11112 assert!(second.fallback_to_planner);
11113
11114 let projection = store.rebuild_projection().unwrap();
11115 let gene = projection
11116 .genes
11117 .iter()
11118 .find(|gene| gene.id == capsule.gene_id)
11119 .unwrap();
11120 assert_eq!(gene.state, AssetState::Promoted);
11121 let committed_capsule = projection
11122 .capsules
11123 .iter()
11124 .find(|current| current.id == capsule.id)
11125 .unwrap();
11126 assert_eq!(committed_capsule.state, AssetState::Promoted);
11127
11128 let events = store.scan(1).unwrap();
11129 assert_eq!(
11130 events
11131 .iter()
11132 .filter(|stored| {
11133 matches!(
11134 &stored.event,
11135 EvolutionEvent::ValidationFailed {
11136 gene_id: Some(gene_id),
11137 ..
11138 } if gene_id == &capsule.gene_id
11139 )
11140 })
11141 .count(),
11142 1
11143 );
11144 assert!(!events.iter().any(|stored| {
11145 matches!(
11146 &stored.event,
11147 EvolutionEvent::GeneRevoked { gene_id, .. } if gene_id == &capsule.gene_id
11148 )
11149 }));
11150
11151 let recovered = build_test_evo_with_store(
11152 "revoke-replay",
11153 "run-replay-check",
11154 command_validator(),
11155 store.clone(),
11156 );
11157 let after_revoke = recovered
11158 .replay_or_fallback(replay_input("missing readme"))
11159 .await
11160 .unwrap();
11161 assert!(!after_revoke.used_capsule);
11162 assert!(after_revoke.fallback_to_planner);
11163 assert!(after_revoke.reason.contains("below replay threshold"));
11164 }
11165
11166 #[tokio::test]
11167 async fn remote_reuse_success_rewards_publisher_and_biases_selection() {
11168 let ledger = Arc::new(Mutex::new(EvuLedger {
11169 accounts: vec![],
11170 reputations: vec![
11171 oris_economics::ReputationRecord {
11172 node_id: "node-a".into(),
11173 publish_success_rate: 0.4,
11174 validator_accuracy: 0.4,
11175 reuse_impact: 0,
11176 },
11177 oris_economics::ReputationRecord {
11178 node_id: "node-b".into(),
11179 publish_success_rate: 0.95,
11180 validator_accuracy: 0.95,
11181 reuse_impact: 8,
11182 },
11183 ],
11184 }));
11185 let (evo, _) = build_test_evo("remote-success", "run-remote", command_validator());
11186 let evo = evo.with_economics(ledger.clone());
11187
11188 let envelope_a = remote_publish_envelope(
11189 "node-a",
11190 "run-remote-a",
11191 "gene-a",
11192 "capsule-a",
11193 "mutation-a",
11194 "shared-signal",
11195 "A.md",
11196 "# from a",
11197 );
11198 let envelope_b = remote_publish_envelope(
11199 "node-b",
11200 "run-remote-b",
11201 "gene-b",
11202 "capsule-b",
11203 "mutation-b",
11204 "shared-signal",
11205 "B.md",
11206 "# from b",
11207 );
11208
11209 evo.import_remote_envelope(&envelope_a).unwrap();
11210 evo.import_remote_envelope(&envelope_b).unwrap();
11211
11212 let decision = evo
11213 .replay_or_fallback(replay_input("shared-signal"))
11214 .await
11215 .unwrap();
11216
11217 assert!(decision.used_capsule);
11218 assert_eq!(decision.capsule_id, Some("capsule-b".into()));
11219 let locked = ledger.lock().unwrap();
11220 let rewarded = locked
11221 .accounts
11222 .iter()
11223 .find(|item| item.node_id == "node-b")
11224 .unwrap();
11225 assert_eq!(rewarded.balance, evo.stake_policy.reuse_reward);
11226 assert!(
11227 locked.selector_reputation_bias()["node-b"]
11228 > locked.selector_reputation_bias()["node-a"]
11229 );
11230 }
11231
11232 #[tokio::test]
11233 async fn remote_reuse_settlement_tracks_selected_capsule_publisher_for_shared_gene() {
11234 let ledger = Arc::new(Mutex::new(EvuLedger::default()));
11235 let (evo, _) = build_test_evo(
11236 "remote-shared-publisher",
11237 "run-remote-shared-publisher",
11238 command_validator(),
11239 );
11240 let evo = evo.with_economics(ledger.clone());
11241 let input = replay_input("shared-signal");
11242 let preferred = remote_publish_envelope_with_env(
11243 "node-a",
11244 "run-remote-a",
11245 "gene-shared",
11246 "capsule-preferred",
11247 "mutation-preferred",
11248 "shared-signal",
11249 "A.md",
11250 "# from a",
11251 input.env.clone(),
11252 );
11253 let fallback = remote_publish_envelope_with_env(
11254 "node-b",
11255 "run-remote-b",
11256 "gene-shared",
11257 "capsule-fallback",
11258 "mutation-fallback",
11259 "shared-signal",
11260 "B.md",
11261 "# from b",
11262 EnvFingerprint {
11263 rustc_version: "old-rustc".into(),
11264 cargo_lock_hash: "other-lock".into(),
11265 target_triple: "aarch64-apple-darwin".into(),
11266 os: "linux".into(),
11267 },
11268 );
11269
11270 evo.import_remote_envelope(&preferred).unwrap();
11271 evo.import_remote_envelope(&fallback).unwrap();
11272
11273 let decision = evo.replay_or_fallback(input).await.unwrap();
11274
11275 assert!(decision.used_capsule);
11276 assert_eq!(decision.capsule_id, Some("capsule-preferred".into()));
11277 let locked = ledger.lock().unwrap();
11278 let rewarded = locked
11279 .accounts
11280 .iter()
11281 .find(|item| item.node_id == "node-a")
11282 .unwrap();
11283 assert_eq!(rewarded.balance, evo.stake_policy.reuse_reward);
11284 assert!(locked.accounts.iter().all(|item| item.node_id != "node-b"));
11285 }
11286
11287 #[test]
11288 fn select_candidates_surfaces_ranked_remote_cold_start_candidates() {
11289 let ledger = Arc::new(Mutex::new(EvuLedger {
11290 accounts: vec![],
11291 reputations: vec![
11292 oris_economics::ReputationRecord {
11293 node_id: "node-a".into(),
11294 publish_success_rate: 0.4,
11295 validator_accuracy: 0.4,
11296 reuse_impact: 0,
11297 },
11298 oris_economics::ReputationRecord {
11299 node_id: "node-b".into(),
11300 publish_success_rate: 0.95,
11301 validator_accuracy: 0.95,
11302 reuse_impact: 8,
11303 },
11304 ],
11305 }));
11306 let (evo, _) = build_test_evo("remote-select", "run-remote-select", command_validator());
11307 let evo = evo.with_economics(ledger);
11308
11309 let envelope_a = remote_publish_envelope(
11310 "node-a",
11311 "run-remote-a",
11312 "gene-a",
11313 "capsule-a",
11314 "mutation-a",
11315 "shared-signal",
11316 "A.md",
11317 "# from a",
11318 );
11319 let envelope_b = remote_publish_envelope(
11320 "node-b",
11321 "run-remote-b",
11322 "gene-b",
11323 "capsule-b",
11324 "mutation-b",
11325 "shared-signal",
11326 "B.md",
11327 "# from b",
11328 );
11329
11330 evo.import_remote_envelope(&envelope_a).unwrap();
11331 evo.import_remote_envelope(&envelope_b).unwrap();
11332
11333 let candidates = evo.select_candidates(&replay_input("shared-signal"));
11334
11335 assert_eq!(candidates.len(), 1);
11336 assert_eq!(candidates[0].gene.id, "gene-b");
11337 assert_eq!(candidates[0].capsules[0].id, "capsule-b");
11338 }
11339
11340 #[tokio::test]
11341 async fn remote_reuse_publisher_bias_survives_restart() {
11342 let ledger = Arc::new(Mutex::new(EvuLedger {
11343 accounts: vec![],
11344 reputations: vec![
11345 oris_economics::ReputationRecord {
11346 node_id: "node-a".into(),
11347 publish_success_rate: 0.4,
11348 validator_accuracy: 0.4,
11349 reuse_impact: 0,
11350 },
11351 oris_economics::ReputationRecord {
11352 node_id: "node-b".into(),
11353 publish_success_rate: 0.95,
11354 validator_accuracy: 0.95,
11355 reuse_impact: 8,
11356 },
11357 ],
11358 }));
11359 let store_root = std::env::temp_dir().join(format!(
11360 "oris-evokernel-remote-restart-store-{}",
11361 next_id("t")
11362 ));
11363 if store_root.exists() {
11364 fs::remove_dir_all(&store_root).unwrap();
11365 }
11366 let store: Arc<dyn EvolutionStore> =
11367 Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
11368 let evo = build_test_evo_with_store(
11369 "remote-success-restart-source",
11370 "run-remote-restart-source",
11371 command_validator(),
11372 store.clone(),
11373 )
11374 .with_economics(ledger.clone());
11375
11376 let envelope_a = remote_publish_envelope(
11377 "node-a",
11378 "run-remote-a",
11379 "gene-a",
11380 "capsule-a",
11381 "mutation-a",
11382 "shared-signal",
11383 "A.md",
11384 "# from a",
11385 );
11386 let envelope_b = remote_publish_envelope(
11387 "node-b",
11388 "run-remote-b",
11389 "gene-b",
11390 "capsule-b",
11391 "mutation-b",
11392 "shared-signal",
11393 "B.md",
11394 "# from b",
11395 );
11396
11397 evo.import_remote_envelope(&envelope_a).unwrap();
11398 evo.import_remote_envelope(&envelope_b).unwrap();
11399
11400 let recovered = build_test_evo_with_store(
11401 "remote-success-restart-recovered",
11402 "run-remote-restart-recovered",
11403 command_validator(),
11404 store.clone(),
11405 )
11406 .with_economics(ledger.clone());
11407
11408 let decision = recovered
11409 .replay_or_fallback(replay_input("shared-signal"))
11410 .await
11411 .unwrap();
11412
11413 assert!(decision.used_capsule);
11414 assert_eq!(decision.capsule_id, Some("capsule-b".into()));
11415 let locked = ledger.lock().unwrap();
11416 let rewarded = locked
11417 .accounts
11418 .iter()
11419 .find(|item| item.node_id == "node-b")
11420 .unwrap();
11421 assert_eq!(rewarded.balance, recovered.stake_policy.reuse_reward);
11422 }
11423
11424 #[tokio::test]
11425 async fn remote_reuse_failure_penalizes_remote_reputation() {
11426 let ledger = Arc::new(Mutex::new(EvuLedger::default()));
11427 let failing_validator: Arc<dyn Validator> = Arc::new(FixedValidator { success: false });
11428 let (evo, _) = build_test_evo("remote-failure", "run-failure", failing_validator);
11429 let evo = evo.with_economics(ledger.clone());
11430
11431 let envelope = remote_publish_envelope(
11432 "node-remote",
11433 "run-remote-failed",
11434 "gene-remote",
11435 "capsule-remote",
11436 "mutation-remote",
11437 "failure-signal",
11438 "FAILED.md",
11439 "# from remote",
11440 );
11441 evo.import_remote_envelope(&envelope).unwrap();
11442
11443 let decision = evo
11444 .replay_or_fallback(replay_input("failure-signal"))
11445 .await
11446 .unwrap();
11447
11448 assert!(!decision.used_capsule);
11449 assert!(decision.fallback_to_planner);
11450
11451 let signal = evo.economics_signal("node-remote").unwrap();
11452 assert_eq!(signal.available_evu, 0);
11453 assert!(signal.publish_success_rate < 0.5);
11454 assert!(signal.validator_accuracy < 0.5);
11455 }
11456
11457 #[test]
11458 fn ensure_builtin_experience_assets_is_idempotent_and_fetchable() {
11459 let store_root = std::env::temp_dir().join(format!(
11460 "oris-evokernel-builtin-experience-store-{}",
11461 next_id("t")
11462 ));
11463 if store_root.exists() {
11464 fs::remove_dir_all(&store_root).unwrap();
11465 }
11466 let store: Arc<dyn EvolutionStore> =
11467 Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
11468 let node = EvolutionNetworkNode::new(store.clone());
11469
11470 let first = node
11471 .ensure_builtin_experience_assets("runtime-bootstrap")
11472 .unwrap();
11473 assert!(!first.imported_asset_ids.is_empty());
11474
11475 let second = node
11476 .ensure_builtin_experience_assets("runtime-bootstrap")
11477 .unwrap();
11478 assert!(second.imported_asset_ids.is_empty());
11479
11480 let fetch = node
11481 .fetch_assets(
11482 "execution-api",
11483 &FetchQuery {
11484 sender_id: "compat-agent".into(),
11485 signals: vec!["error".into()],
11486 since_cursor: None,
11487 resume_token: None,
11488 },
11489 )
11490 .unwrap();
11491
11492 let mut has_builtin_evomap = false;
11493 for asset in fetch.assets {
11494 if let NetworkAsset::Gene { gene } = asset {
11495 if strategy_metadata_value(&gene.strategy, "asset_origin").as_deref()
11496 == Some("builtin_evomap")
11497 && gene.state == AssetState::Promoted
11498 {
11499 has_builtin_evomap = true;
11500 break;
11501 }
11502 }
11503 }
11504 assert!(has_builtin_evomap);
11505 }
11506
11507 #[test]
11508 fn reported_experience_retention_keeps_latest_three_and_preserves_builtin_assets() {
11509 let store_root = std::env::temp_dir().join(format!(
11510 "oris-evokernel-reported-retention-store-{}",
11511 next_id("t")
11512 ));
11513 if store_root.exists() {
11514 fs::remove_dir_all(&store_root).unwrap();
11515 }
11516 let store: Arc<dyn EvolutionStore> =
11517 Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
11518 let node = EvolutionNetworkNode::new(store.clone());
11519
11520 node.ensure_builtin_experience_assets("runtime-bootstrap")
11521 .unwrap();
11522
11523 for idx in 0..4 {
11524 node.record_reported_experience(
11525 "reporter-a",
11526 format!("reported-docs-rewrite-v{}", idx + 1),
11527 vec!["docs.rewrite".into(), format!("task-{}", idx + 1)],
11528 vec![
11529 "task_class=docs.rewrite".into(),
11530 format!("task_label=Docs rewrite v{}", idx + 1),
11531 format!("summary=reported replay {}", idx + 1),
11532 ],
11533 vec!["a2a.tasks.report".into()],
11534 )
11535 .unwrap();
11536 }
11537
11538 let (_, projection) = store.scan_projection().unwrap();
11539 let reported_promoted = projection
11540 .genes
11541 .iter()
11542 .filter(|gene| {
11543 gene.state == AssetState::Promoted
11544 && strategy_metadata_value(&gene.strategy, "asset_origin").as_deref()
11545 == Some("reported_experience")
11546 && strategy_metadata_value(&gene.strategy, "task_class").as_deref()
11547 == Some("docs.rewrite")
11548 })
11549 .count();
11550 let reported_revoked = projection
11551 .genes
11552 .iter()
11553 .filter(|gene| {
11554 gene.state == AssetState::Revoked
11555 && strategy_metadata_value(&gene.strategy, "asset_origin").as_deref()
11556 == Some("reported_experience")
11557 && strategy_metadata_value(&gene.strategy, "task_class").as_deref()
11558 == Some("docs.rewrite")
11559 })
11560 .count();
11561 let builtin_promoted = projection
11562 .genes
11563 .iter()
11564 .filter(|gene| {
11565 gene.state == AssetState::Promoted
11566 && matches!(
11567 strategy_metadata_value(&gene.strategy, "asset_origin").as_deref(),
11568 Some("builtin") | Some("builtin_evomap")
11569 )
11570 })
11571 .count();
11572
11573 assert_eq!(reported_promoted, 3);
11574 assert_eq!(reported_revoked, 1);
11575 assert!(builtin_promoted >= 1);
11576
11577 let fetch = node
11578 .fetch_assets(
11579 "execution-api",
11580 &FetchQuery {
11581 sender_id: "consumer-b".into(),
11582 signals: vec!["docs.rewrite".into()],
11583 since_cursor: None,
11584 resume_token: None,
11585 },
11586 )
11587 .unwrap();
11588 let docs_genes = fetch
11589 .assets
11590 .into_iter()
11591 .filter_map(|asset| match asset {
11592 NetworkAsset::Gene { gene } => Some(gene),
11593 _ => None,
11594 })
11595 .filter(|gene| {
11596 strategy_metadata_value(&gene.strategy, "task_class").as_deref()
11597 == Some("docs.rewrite")
11598 })
11599 .collect::<Vec<_>>();
11600 assert!(docs_genes.len() >= 3);
11601 }
11602
11603 #[test]
11606 fn cargo_dep_upgrade_single_manifest_accepted() {
11607 let files = vec!["Cargo.toml".to_string()];
11608 let result = validate_bounded_cargo_dep_files(&files);
11609 assert!(result.is_ok());
11610 assert_eq!(result.unwrap(), vec!["Cargo.toml"]);
11611 }
11612
11613 #[test]
11614 fn cargo_dep_upgrade_nested_manifest_accepted() {
11615 let files = vec!["crates/oris-runtime/Cargo.toml".to_string()];
11616 let result = validate_bounded_cargo_dep_files(&files);
11617 assert!(result.is_ok());
11618 }
11619
11620 #[test]
11621 fn cargo_dep_upgrade_lock_file_accepted() {
11622 let files = vec!["Cargo.lock".to_string()];
11623 let result = validate_bounded_cargo_dep_files(&files);
11624 assert!(result.is_ok());
11625 }
11626
11627 #[test]
11628 fn cargo_dep_upgrade_too_many_files_rejected_fail_closed() {
11629 let files: Vec<String> = (0..6)
11630 .map(|i| format!("crates/crate{i}/Cargo.toml"))
11631 .collect();
11632 let result = validate_bounded_cargo_dep_files(&files);
11633 assert!(
11634 result.is_err(),
11635 "more than 5 manifests should be rejected fail-closed"
11636 );
11637 assert_eq!(
11638 result.unwrap_err(),
11639 MutationProposalContractReasonCode::UnsupportedTaskClass
11640 );
11641 }
11642
11643 #[test]
11644 fn cargo_dep_upgrade_rs_source_file_rejected_fail_closed() {
11645 let files = vec!["crates/oris-runtime/src/lib.rs".to_string()];
11646 let result = validate_bounded_cargo_dep_files(&files);
11647 assert!(
11648 result.is_err(),
11649 ".rs files must be rejected from dep-upgrade scope"
11650 );
11651 assert_eq!(
11652 result.unwrap_err(),
11653 MutationProposalContractReasonCode::OutOfBoundsPath
11654 );
11655 }
11656
11657 #[test]
11658 fn cargo_dep_upgrade_path_traversal_rejected_fail_closed() {
11659 let files = vec!["../outside/Cargo.toml".to_string()];
11660 let result = validate_bounded_cargo_dep_files(&files);
11661 assert!(
11662 result.is_err(),
11663 "path traversal must be rejected fail-closed"
11664 );
11665 assert_eq!(
11666 result.unwrap_err(),
11667 MutationProposalContractReasonCode::OutOfBoundsPath
11668 );
11669 }
11670
11671 #[test]
11672 fn lint_fix_src_rs_file_accepted() {
11673 let files = vec!["src/lib.rs".to_string()];
11674 let result = validate_bounded_lint_files(&files);
11675 assert!(result.is_ok());
11676 assert_eq!(result.unwrap(), vec!["src/lib.rs"]);
11677 }
11678
11679 #[test]
11680 fn lint_fix_crates_rs_file_accepted() {
11681 let files = vec!["crates/oris-runtime/src/agent.rs".to_string()];
11682 let result = validate_bounded_lint_files(&files);
11683 assert!(result.is_ok());
11684 }
11685
11686 #[test]
11687 fn lint_fix_examples_rs_file_accepted() {
11688 let files = vec!["examples/evo_oris_repo/src/main.rs".to_string()];
11689 let result = validate_bounded_lint_files(&files);
11690 assert!(result.is_ok());
11691 }
11692
11693 #[test]
11694 fn lint_fix_too_many_files_rejected_fail_closed() {
11695 let files: Vec<String> = (0..6).map(|i| format!("src/module{i}.rs")).collect();
11696 let result = validate_bounded_lint_files(&files);
11697 assert!(
11698 result.is_err(),
11699 "more than 5 source files should be rejected fail-closed"
11700 );
11701 assert_eq!(
11702 result.unwrap_err(),
11703 MutationProposalContractReasonCode::UnsupportedTaskClass
11704 );
11705 }
11706
11707 #[test]
11708 fn lint_fix_non_rs_extension_rejected_fail_closed() {
11709 let files = vec!["src/config.toml".to_string()];
11710 let result = validate_bounded_lint_files(&files);
11711 assert!(
11712 result.is_err(),
11713 "non-.rs files must be rejected from lint-fix scope"
11714 );
11715 assert_eq!(
11716 result.unwrap_err(),
11717 MutationProposalContractReasonCode::OutOfBoundsPath
11718 );
11719 }
11720
11721 #[test]
11722 fn lint_fix_out_of_allowed_prefix_rejected_fail_closed() {
11723 let files = vec!["scripts/helper.rs".to_string()];
11724 let result = validate_bounded_lint_files(&files);
11725 assert!(
11726 result.is_err(),
11727 "rs files outside allowed prefixes must be rejected fail-closed"
11728 );
11729 assert_eq!(
11730 result.unwrap_err(),
11731 MutationProposalContractReasonCode::OutOfBoundsPath
11732 );
11733 }
11734
11735 #[test]
11736 fn lint_fix_path_traversal_rejected_fail_closed() {
11737 let files = vec!["../../outside/src/lib.rs".to_string()];
11738 let result = validate_bounded_lint_files(&files);
11739 assert!(
11740 result.is_err(),
11741 "path traversal must be rejected fail-closed"
11742 );
11743 assert_eq!(
11744 result.unwrap_err(),
11745 MutationProposalContractReasonCode::OutOfBoundsPath
11746 );
11747 }
11748
11749 #[test]
11750 fn proposal_scope_classifies_cargo_dep_upgrade() {
11751 use oris_agent_contract::{
11752 AgentTask, BoundedTaskClass, HumanApproval, MutationProposal, SupervisedDevloopRequest,
11753 };
11754 let request = SupervisedDevloopRequest {
11755 task: AgentTask {
11756 id: "t-dep".into(),
11757 description: "bump serde".into(),
11758 },
11759 proposal: MutationProposal {
11760 intent: "bump serde to 1.0.200".into(),
11761 expected_effect: "version field updated".into(),
11762 files: vec!["Cargo.toml".to_string()],
11763 },
11764 approval: HumanApproval {
11765 approved: true,
11766 approver: Some("alice".into()),
11767 note: None,
11768 },
11769 };
11770 let scope_result = supervised_devloop_mutation_proposal_scope(&request);
11771 assert!(scope_result.is_ok());
11772 assert_eq!(
11773 scope_result.unwrap().task_class,
11774 BoundedTaskClass::CargoDepUpgrade
11775 );
11776 }
11777
11778 #[test]
11779 fn proposal_scope_classifies_lint_fix() {
11780 use oris_agent_contract::{
11781 AgentTask, BoundedTaskClass, HumanApproval, MutationProposal, SupervisedDevloopRequest,
11782 };
11783 let request = SupervisedDevloopRequest {
11784 task: AgentTask {
11785 id: "t-lint".into(),
11786 description: "cargo fmt fix".into(),
11787 },
11788 proposal: MutationProposal {
11789 intent: "apply cargo fmt to src/lib.rs".into(),
11790 expected_effect: "formatting normalized".into(),
11791 files: vec!["src/lib.rs".to_string()],
11792 },
11793 approval: HumanApproval {
11794 approved: true,
11795 approver: Some("alice".into()),
11796 note: None,
11797 },
11798 };
11799 let scope_result = supervised_devloop_mutation_proposal_scope(&request);
11800 assert!(scope_result.is_ok());
11801 assert_eq!(scope_result.unwrap().task_class, BoundedTaskClass::LintFix);
11802 }
11803}