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