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