1use crate::act::ActionFamily;
7use crate::observe::{ObservationPaths, ObservationStatus};
8use crate::types::{
9 DecisionAudit, ExecutionLineageReceipt, ExportActionTraceV1, RepairRecordV1,
10 StopRuleEvaluation, TargetNormalization, VerificationPlanArtifact,
11};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use serde_json::json;
15use stack_ids::{AttemptId, TraceCtx};
16use verification_adjudication::AdjudicationResult;
17use verification_calibration::CalibrationSnapshot;
18use verification_control::{
19 BoundaryArtifactKind, BoundaryRepairRecord, CheckPlan, ControlReceipt, SchedulerDecision,
20 VerificationAttempt, VerificationCase,
21};
22use verification_policy::{ApprovalRecord, PolicyDecision};
23
24pub const PILOT_LOOP_RECEIPT_V1_SCHEMA: &str = "forge_pilot_loop_receipt_v1";
26pub const LOOP_ITERATION_REPORT_V1_SCHEMA: &str = "forge_pilot_loop_iteration_report_v1";
28pub const LOOP_REPORT_V1_SCHEMA: &str = "forge_pilot_loop_report_v1";
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "snake_case")]
34pub enum HaltReason {
35 MaxIterationsReached,
36 TimeBudgetExhausted,
37 NothingWorthInvestigating,
38 ImportRequired,
39 NamespaceMismatch,
40 StorageUnavailable,
41 StorageCorrupt,
42 AllTargetsExhausted,
43 MissingKernelPayload,
44 ThinExportBlockedExecution,
45 ExternalHalt,
46 RetryCapExceeded,
47 AdvisoryOnlyFallback,
48 #[cfg(feature = "governance")]
49 GovernanceIncidentHalt,
50 #[cfg(feature = "governance")]
51 GovernanceAuthorityInsufficient,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
56pub struct LoopBudgetReceipt {
57 pub workload_class: String,
58 pub time_budget_secs: u64,
59 pub max_iterations: u32,
60 pub cooldown_secs: u64,
61 pub max_retries_per_target: u32,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
66pub struct LoopReceipt {
67 pub schema_version: String,
68 pub receipt_id: String,
69 pub trace_ctx: TraceCtx,
70 pub attempt_id: AttemptId,
71 pub namespace: String,
72 pub workspace_path: String,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub observation_paths: Option<ObservationPaths>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub observation_status: Option<ObservationStatus>,
77 pub non_authoritative: bool,
78 pub advisory_only: bool,
79 #[serde(default)]
80 pub consumer_only_truth_access: bool,
81 pub budget: LoopBudgetReceipt,
82 pub halt_reason: HaltReason,
83 pub iterations_completed: u32,
84 pub actions_executed: u32,
85 pub exports_completed: u32,
86 pub imports_completed: u32,
87 pub degraded_iterations: u32,
88 pub targets_investigated: Vec<String>,
89 pub started_at: String,
90 pub finished_at: String,
91 pub elapsed_seconds: f64,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
99pub struct LoopIterationReport {
100 pub schema_version: String,
101 pub iteration: u32,
102 pub selected_target_key: Option<String>,
103 pub action_family: Option<ActionFamily>,
104 pub export_completed: bool,
105 pub import_completed: bool,
106 pub degraded: bool,
107 pub advisory_only: bool,
108 pub outcome_signature: Option<String>,
109 pub verification_case: Option<VerificationCase>,
110 pub check_plan: Option<CheckPlan>,
111 pub verification_attempt: Option<VerificationAttempt>,
112 pub scheduler_decision: Option<SchedulerDecision>,
113 pub policy_decision: Option<PolicyDecision>,
114 pub approval_records: Vec<ApprovalRecord>,
115 pub calibration_snapshot: Option<CalibrationSnapshot>,
116 pub adjudication: Option<AdjudicationResult>,
117 pub control_receipt: Option<ControlReceipt>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub target_normalization: Option<TargetNormalization>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub decision_audit: Option<DecisionAudit>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub lineage_receipt: Option<ExecutionLineageReceipt>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub export_trace: Option<ExportActionTraceV1>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub stop_rule_evaluation: Option<StopRuleEvaluation>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub verification_plan_artifact: Option<VerificationPlanArtifact>,
130 #[serde(default)]
131 pub boundary_repairs: Vec<BoundaryRepairRecord>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub repair_records: Vec<RepairRecordV1>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub rollback_invalidation_count: Option<usize>,
136 #[cfg(feature = "governance")]
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub governance_receipt: Option<crate::governance_gate::GovernanceReceiptV1>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
146pub struct LoopReport {
147 pub schema_version: String,
148 pub receipt: LoopReceipt,
149 pub iterations_completed: u32,
150 pub actions_executed: u32,
151 pub exports_completed: u32,
152 pub imports_completed: u32,
153 pub halt_reason: HaltReason,
154 pub targets_investigated: Vec<String>,
155 pub degraded_iterations: u32,
156 pub elapsed_seconds: f64,
157 pub advisory_only_steps: bool,
158 pub iterations: Vec<LoopIterationReport>,
159}
160
161pub(crate) fn apply_loop_report_boundary_repairs(report: &mut LoopReport) {
162 for iteration in &mut report.iterations {
163 if iteration.boundary_repairs.is_empty() && iteration.approval_records.is_empty() {
164 iteration.boundary_repairs.push(BoundaryRepairRecord::new(
165 BoundaryArtifactKind::LoopIterationReport,
166 LOOP_ITERATION_REPORT_V1_SCHEMA,
167 "default_empty_array",
168 "$.approval_records",
169 None,
170 json!([]),
171 "approval records default to an empty array at the loop report boundary",
172 ));
173 }
174 }
175}
176
177pub fn parse_loop_report_boundary(mut value: serde_json::Value) -> Result<LoopReport, String> {
179 let iterations = value
180 .get_mut("iterations")
181 .and_then(|iterations| iterations.as_array_mut())
182 .ok_or_else(|| {
183 "loop report boundary validation failed: missing iterations array".to_string()
184 })?;
185
186 for iteration in iterations {
187 let Some(iteration_object) = iteration.as_object_mut() else {
188 return Err(
189 "loop report boundary validation failed: iteration entry is not an object".into(),
190 );
191 };
192 if !iteration_object.contains_key("approval_records") {
193 iteration_object.insert("approval_records".into(), json!([]));
194 }
195 if !iteration_object.contains_key("boundary_repairs") {
196 iteration_object.insert("boundary_repairs".into(), json!([]));
197 }
198 }
199
200 let mut report: LoopReport = serde_json::from_value(value)
201 .map_err(|error| format!("loop report boundary validation failed: {error}"))?;
202 apply_loop_report_boundary_repairs(&mut report);
203 Ok(report)
204}