1use crate::bundle_builder::{
8 build_bundle_from_oracle, build_bundle_from_patch, OracleBundleInput, PatchBundleInput,
9};
10use crate::config::LoopConfig;
11use crate::error::PilotError;
12use crate::observe::Observation;
13use forge_engine::lab::evidence::{
14 RefutationArtifact, RefutationArtifactOutcome, RefutationArtifactType,
15};
16use forge_engine::{
17 select_backend, CargoAdapter, ExperimentConfig, ExperimentEvidenceBundle,
18 PairedExperimentRunner, ProjectAdapter, StructuredPatch,
19};
20use kernel_oracles::{
21 evaluate_causal_refuter, evaluate_conservative, evaluate_delta_parity, evaluate_exact_bounded,
22 evaluate_minimal_perturbation, evaluate_temporal_replay, DeltaParityAssessment,
23 OracleAssessment, OracleRefutationOutcome, OracleRefutationResult, TemporalReplayAssessment,
24};
25use schemars::JsonSchema;
26use serde::{Deserialize, Serialize};
27use stack_ids::{ClaimVersionId, OracleSliceId};
28use std::path::Path;
29use verification_policy::ExecutionPermit;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct AdvisoryPlan {
34 pub description: String,
35}
36
37#[derive(Debug, Clone)]
42pub enum PlanKind {
43 OracleExactBounded {
44 oracle_slice_id: OracleSliceId,
45 },
46 OracleConservative,
47 OracleDeltaParity {
48 changed_node_ids: Vec<String>,
49 max_iterations: u32,
50 },
51 OracleTemporalReplay {
52 cutoff_recorded_at: String,
53 },
54 OracleCausalRefuter {
55 target_node_id: String,
56 max_removed_nodes: usize,
57 },
58 OracleMinimalPerturbation {
59 target_node_id: String,
60 max_removed_nodes: usize,
61 },
62 PairedPatch {
63 fixture_path: String,
64 patch: StructuredPatch,
65 experiment_config: ExperimentConfig,
66 description: String,
67 },
68 AdvisoryOnlyVerificationPlan(AdvisoryPlan),
69}
70
71impl PlanKind {
72 pub fn supersedes_claim_version_id(&self) -> Option<ClaimVersionId> {
74 None
75 }
76
77 pub fn check_names(&self) -> Vec<String> {
79 match self {
80 Self::PairedPatch { .. } => vec!["fmt".into(), "clippy".into(), "test".into()],
81 _ => vec!["kernel_oracle".into()],
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
88#[serde(rename_all = "snake_case")]
89pub enum ActionFamily {
90 Oracle,
91 PairedPatch,
92 AdvisoryOnly,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct OracleExecution {
98 pub assessment: Option<OracleAssessment>,
99 pub delta_parity: Option<DeltaParityAssessment>,
100 pub temporal_replay: Option<TemporalReplayAssessment>,
101 pub refutation: Option<OracleRefutationResult>,
102}
103
104impl OracleExecution {
105 pub fn outcome_summary(&self) -> Option<String> {
107 if let Some(assessment) = &self.assessment {
108 return Some(format!(
109 "oracle mode {:?} supported={} satisfied_constraints={}",
110 assessment.mode, assessment.supported, assessment.satisfied_constraint_count
111 ));
112 }
113 if let Some(delta) = &self.delta_parity {
114 return Some(format!(
115 "delta parity matched={} recomputed_nodes={}",
116 delta.parity_match,
117 delta.recomputed_node_ids.len()
118 ));
119 }
120 if let Some(replay) = &self.temporal_replay {
121 return Some(format!(
122 "temporal replay matched_expected_hash={}",
123 replay.matched_expected_hash
124 ));
125 }
126 self.refutation
127 .as_ref()
128 .map(|refutation| format!("{:?}", refutation.outcome))
129 }
130
131 pub fn summary_json(&self) -> serde_json::Value {
133 serde_json::json!({
134 "assessment": self.assessment,
135 "delta_parity": self.delta_parity,
136 "temporal_replay": self.temporal_replay,
137 "refutation": self.refutation,
138 })
139 }
140
141 pub fn refutation_artifacts(&self) -> Vec<RefutationArtifact> {
143 self.refutation
144 .iter()
145 .map(|refutation| RefutationArtifact {
146 artifact_id: format!("refutation:{}", refutation.target_node_id),
147 artifact_type: RefutationArtifactType::SubsampleStability,
148 trial_id: None,
149 attempt_id: None,
150 outcome: match &refutation.outcome {
151 OracleRefutationOutcome::FlipWitness { .. } => {
152 RefutationArtifactOutcome::Passed
153 }
154 OracleRefutationOutcome::NoFlipFound { searched_budget } => {
155 RefutationArtifactOutcome::Failed {
156 reason: format!("no flip found in budget {searched_budget}"),
157 }
158 }
159 OracleRefutationOutcome::NotApplicable { reason } => {
160 RefutationArtifactOutcome::Inconclusive {
161 reason: reason.clone(),
162 }
163 }
164 },
165 estimate_delta: None,
166 details: Some(format!("{:?}", refutation.outcome)),
167 })
168 .collect()
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct PatchExecution {
175 pub run_id: String,
176 pub improvements: u32,
177 pub regressions: u32,
178}
179
180#[derive(Debug, Clone)]
182pub struct ActionOutcome {
183 pub family: ActionFamily,
184 pub plan: PlanKind,
185 pub bundle: Option<ExperimentEvidenceBundle>,
186 pub oracle_execution: Option<OracleExecution>,
187 pub patch_execution: Option<PatchExecution>,
188 pub advisory_only: bool,
189 pub outcome_signature: String,
190}
191
192pub async fn execute_plan(
198 observation: &Observation,
199 target_key: &str,
200 plan: &PlanKind,
201 permit: &ExecutionPermit,
202 config: &LoopConfig,
203) -> Result<ActionOutcome, PilotError> {
204 if permit.scope().target_key() != target_key {
205 return Err(PilotError::Other(format!(
206 "execution permit target {} does not match requested target {}",
207 permit.scope().target_key(),
208 target_key
209 )));
210 }
211 match plan {
212 PlanKind::OracleExactBounded { .. }
213 | PlanKind::OracleConservative
214 | PlanKind::OracleDeltaParity { .. }
215 | PlanKind::OracleTemporalReplay { .. }
216 | PlanKind::OracleCausalRefuter { .. }
217 | PlanKind::OracleMinimalPerturbation { .. } => {
218 execute_oracle_plan(observation, target_key, plan, config).await
219 }
220 PlanKind::PairedPatch {
221 fixture_path,
222 patch,
223 experiment_config,
224 ..
225 } => {
226 execute_patch_plan(
227 observation,
228 target_key,
229 plan,
230 fixture_path,
231 patch,
232 experiment_config,
233 config,
234 )
235 .await
236 }
237 PlanKind::AdvisoryOnlyVerificationPlan(_) => Err(PilotError::Other(
238 "advisory-only plans cannot consume execution permits".into(),
239 )),
240 }
241}
242
243async fn execute_oracle_plan(
244 observation: &Observation,
245 target_key: &str,
246 plan: &PlanKind,
247 _config: &LoopConfig,
248) -> Result<ActionOutcome, PilotError> {
249 let compiled = observation
250 .compiled
251 .as_ref()
252 .ok_or(PilotError::MissingCompiledContext)?;
253
254 let oracle_execution = match plan {
255 PlanKind::OracleExactBounded { .. } => OracleExecution {
256 assessment: evaluate_exact_bounded(compiled),
257 delta_parity: None,
258 temporal_replay: None,
259 refutation: None,
260 },
261 PlanKind::OracleConservative => OracleExecution {
262 assessment: Some(evaluate_conservative(compiled)),
263 delta_parity: None,
264 temporal_replay: None,
265 refutation: None,
266 },
267 PlanKind::OracleDeltaParity {
268 changed_node_ids,
269 max_iterations,
270 } => OracleExecution {
271 assessment: None,
272 delta_parity: Some(evaluate_delta_parity(
273 compiled,
274 changed_node_ids,
275 *max_iterations,
276 )),
277 temporal_replay: None,
278 refutation: None,
279 },
280 PlanKind::OracleTemporalReplay { cutoff_recorded_at } => OracleExecution {
281 assessment: None,
282 delta_parity: None,
283 temporal_replay: Some(
284 evaluate_temporal_replay(
285 &observation.temporal_snapshots,
286 cutoff_recorded_at,
287 &constraint_compiler::CompilerPolicy {
288 policy_version: "forge-pilot.v1".into(),
289 include_hyperedges: true,
290 },
291 &compiled.graph_hash,
292 )
293 .ok_or(PilotError::MissingTemporalSnapshots)?,
294 ),
295 refutation: None,
296 },
297 PlanKind::OracleCausalRefuter {
298 target_node_id,
299 max_removed_nodes,
300 } => OracleExecution {
301 assessment: None,
302 delta_parity: None,
303 temporal_replay: None,
304 refutation: Some(evaluate_causal_refuter(
305 compiled,
306 target_node_id,
307 *max_removed_nodes,
308 )),
309 },
310 PlanKind::OracleMinimalPerturbation {
311 target_node_id,
312 max_removed_nodes,
313 } => OracleExecution {
314 assessment: None,
315 delta_parity: None,
316 temporal_replay: None,
317 refutation: Some(evaluate_minimal_perturbation(
318 compiled,
319 target_node_id,
320 *max_removed_nodes,
321 )),
322 },
323 _ => {
325 return Err(PilotError::Other(format!(
326 "unsupported plan kind for oracle execution: {:?}",
327 plan
328 )))
329 }
330 };
331
332 let bundle = build_bundle_from_oracle(OracleBundleInput {
333 plan,
334 target_key,
335 trace_id: observation
336 .batch
337 .as_ref()
338 .and_then(|batch| batch.trace_ctx.as_ref().map(|ctx| ctx.trace_id.clone())),
339 scope_namespace: &observation.scope_key.namespace,
340 oracle_execution: &oracle_execution,
341 known_threats: observation
342 .degradations
343 .iter()
344 .map(|degradation| degradation.kind.clone())
345 .collect(),
346 });
347 let outcome_signature = oracle_execution
348 .outcome_summary()
349 .unwrap_or_else(|| "oracle".into());
350
351 Ok(ActionOutcome {
352 family: ActionFamily::Oracle,
353 plan: plan.clone(),
354 bundle: Some(bundle),
355 oracle_execution: Some(oracle_execution),
356 patch_execution: None,
357 advisory_only: false,
358 outcome_signature,
359 })
360}
361
362async fn execute_patch_plan(
363 observation: &Observation,
364 target_key: &str,
365 plan: &PlanKind,
366 fixture_path: &str,
367 patch: &StructuredPatch,
368 experiment_config: &ExperimentConfig,
369 config: &LoopConfig,
370) -> Result<ActionOutcome, PilotError> {
371 let backend = select_backend(&config.forge_config)?;
372 let fixture = Path::new(fixture_path);
373 if !CargoAdapter::detect(fixture) {
374 return Err(PilotError::UnsupportedPatchFixture {
375 fixture_path: fixture_path.to_string(),
376 });
377 }
378 let adapter = CargoAdapter;
379 let runner = PairedExperimentRunner::new(backend.as_ref(), &adapter, &config.forge_config);
380 let experiment_result = runner.run(fixture, patch, experiment_config).await?;
381 let bundle = build_bundle_from_patch(PatchBundleInput {
382 plan,
383 target_key,
384 trace_id: observation
385 .batch
386 .as_ref()
387 .and_then(|batch| batch.trace_ctx.as_ref().map(|ctx| ctx.trace_id.clone())),
388 scope_namespace: &observation.scope_key.namespace,
389 experiment_result: &experiment_result,
390 known_threats: observation
391 .degradations
392 .iter()
393 .map(|degradation| degradation.kind.clone())
394 .collect(),
395 });
396
397 Ok(ActionOutcome {
398 family: ActionFamily::PairedPatch,
399 plan: plan.clone(),
400 bundle: Some(bundle),
401 oracle_execution: None,
402 patch_execution: Some(PatchExecution {
403 run_id: experiment_result.run_id.clone(),
404 improvements: experiment_result.diff.improvements,
405 regressions: experiment_result.diff.regressions,
406 }),
407 advisory_only: false,
408 outcome_signature: format!(
409 "patch:improvements={} regressions={}",
410 experiment_result.diff.improvements, experiment_result.diff.regressions
411 ),
412 })
413}