Skip to main content

verification_control/
lib.rs

1//! Verification control-plane artifact types and scheduling logic.
2//!
3//! This crate defines the canonical schema types for verification cases,
4//! check plans, attempts, control receipts, ledger entries, and governance
5//! review artifacts (effect, delegation, release-gate, continuity). It also
6//! provides scheduling, promotion-eligibility, and ledger-replay functions
7//! consumed by `forge-pilot` and the verification pipeline crates.
8#![cfg_attr(test, allow(clippy::expect_used))]
9
10use llm_tool_runtime::{ToolExposureDecision, ToolReceipt, ToolRetryOwner};
11use schemars::JsonSchema;
12use semantic_memory_forge::{
13    DegradationKindV1, EvidenceAdmissibilityV1, ExactnessBudgetV1, ExactnessEscalationRuleV1,
14    ExactnessLevelV1, ForgeToolReceiptV2,
15};
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18use stack_ids::{
19    ApplicabilityContextId, ArtifactTransportId, AssuranceCaseId, AttemptId, AuthorityChainId,
20    BoundaryRepairRecordId, CheckPlanId, ClaimVersionId, CompiledObligationSetId,
21    CompositionConflictSetId, CompositionReceiptId, ContentDigest, ContinuityExceptionId,
22    ContinuityReviewCaseId, ControlReceiptId, DelegationReviewCaseId, DeploymentProfileId,
23    EffectBlockReceiptId, EffectCommitDecisionId, EffectIntentId, EffectPreflightReportId,
24    EffectReviewCaseId, EffectiveConstitutionId, IncidentCaseId, LedgerEntryId,
25    ProfileExceptionBundleId, ProfileSetId, RecoveryReplaySliceId, RegionDigestId, RegionId,
26    ReleaseGateCaseId, ReleaseReadinessDecisionId, RepairCandidateId, RepairRouteId, ScopeKey,
27    SeparationOfDutiesPolicyId, SyndromeId, TraceCtx, TrialId, VerificationCaseId,
28};
29
30pub mod v14;
31pub use v14::{
32    ComparabilityMatrixV1, DecisionTraceV1, DisputeBundleV1, ExperimentCaseV1, RefuterResultV1,
33    COMPARABILITY_MATRIX_V1_SCHEMA, DECISION_TRACE_V1_SCHEMA, DISPUTE_BUNDLE_V1_SCHEMA,
34    EXPERIMENT_CASE_V1_SCHEMA, REFUTER_RESULT_V1_SCHEMA,
35};
36
37pub const VERIFICATION_CASE_V1_SCHEMA: &str = "verification_case_v1";
38pub const CHECK_PLAN_V1_SCHEMA: &str = "check_plan_v1";
39pub const DISPATCH_DECISION_V1_SCHEMA: &str = "dispatch_decision_v1";
40pub const TOOL_EXPOSURE_V1_SCHEMA: &str = "tool_exposure_v1";
41pub const RETRY_DECISION_V1_SCHEMA: &str = "retry_decision_v1";
42pub const BUDGET_DEBIT_V1_SCHEMA: &str = "budget_debit_v1";
43pub const ROLLBACK_DECISION_V1_SCHEMA: &str = "rollback_decision_v1";
44pub const VERIFICATION_ATTEMPT_V1_SCHEMA: &str = "verification_attempt_v1";
45pub const CONTROL_RECEIPT_V1_SCHEMA: &str = "control_receipt_v1";
46pub const LEDGER_ENTRY_V1_SCHEMA: &str = "ledger_entry_v1";
47pub const SCHEDULER_DECISION_V1_SCHEMA: &str = "scheduler_decision_v1";
48pub const BOUNDARY_REPAIR_RECORD_V1_SCHEMA: &str = "boundary_repair_record_v1";
49pub const REGION_ARTIFACT_TRANSPORT_V1_SCHEMA: &str = "region_artifact_transport_v1";
50pub const SYNDROME_ROUTE_RECORD_V1_SCHEMA: &str = "syndrome_route_record_v1";
51pub const EFFECT_REVIEW_CASE_V1_SCHEMA: &str = "effect_review_case_v1";
52pub const EFFECT_BLOCK_RECEIPT_V1_SCHEMA: &str = "effect_block_receipt_v1";
53pub const DELEGATION_REVIEW_CASE_V1_SCHEMA: &str = "delegation_review_case_v1";
54pub const RELEASE_GATE_CASE_V1_SCHEMA: &str = "release_gate_case_v1";
55pub const CONTINUITY_REVIEW_CASE_V1_SCHEMA: &str = "continuity_review_case_v1";
56pub const CONTROL_CASE_V1_SCHEMA: &str = VERIFICATION_CASE_V1_SCHEMA;
57pub const EXECUTION_PLAN_V1_SCHEMA: &str = CHECK_PLAN_V1_SCHEMA;
58pub const REPAIR_RECORD_V1_SCHEMA: &str = BOUNDARY_REPAIR_RECORD_V1_SCHEMA;
59pub const ROLLBACK_RECORD_V1_SCHEMA: &str = ROLLBACK_DECISION_V1_SCHEMA;
60
61pub use BoundaryRepairRecord as RepairRecord;
62pub use CheckPlan as ExecutionPlan;
63pub use VerificationCase as ControlCase;
64
65/// Constitutional citation context attached to governance review artifacts.
66///
67/// Tracks which applicability, profile, composition, and obligation artifacts
68/// were consulted when producing a review decision.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
70pub struct V25CitationContext {
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub applicability_context_id: Option<ApplicabilityContextId>,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub profile_set_id: Option<ProfileSetId>,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub composition_receipt_id: Option<CompositionReceiptId>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub effective_constitution_id: Option<EffectiveConstitutionId>,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub compiled_obligation_set_id: Option<CompiledObligationSetId>,
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub composition_conflict_set_id: Option<CompositionConflictSetId>,
83    #[serde(default)]
84    pub profile_exception_bundle_ids: Vec<ProfileExceptionBundleId>,
85}
86
87/// Completeness status of the constitutional context payload on a review artifact.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
89#[serde(rename_all = "snake_case")]
90pub enum ConstitutionalContextStatus {
91    Missing,
92    Partial,
93    Complete,
94}
95
96impl ConstitutionalContextStatus {
97    /// Returns true only when the full constitutional context is present.
98    pub fn is_complete(self) -> bool {
99        matches!(self, Self::Complete)
100    }
101}
102
103impl V25CitationContext {
104    /// Constructs an explicit missing constitutional citation payload.
105    pub fn missing() -> Self {
106        Self {
107            applicability_context_id: None,
108            profile_set_id: None,
109            composition_receipt_id: None,
110            effective_constitution_id: None,
111            compiled_obligation_set_id: None,
112            composition_conflict_set_id: None,
113            profile_exception_bundle_ids: Vec::new(),
114        }
115    }
116
117    /// Classifies how much of the required constitutional citation payload is present.
118    pub fn context_status(&self) -> ConstitutionalContextStatus {
119        let required_present = [
120            self.applicability_context_id.is_some(),
121            self.profile_set_id.is_some(),
122            self.composition_receipt_id.is_some(),
123            self.effective_constitution_id.is_some(),
124            self.compiled_obligation_set_id.is_some(),
125        ];
126        let present = required_present
127            .into_iter()
128            .filter(|present| *present)
129            .count();
130        if present == 0 {
131            ConstitutionalContextStatus::Missing
132        } else if present == required_present.len() {
133            ConstitutionalContextStatus::Complete
134        } else {
135            ConstitutionalContextStatus::Partial
136        }
137    }
138}
139
140/// Obligation references attached to a governance review artifact.
141///
142/// Captures required, blocking, and monitoring obligation refs from the
143/// compiled obligation set that applied at review time.
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
145pub struct V25ControlObligationRefs {
146    #[serde(default)]
147    pub required_obligation_refs: Vec<String>,
148    #[serde(default)]
149    pub blocking_obligation_refs: Vec<String>,
150    #[serde(default)]
151    pub monitoring_obligation_refs: Vec<String>,
152}
153
154impl V25ControlObligationRefs {
155    /// Constructs an explicit missing obligation-reference payload.
156    pub fn missing() -> Self {
157        Self {
158            required_obligation_refs: Vec::new(),
159            blocking_obligation_refs: Vec::new(),
160            monitoring_obligation_refs: Vec::new(),
161        }
162    }
163
164    /// Classifies whether obligation references are absent or present on the artifact.
165    pub fn context_status(&self) -> ConstitutionalContextStatus {
166        if self.required_obligation_refs.is_empty()
167            && self.blocking_obligation_refs.is_empty()
168            && self.monitoring_obligation_refs.is_empty()
169        {
170            ConstitutionalContextStatus::Missing
171        } else {
172            ConstitutionalContextStatus::Complete
173        }
174    }
175}
176
177/// A review case for an effect-system action (intent, preflight, commit decision).
178///
179/// Gates side-effecting actions through constitutional citation and obligation checks.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
181pub struct EffectReviewCaseV1 {
182    pub schema_version: String,
183    pub effect_review_case_id: EffectReviewCaseId,
184    pub effect_intent_id: EffectIntentId,
185    pub effect_preflight_report_id: EffectPreflightReportId,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub effect_commit_decision_id: Option<EffectCommitDecisionId>,
188    #[serde(flatten)]
189    pub citation: V25CitationContext,
190    #[serde(flatten)]
191    pub obligation_refs: V25ControlObligationRefs,
192    pub citation_status: ConstitutionalContextStatus,
193    pub obligation_refs_status: ConstitutionalContextStatus,
194    #[serde(default)]
195    pub required_policy_refs: Vec<String>,
196    pub decision_basis: String,
197    pub final_state: EffectReviewFinalStateV1,
198    pub advisory_only: bool,
199    pub generated_at: String,
200}
201
202/// Terminal state of an effect review case.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum EffectReviewFinalStateV1 {
206    Approved,
207    Authorized,
208    AdvisoryOnly,
209    Denied,
210    Blocked,
211}
212
213/// Receipt emitted when an effect action is blocked by policy or budget constraints.
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
215pub struct EffectBlockReceiptV1 {
216    pub schema_version: String,
217    pub effect_block_receipt_id: EffectBlockReceiptId,
218    pub effect_review_case_id: EffectReviewCaseId,
219    #[serde(flatten)]
220    pub citation: V25CitationContext,
221    #[serde(flatten)]
222    pub obligation_refs: V25ControlObligationRefs,
223    pub citation_status: ConstitutionalContextStatus,
224    pub obligation_refs_status: ConstitutionalContextStatus,
225    pub block_reason: EffectBlockReasonV1,
226    pub generated_at: String,
227}
228
229/// Reason an effect action was blocked.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231#[serde(rename_all = "snake_case")]
232pub enum EffectBlockReasonV1 {
233    PolicyRequiredObligationsOutstanding,
234    #[serde(alias = "budget exhausted")]
235    BudgetExhausted,
236}
237
238/// A review case for authority delegation decisions.
239///
240/// Validates that delegation chains and separation-of-duties policies
241/// are satisfied before approving delegated authority.
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
243pub struct DelegationReviewCaseV1 {
244    pub schema_version: String,
245    pub delegation_review_case_id: DelegationReviewCaseId,
246    pub authority_chain_id: AuthorityChainId,
247    pub separation_of_duties_policy_id: SeparationOfDutiesPolicyId,
248    #[serde(flatten)]
249    pub citation: V25CitationContext,
250    #[serde(flatten)]
251    pub obligation_refs: V25ControlObligationRefs,
252    pub citation_status: ConstitutionalContextStatus,
253    pub obligation_refs_status: ConstitutionalContextStatus,
254    pub decision_state: DelegationReviewDecisionStateV1,
255    pub generated_at: String,
256}
257
258/// Terminal decision state of a delegation review case.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
260#[serde(rename_all = "snake_case")]
261pub enum DelegationReviewDecisionStateV1 {
262    Approved,
263    Valid,
264    Denied,
265    Blocked,
266}
267
268/// A review case for release-gate decisions.
269///
270/// Evaluates deployment profile, assurance case, and readiness decision
271/// against constitutional context before approving a release.
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
273pub struct ReleaseGateCaseV1 {
274    pub schema_version: String,
275    pub release_gate_case_id: ReleaseGateCaseId,
276    pub deployment_profile_id: DeploymentProfileId,
277    pub assurance_case_id: AssuranceCaseId,
278    pub release_readiness_decision_id: ReleaseReadinessDecisionId,
279    #[serde(flatten)]
280    pub citation: V25CitationContext,
281    #[serde(flatten)]
282    pub obligation_refs: V25ControlObligationRefs,
283    pub citation_status: ConstitutionalContextStatus,
284    pub obligation_refs_status: ConstitutionalContextStatus,
285    pub score_only_gate: bool,
286    pub final_state: ReleaseGateFinalStateV1,
287}
288
289/// Terminal state of a release-gate review case.
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
291#[serde(rename_all = "snake_case")]
292pub enum ReleaseGateFinalStateV1 {
293    Approved,
294    #[serde(alias = "approved_with_monitors")]
295    ApprovedWithMonitoring,
296    Blocked,
297}
298
299/// A review case for continuity incidents (outage, recovery, exception handling).
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
301pub struct ContinuityReviewCaseV1 {
302    pub schema_version: String,
303    pub continuity_review_case_id: ContinuityReviewCaseId,
304    pub incident_case_id: IncidentCaseId,
305    #[serde(flatten)]
306    pub citation: V25CitationContext,
307    #[serde(flatten)]
308    pub obligation_refs: V25ControlObligationRefs,
309    pub citation_status: ConstitutionalContextStatus,
310    pub obligation_refs_status: ConstitutionalContextStatus,
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub continuity_exception_id: Option<ContinuityExceptionId>,
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub recovery_replay_slice_id: Option<RecoveryReplaySliceId>,
315    pub post_hoc_review_due: bool,
316    pub final_state: ContinuityReviewFinalStateV1,
317}
318
319/// Terminal state of a continuity review case.
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
321#[serde(rename_all = "snake_case")]
322pub enum ContinuityReviewFinalStateV1 {
323    OpenReview,
324    Closed,
325}
326
327/// Classification of the issue that opened a verification case.
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
329#[serde(rename_all = "snake_case")]
330pub enum VerificationCaseClass {
331    ActiveSyndrome,
332    FragileNode,
333    ThinExport,
334    UnverifiedClaimVersion,
335    RefutationGap,
336    SupersessionVerification,
337    ComparabilityDrift,
338    CalibrationCaveat,
339    ScopeStale,
340    QueryTurn,
341}
342
343/// Scope and location metadata for a verification case target.
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
345pub struct CaseRegion {
346    pub namespace: String,
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub scope_key: Option<ScopeKey>,
349    pub target_key: String,
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub region_id: Option<RegionId>,
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub region_digest_id: Option<RegionDigestId>,
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    pub claim_version_id: Option<ClaimVersionId>,
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub as_of_recorded_at: Option<String>,
358}
359
360/// Lifecycle state of a verification case from open to closed.
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
362#[serde(rename_all = "snake_case")]
363pub enum VerificationCaseLifecycleState {
364    Open,
365    Planned,
366    Executing,
367    Closed,
368}
369
370/// Terminal outcome of a closed verification case.
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
372#[serde(rename_all = "snake_case")]
373pub enum TerminalDisposition {
374    EligibleForPromotion,
375    AdvisoryOnly,
376    DegradedNoPromotion,
377    Refuted,
378    Quarantined,
379    RolledBack,
380    Exhausted,
381    Blocked,
382}
383
384/// Priority tier for promotion eligibility (P0 = highest).
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
386#[serde(rename_all = "snake_case")]
387pub enum PromotionClass {
388    P0,
389    P1,
390    P2,
391    P3,
392}
393
394/// How difficult it is to reverse a promoted verification result.
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
396#[serde(rename_all = "snake_case")]
397pub enum ReversibilityClass {
398    ReversibleLocal,
399    ReversibleScoped,
400    RequiresSupersession,
401    RequiresAuthorityWorkflow,
402}
403
404/// A single verification case tracking a governance issue from open to closed.
405///
406/// Cases are opened by the observation phase, assigned check plans, executed
407/// through verification attempts, and closed with a terminal disposition.
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
409pub struct VerificationCase {
410    pub schema_version: String,
411    pub case_id: VerificationCaseId,
412    pub class: VerificationCaseClass,
413    pub region: CaseRegion,
414    pub trace_ctx: TraceCtx,
415    pub attempt_id: AttemptId,
416    pub lifecycle_state: VerificationCaseLifecycleState,
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub terminal_disposition: Option<TerminalDisposition>,
419    pub non_authoritative: bool,
420    pub advisory_only: bool,
421    pub degraded: bool,
422    pub opened_at: String,
423}
424
425impl VerificationCase {
426    /// Creates an open verification case with the supplied scope, trace, and degradation flags.
427    pub fn new(
428        class: VerificationCaseClass,
429        region: CaseRegion,
430        trace_ctx: TraceCtx,
431        attempt_id: AttemptId,
432        opened_at: impl Into<String>,
433        degraded: bool,
434        advisory_only: bool,
435    ) -> Self {
436        Self {
437            schema_version: VERIFICATION_CASE_V1_SCHEMA.into(),
438            case_id: VerificationCaseId::generate(),
439            class,
440            region,
441            trace_ctx,
442            attempt_id,
443            lifecycle_state: VerificationCaseLifecycleState::Open,
444            terminal_disposition: None,
445            non_authoritative: true,
446            advisory_only,
447            degraded,
448            opened_at: opened_at.into(),
449        }
450    }
451
452    /// Closes the verification case with its terminal disposition.
453    pub fn close(mut self, disposition: TerminalDisposition) -> Self {
454        self.lifecycle_state = VerificationCaseLifecycleState::Closed;
455        self.terminal_disposition = Some(disposition);
456        self
457    }
458}
459
460/// Verification method used by a check plan to evaluate a case.
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
462#[serde(rename_all = "snake_case")]
463pub enum CheckMethod {
464    ExactBoundedOracle,
465    ConservativeOracle,
466    DeltaParityOracle,
467    TemporalReplayOracle,
468    CausalRefuter,
469    MinimalPerturbationOracle,
470    PairedPatch,
471    AdvisoryOnly,
472}
473
474/// Scheduler workload class used for budgeting and queue routing.
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
476#[serde(rename_all = "snake_case")]
477pub enum VerificationWorkloadClass {
478    Orchestration,
479    Oracle,
480    Patch,
481    Advisory,
482}
483
484/// A single queue-to-queue hop in the scheduler routing chain.
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
486pub struct QueueHop {
487    pub hop_index: u32,
488    pub from_queue: String,
489    pub to_queue: String,
490    pub enqueued_at: String,
491    #[serde(default, skip_serializing_if = "Option::is_none")]
492    pub dequeued_at: Option<String>,
493}
494
495/// Budget and retry lineage for a scheduled verification plan.
496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
497pub struct BudgetLineage {
498    pub budget_family: String,
499    pub retry_family: AttemptId,
500    pub queue_hop_count: u32,
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub max_time_budget_ms: Option<u64>,
503    #[serde(default, skip_serializing_if = "Option::is_none")]
504    pub remaining_time_budget_ms: Option<u64>,
505    #[serde(default, skip_serializing_if = "Option::is_none")]
506    pub max_cost_budget_units: Option<u64>,
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub remaining_cost_budget_units: Option<u64>,
509    pub exhausted: bool,
510}
511
512/// Kind of lightweight preflight check in the cheap-check ladder.
513#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
514#[serde(rename_all = "snake_case")]
515pub enum CheapCheckKindV1 {
516    SchemaValidation,
517    ProvenanceAudit,
518    ReplayPreflight,
519    OraclePreflight,
520    RefutationPreflight,
521}
522
523/// Status of a single cheap-check preflight evaluation.
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
525pub struct CheapCheckStatusV1 {
526    pub check: CheapCheckKindV1,
527    pub satisfied: bool,
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    pub detail: Option<String>,
530}
531
532/// Proof profile describing evidence admissibility, cheap-check status, and remaining obligations.
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
534pub struct ProofProfileV1 {
535    pub profile_name: String,
536    pub evidence_admissibility: EvidenceAdmissibilityV1,
537    #[serde(default, skip_serializing_if = "Vec::is_empty")]
538    pub cheap_checks: Vec<CheapCheckStatusV1>,
539    #[serde(default, skip_serializing_if = "Vec::is_empty")]
540    pub proof_obligations_remaining: Vec<String>,
541    #[serde(default, skip_serializing_if = "Vec::is_empty")]
542    pub admissible_evidence: Vec<String>,
543}
544
545/// A marker indicating degraded verification quality and whether it blocks promotion.
546#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
547pub struct DegradationMarker {
548    pub kind: String,
549    pub reason: String,
550    pub blocks_promotion: bool,
551}
552
553/// Scheduler decision record preserving budget, degradation, and routing lineage for a plan.
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
555pub struct SchedulerDecision {
556    pub schema_version: String,
557    pub case_id: VerificationCaseId,
558    pub plan_id: CheckPlanId,
559    pub workload_class: VerificationWorkloadClass,
560    pub cheapest_adequate: bool,
561    pub queue_hops: Vec<QueueHop>,
562    pub budget_lineage: BudgetLineage,
563    #[serde(default, skip_serializing_if = "Option::is_none")]
564    pub exactness_budget: Option<ExactnessBudgetV1>,
565    #[serde(default)]
566    pub degradation_markers: Vec<DegradationMarker>,
567    pub promotion_blocked: bool,
568}
569
570/// How a turn ultimately dispatched after planning completed.
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
572#[serde(rename_all = "snake_case")]
573pub enum DispatchPathKind {
574    ProviderNative,
575    RuntimeNative,
576    ParserFallback,
577    NoTools,
578    PolicyBlocked,
579}
580
581/// Terminal reason why a turn stopped.
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
583#[serde(rename_all = "snake_case")]
584pub enum ExecutionStopReason {
585    Success,
586    PolicyBlocked,
587    RoundTripBudgetExhausted,
588    BudgetExhausted,
589    RetryExhausted,
590    EscalationRequested,
591    Abstained,
592}
593
594/// Per-turn dispatch truth: which backend ran and why.
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
596pub struct DispatchDecision {
597    pub schema_version: String,
598    pub case_id: VerificationCaseId,
599    pub plan_id: CheckPlanId,
600    pub trace_ctx: TraceCtx,
601    pub provider_kind: String,
602    pub backend_kind: String,
603    pub execution_mode_label: String,
604    pub dispatch_path: DispatchPathKind,
605    pub query_mode: String,
606    pub dispatch_reason: String,
607    pub stop_reason: ExecutionStopReason,
608}
609
610impl DispatchDecision {
611    #[allow(clippy::too_many_arguments)]
612    pub fn new(
613        case_id: VerificationCaseId,
614        plan_id: CheckPlanId,
615        trace_ctx: TraceCtx,
616        provider_kind: impl Into<String>,
617        backend_kind: impl Into<String>,
618        execution_mode_label: impl Into<String>,
619        dispatch_path: DispatchPathKind,
620        query_mode: impl Into<String>,
621        dispatch_reason: impl Into<String>,
622        stop_reason: ExecutionStopReason,
623    ) -> Self {
624        Self {
625            schema_version: DISPATCH_DECISION_V1_SCHEMA.into(),
626            case_id,
627            plan_id,
628            trace_ctx,
629            provider_kind: provider_kind.into(),
630            backend_kind: backend_kind.into(),
631            execution_mode_label: execution_mode_label.into(),
632            dispatch_path,
633            query_mode: query_mode.into(),
634            dispatch_reason: dispatch_reason.into(),
635            stop_reason,
636        }
637    }
638}
639
640/// Explicit per-turn tool exposure, including the empty set.
641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
642pub struct ToolExposureDecisionV1 {
643    pub tool_name: String,
644    pub exposed: bool,
645    pub reason: String,
646}
647
648impl From<ToolExposureDecision> for ToolExposureDecisionV1 {
649    fn from(value: ToolExposureDecision) -> Self {
650        Self {
651            tool_name: value.tool_name,
652            exposed: value.exposed,
653            reason: value.reason,
654        }
655    }
656}
657
658#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
659pub struct ToolExposure {
660    pub schema_version: String,
661    pub case_id: VerificationCaseId,
662    pub plan_id: CheckPlanId,
663    pub trace_ctx: TraceCtx,
664    pub planner_stage: String,
665    #[serde(default, skip_serializing_if = "Vec::is_empty")]
666    pub exposed_tools: Vec<String>,
667    #[serde(default, skip_serializing_if = "Vec::is_empty")]
668    pub decisions: Vec<ToolExposureDecisionV1>,
669    pub explicit_empty: bool,
670}
671
672impl ToolExposure {
673    pub fn new(
674        case_id: VerificationCaseId,
675        plan_id: CheckPlanId,
676        trace_ctx: TraceCtx,
677        planner_stage: impl Into<String>,
678        decisions: Vec<ToolExposureDecision>,
679    ) -> Self {
680        let exposed_tools = decisions
681            .iter()
682            .filter(|decision| decision.exposed)
683            .map(|decision| decision.tool_name.clone())
684            .collect::<Vec<_>>();
685        let explicit_empty = exposed_tools.is_empty();
686        let decisions = decisions
687            .into_iter()
688            .map(ToolExposureDecisionV1::from)
689            .collect();
690        Self {
691            schema_version: TOOL_EXPOSURE_V1_SCHEMA.into(),
692            case_id,
693            plan_id,
694            trace_ctx,
695            planner_stage: planner_stage.into(),
696            exposed_tools,
697            decisions,
698            explicit_empty,
699        }
700    }
701}
702
703/// Queryable retry lineage for a turn or tool family.
704#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
705pub struct RetryDecision {
706    pub schema_version: String,
707    pub case_id: VerificationCaseId,
708    pub plan_id: CheckPlanId,
709    pub retry_family: String,
710    pub attempt_number: u32,
711    #[serde(default, skip_serializing_if = "Option::is_none")]
712    pub backoff_ms: Option<u64>,
713    pub will_retry: bool,
714    pub stop_reason: ExecutionStopReason,
715    pub owner: ControlRetryOwnerV1,
716    pub reason: String,
717}
718
719impl RetryDecision {
720    #[allow(clippy::too_many_arguments)]
721    pub fn new(
722        case_id: VerificationCaseId,
723        plan_id: CheckPlanId,
724        retry_family: impl Into<String>,
725        attempt_number: u32,
726        backoff_ms: Option<u64>,
727        will_retry: bool,
728        stop_reason: ExecutionStopReason,
729        owner: ControlRetryOwnerV1,
730        reason: impl Into<String>,
731    ) -> Self {
732        Self {
733            schema_version: RETRY_DECISION_V1_SCHEMA.into(),
734            case_id,
735            plan_id,
736            retry_family: retry_family.into(),
737            attempt_number,
738            backoff_ms,
739            will_retry,
740            stop_reason,
741            owner,
742            reason: reason.into(),
743        }
744    }
745}
746
747/// Budget lineage for one debit against a turn's execution envelope.
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
749pub struct BudgetDebit {
750    pub schema_version: String,
751    pub case_id: VerificationCaseId,
752    pub plan_id: CheckPlanId,
753    pub debit_kind: String,
754    pub unit: String,
755    pub amount: u64,
756    #[serde(default, skip_serializing_if = "Option::is_none")]
757    pub remaining: Option<u64>,
758    pub exhausted: bool,
759    #[serde(default, skip_serializing_if = "Option::is_none")]
760    pub detail: Option<String>,
761}
762
763impl BudgetDebit {
764    #[allow(clippy::too_many_arguments)]
765    pub fn new(
766        case_id: VerificationCaseId,
767        plan_id: CheckPlanId,
768        debit_kind: impl Into<String>,
769        unit: impl Into<String>,
770        amount: u64,
771        remaining: Option<u64>,
772        exhausted: bool,
773        detail: Option<String>,
774    ) -> Self {
775        Self {
776            schema_version: BUDGET_DEBIT_V1_SCHEMA.into(),
777            case_id,
778            plan_id,
779            debit_kind: debit_kind.into(),
780            unit: unit.into(),
781            amount,
782            remaining,
783            exhausted,
784            detail,
785        }
786    }
787}
788
789/// Canonical rollback/compensation requirement for a turn.
790#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
791pub struct RollbackDecision {
792    pub schema_version: String,
793    pub case_id: VerificationCaseId,
794    pub plan_id: CheckPlanId,
795    pub required: bool,
796    pub reason: String,
797    pub source: String,
798}
799
800impl RollbackDecision {
801    pub fn new(
802        case_id: VerificationCaseId,
803        plan_id: CheckPlanId,
804        required: bool,
805        reason: impl Into<String>,
806        source: impl Into<String>,
807    ) -> Self {
808        Self {
809            schema_version: ROLLBACK_DECISION_V1_SCHEMA.into(),
810            case_id,
811            plan_id,
812            required,
813            reason: reason.into(),
814            source: source.into(),
815        }
816    }
817}
818
819/// A check plan specifying the verification method, promotion class, and proof profile for a case.
820#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
821pub struct CheckPlan {
822    pub schema_version: String,
823    pub plan_id: CheckPlanId,
824    pub case_id: VerificationCaseId,
825    pub method: CheckMethod,
826    pub planner_family: String,
827    #[serde(default)]
828    pub check_names: Vec<String>,
829    pub promotion_class: PromotionClass,
830    pub reversibility_class: ReversibilityClass,
831    pub cheapest_adequate: bool,
832    pub promotable_if_completed: bool,
833    pub advisory_only: bool,
834    pub degraded: bool,
835    pub target_exactness: ExactnessLevelV1,
836    pub evidence_admissibility: EvidenceAdmissibilityV1,
837    #[serde(default, skip_serializing_if = "Vec::is_empty")]
838    pub proof_obligations_remaining: Vec<String>,
839    #[serde(default, skip_serializing_if = "Vec::is_empty")]
840    pub cheap_check_ladder: Vec<CheapCheckStatusV1>,
841    #[serde(default, skip_serializing_if = "Option::is_none")]
842    pub proof_profile: Option<ProofProfileV1>,
843    pub rationale: String,
844    pub input: Value,
845}
846
847impl CheckPlan {
848    #[allow(clippy::too_many_arguments)]
849    /// Creates a check plan with the default planner family and baseline admissibility policy.
850    pub fn new(
851        case_id: VerificationCaseId,
852        method: CheckMethod,
853        check_names: Vec<String>,
854        promotion_class: PromotionClass,
855        reversibility_class: ReversibilityClass,
856        promotable_if_completed: bool,
857        advisory_only: bool,
858        degraded: bool,
859        rationale: impl Into<String>,
860        input: Value,
861    ) -> Self {
862        Self {
863            schema_version: CHECK_PLAN_V1_SCHEMA.into(),
864            plan_id: CheckPlanId::generate(),
865            case_id,
866            method,
867            planner_family: "forge-pilot".into(),
868            check_names,
869            promotion_class,
870            reversibility_class,
871            cheapest_adequate: true,
872            promotable_if_completed,
873            advisory_only,
874            degraded,
875            target_exactness: if degraded {
876                ExactnessLevelV1::Conservative
877            } else {
878                ExactnessLevelV1::Exact
879            },
880            evidence_admissibility: if degraded {
881                EvidenceAdmissibilityV1::Restricted
882            } else {
883                EvidenceAdmissibilityV1::Admissible
884            },
885            proof_obligations_remaining: Vec::new(),
886            cheap_check_ladder: Vec::new(),
887            proof_profile: None,
888            rationale: rationale.into(),
889            input,
890        }
891    }
892}
893
894/// Execution state of a verification attempt.
895#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
896#[serde(rename_all = "snake_case")]
897pub enum VerificationAttemptState {
898    Scheduled,
899    Running,
900    Succeeded,
901    Failed,
902    AdvisoryOnly,
903    Blocked,
904}
905
906/// A single execution attempt within a verification case and plan.
907#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
908pub struct VerificationAttempt {
909    pub schema_version: String,
910    pub case_id: VerificationCaseId,
911    pub plan_id: CheckPlanId,
912    pub attempt_id: AttemptId,
913    #[serde(default, skip_serializing_if = "Option::is_none")]
914    pub trial_id: Option<TrialId>,
915    pub state: VerificationAttemptState,
916    pub advisory_only: bool,
917    pub degraded: bool,
918    pub started_at: String,
919    pub finished_at: String,
920    #[serde(default, skip_serializing_if = "Option::is_none")]
921    pub outcome_signature: Option<String>,
922}
923
924impl VerificationAttempt {
925    #[allow(clippy::too_many_arguments)]
926    /// Records a completed verification attempt and its terminal execution metadata.
927    pub fn completed(
928        case_id: VerificationCaseId,
929        plan_id: CheckPlanId,
930        attempt_id: AttemptId,
931        trial_id: Option<TrialId>,
932        state: VerificationAttemptState,
933        advisory_only: bool,
934        degraded: bool,
935        started_at: impl Into<String>,
936        finished_at: impl Into<String>,
937        outcome_signature: Option<String>,
938    ) -> Self {
939        Self {
940            schema_version: VERIFICATION_ATTEMPT_V1_SCHEMA.into(),
941            case_id,
942            plan_id,
943            attempt_id,
944            trial_id,
945            state,
946            advisory_only,
947            degraded,
948            started_at: started_at.into(),
949            finished_at: finished_at.into(),
950            outcome_signature,
951        }
952    }
953}
954
955/// Kind of action recorded in a control receipt.
956#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
957#[serde(rename_all = "snake_case")]
958pub enum ControlActionKind {
959    ToolExecution,
960    OrchestrationLoop,
961    VerificationExecution,
962}
963
964/// Budget context attached to a control receipt for cost and time tracking.
965#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
966pub struct ReceiptBudgetContext {
967    #[serde(default, skip_serializing_if = "Option::is_none")]
968    pub budget_kind: Option<String>,
969    #[serde(default, skip_serializing_if = "Option::is_none")]
970    pub max_steps: Option<u32>,
971    #[serde(default, skip_serializing_if = "Option::is_none")]
972    pub time_budget_ms: Option<u64>,
973    #[serde(default, skip_serializing_if = "Option::is_none")]
974    pub cost_budget_units: Option<u64>,
975}
976
977/// An immutable receipt recording a control-plane action with full provenance.
978///
979/// Control receipts carry constitutional citation context, budget context,
980/// retry ownership, and promotion eligibility. They are the atomic audit
981/// unit for the verification ledger.
982#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
983pub struct ControlReceipt {
984    pub schema_version: String,
985    pub receipt_id: ControlReceiptId,
986    pub action_kind: ControlActionKind,
987    #[serde(default, skip_serializing_if = "Option::is_none")]
988    pub case_id: Option<VerificationCaseId>,
989    #[serde(default, skip_serializing_if = "Option::is_none")]
990    pub plan_id: Option<CheckPlanId>,
991    pub trace_ctx: TraceCtx,
992    pub attempt_id: AttemptId,
993    #[serde(default, skip_serializing_if = "Option::is_none")]
994    pub trial_id: Option<TrialId>,
995    pub actor: String,
996    #[serde(default, skip_serializing_if = "Option::is_none")]
997    pub planner_stage: Option<String>,
998    pub started_at: String,
999    pub finished_at: String,
1000    #[serde(flatten)]
1001    pub citation: V25CitationContext,
1002    #[serde(flatten)]
1003    pub obligation_refs: V25ControlObligationRefs,
1004    pub citation_status: ConstitutionalContextStatus,
1005    pub obligation_refs_status: ConstitutionalContextStatus,
1006    #[serde(default, skip_serializing_if = "Option::is_none")]
1007    pub deadline: Option<String>,
1008    #[serde(default, skip_serializing_if = "Option::is_none")]
1009    pub workload_class: Option<String>,
1010    #[serde(default, skip_serializing_if = "Option::is_none")]
1011    pub budget_context: Option<ReceiptBudgetContext>,
1012    #[serde(default, skip_serializing_if = "Option::is_none")]
1013    pub parent_receipt_id: Option<ControlReceiptId>,
1014    #[serde(default, skip_serializing_if = "Option::is_none")]
1015    pub family_receipt_id: Option<ControlReceiptId>,
1016    #[serde(default, skip_serializing_if = "Option::is_none")]
1017    pub replay_parent_receipt_id: Option<ControlReceiptId>,
1018    #[serde(default, skip_serializing_if = "Option::is_none")]
1019    pub retry_owner: Option<ControlRetryOwnerV1>,
1020    pub non_authoritative: bool,
1021    pub advisory_only: bool,
1022    pub degraded: bool,
1023    pub promotable: bool,
1024    pub source_receipt_kind: String,
1025    #[serde(default, skip_serializing_if = "Option::is_none")]
1026    pub source_receipt_id: Option<String>,
1027    pub details: Value,
1028}
1029
1030/// Which subsystem owns retry responsibility for a control action.
1031#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1032#[serde(rename_all = "snake_case")]
1033pub enum ControlRetryOwnerV1 {
1034    LlmPipeline,
1035    AgentGraph,
1036    JobQueue,
1037    AiBatchQueue,
1038    ForgeOrchestration,
1039    External,
1040}
1041
1042impl From<&ToolRetryOwner> for ControlRetryOwnerV1 {
1043    fn from(value: &ToolRetryOwner) -> Self {
1044        match value {
1045            ToolRetryOwner::LlmPipeline => Self::LlmPipeline,
1046            ToolRetryOwner::AgentGraph => Self::AgentGraph,
1047            ToolRetryOwner::JobQueue => Self::JobQueue,
1048            ToolRetryOwner::AiBatchQueue => Self::AiBatchQueue,
1049            ToolRetryOwner::ForgeOrchestration => Self::ForgeOrchestration,
1050            ToolRetryOwner::External => Self::External,
1051        }
1052    }
1053}
1054
1055/// Kind of artifact subject to boundary repair.
1056#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1057#[serde(rename_all = "snake_case")]
1058pub enum BoundaryArtifactKind {
1059    LoopReport,
1060    LoopIterationReport,
1061    ControlReceipt,
1062}
1063
1064/// Record of a single field-level repair applied to a boundary artifact.
1065#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1066pub struct BoundaryRepairRecord {
1067    pub schema_version: String,
1068    pub repair_record_id: BoundaryRepairRecordId,
1069    pub artifact_kind: BoundaryArtifactKind,
1070    pub artifact_schema_version: String,
1071    pub repair_kind: String,
1072    pub field_path: String,
1073    #[serde(default, skip_serializing_if = "Option::is_none")]
1074    pub original_value: Option<Value>,
1075    pub repaired_value: Value,
1076    pub rationale: String,
1077}
1078
1079/// Kind of artifact transported between compiled regions.
1080#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1081#[serde(rename_all = "snake_case")]
1082pub enum RegionArtifactKind {
1083    Delta,
1084    Residual,
1085    Syndrome,
1086    Witness,
1087    ControlReceipt,
1088    NuisanceState,
1089    OracleParity,
1090}
1091
1092/// Transport record for an artifact crossing between compiled verification regions.
1093#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1094pub struct RegionArtifactTransport {
1095    pub schema_version: String,
1096    pub transport_id: ArtifactTransportId,
1097    pub artifact_kind: RegionArtifactKind,
1098    pub artifact_id: String,
1099    pub artifact_digest: ContentDigest,
1100    pub from_region_id: RegionId,
1101    pub to_region_id: RegionId,
1102    pub from_surface: String,
1103    pub to_surface: String,
1104    pub replay_ref: String,
1105}
1106
1107/// Routing record linking a syndrome to a repair candidate and its blast radius.
1108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1109pub struct SyndromeRouteRecord {
1110    pub schema_version: String,
1111    pub route_id: RepairRouteId,
1112    pub syndrome_id: SyndromeId,
1113    pub region_id: RegionId,
1114    pub candidate_id: RepairCandidateId,
1115    pub repair_surface: String,
1116    pub blast_radius_node_ids: Vec<String>,
1117    pub witness_refs: Vec<String>,
1118    pub routed_before_global_invalidation: bool,
1119}
1120
1121fn validate_review_artifact_context(
1122    schema_version: &str,
1123    expected_schema: &str,
1124    citation: &V25CitationContext,
1125    citation_status: ConstitutionalContextStatus,
1126    obligation_refs: &V25ControlObligationRefs,
1127    obligation_refs_status: ConstitutionalContextStatus,
1128) -> Result<(), String> {
1129    if schema_version != expected_schema {
1130        return Err(format!(
1131            "expected schema_version `{expected_schema}`, found `{schema_version}`"
1132        ));
1133    }
1134    let actual_citation_status = citation.context_status();
1135    if citation_status != actual_citation_status {
1136        return Err(format!(
1137            "citation_status {:?} does not match citation payload {:?}",
1138            citation_status, actual_citation_status
1139        ));
1140    }
1141    let actual_obligation_status = obligation_refs.context_status();
1142    if obligation_refs_status != actual_obligation_status {
1143        return Err(format!(
1144            "obligation_refs_status {:?} does not match obligation payload {:?}",
1145            obligation_refs_status, actual_obligation_status
1146        ));
1147    }
1148    if !citation_status.is_complete() {
1149        return Err("review artifact is missing constitutional citation context".into());
1150    }
1151    if !obligation_refs_status.is_complete() {
1152        return Err("review artifact is missing constitutional obligation refs".into());
1153    }
1154    Ok(())
1155}
1156
1157impl EffectReviewCaseV1 {
1158    #[allow(clippy::too_many_arguments)]
1159    /// Creates an effect review case with computed constitutional context status.
1160    pub fn new(
1161        effect_intent_id: EffectIntentId,
1162        effect_preflight_report_id: EffectPreflightReportId,
1163        effect_commit_decision_id: Option<EffectCommitDecisionId>,
1164        citation: V25CitationContext,
1165        obligation_refs: V25ControlObligationRefs,
1166        required_policy_refs: Vec<String>,
1167        decision_basis: impl Into<String>,
1168        final_state: EffectReviewFinalStateV1,
1169        advisory_only: bool,
1170        generated_at: impl Into<String>,
1171    ) -> Result<Self, String> {
1172        let review = Self {
1173            schema_version: EFFECT_REVIEW_CASE_V1_SCHEMA.into(),
1174            effect_review_case_id: EffectReviewCaseId::generate(),
1175            effect_intent_id,
1176            effect_preflight_report_id,
1177            effect_commit_decision_id,
1178            citation_status: citation.context_status(),
1179            obligation_refs_status: obligation_refs.context_status(),
1180            citation,
1181            obligation_refs,
1182            required_policy_refs,
1183            decision_basis: decision_basis.into(),
1184            final_state,
1185            advisory_only,
1186            generated_at: generated_at.into(),
1187        };
1188        review.validate()?;
1189        Ok(review)
1190    }
1191
1192    /// Rejects review artifacts whose context/status/state combinations are impossible.
1193    pub fn validate(&self) -> Result<(), String> {
1194        validate_review_artifact_context(
1195            &self.schema_version,
1196            EFFECT_REVIEW_CASE_V1_SCHEMA,
1197            &self.citation,
1198            self.citation_status,
1199            &self.obligation_refs,
1200            self.obligation_refs_status,
1201        )?;
1202        if self.required_policy_refs.is_empty() {
1203            return Err("effect review case requires at least one policy ref".into());
1204        }
1205        if self.advisory_only != matches!(self.final_state, EffectReviewFinalStateV1::AdvisoryOnly)
1206        {
1207            return Err("advisory_only must match effect review final_state".into());
1208        }
1209        Ok(())
1210    }
1211}
1212
1213impl EffectBlockReceiptV1 {
1214    /// Creates an effect block receipt with computed constitutional context status.
1215    pub fn new(
1216        effect_review_case_id: EffectReviewCaseId,
1217        citation: V25CitationContext,
1218        obligation_refs: V25ControlObligationRefs,
1219        block_reason: EffectBlockReasonV1,
1220        generated_at: impl Into<String>,
1221    ) -> Result<Self, String> {
1222        let receipt = Self {
1223            schema_version: EFFECT_BLOCK_RECEIPT_V1_SCHEMA.into(),
1224            effect_block_receipt_id: EffectBlockReceiptId::generate(),
1225            effect_review_case_id,
1226            citation_status: citation.context_status(),
1227            obligation_refs_status: obligation_refs.context_status(),
1228            citation,
1229            obligation_refs,
1230            block_reason,
1231            generated_at: generated_at.into(),
1232        };
1233        receipt.validate()?;
1234        Ok(receipt)
1235    }
1236
1237    /// Rejects block receipts that drift from their constitutional context payload.
1238    pub fn validate(&self) -> Result<(), String> {
1239        validate_review_artifact_context(
1240            &self.schema_version,
1241            EFFECT_BLOCK_RECEIPT_V1_SCHEMA,
1242            &self.citation,
1243            self.citation_status,
1244            &self.obligation_refs,
1245            self.obligation_refs_status,
1246        )
1247    }
1248}
1249
1250impl DelegationReviewCaseV1 {
1251    /// Creates a delegation review case with computed constitutional context status.
1252    pub fn new(
1253        authority_chain_id: AuthorityChainId,
1254        separation_of_duties_policy_id: SeparationOfDutiesPolicyId,
1255        citation: V25CitationContext,
1256        obligation_refs: V25ControlObligationRefs,
1257        decision_state: DelegationReviewDecisionStateV1,
1258        generated_at: impl Into<String>,
1259    ) -> Result<Self, String> {
1260        let review = Self {
1261            schema_version: DELEGATION_REVIEW_CASE_V1_SCHEMA.into(),
1262            delegation_review_case_id: DelegationReviewCaseId::generate(),
1263            authority_chain_id,
1264            separation_of_duties_policy_id,
1265            citation_status: citation.context_status(),
1266            obligation_refs_status: obligation_refs.context_status(),
1267            citation,
1268            obligation_refs,
1269            decision_state,
1270            generated_at: generated_at.into(),
1271        };
1272        review.validate()?;
1273        Ok(review)
1274    }
1275
1276    /// Rejects delegation review cases with inconsistent constitutional context status.
1277    pub fn validate(&self) -> Result<(), String> {
1278        validate_review_artifact_context(
1279            &self.schema_version,
1280            DELEGATION_REVIEW_CASE_V1_SCHEMA,
1281            &self.citation,
1282            self.citation_status,
1283            &self.obligation_refs,
1284            self.obligation_refs_status,
1285        )
1286    }
1287}
1288
1289impl ReleaseGateCaseV1 {
1290    /// Creates a release-gate review case with computed constitutional context status.
1291    pub fn new(
1292        deployment_profile_id: DeploymentProfileId,
1293        assurance_case_id: AssuranceCaseId,
1294        release_readiness_decision_id: ReleaseReadinessDecisionId,
1295        citation: V25CitationContext,
1296        obligation_refs: V25ControlObligationRefs,
1297        score_only_gate: bool,
1298        final_state: ReleaseGateFinalStateV1,
1299    ) -> Result<Self, String> {
1300        let review = Self {
1301            schema_version: RELEASE_GATE_CASE_V1_SCHEMA.into(),
1302            release_gate_case_id: ReleaseGateCaseId::generate(),
1303            deployment_profile_id,
1304            assurance_case_id,
1305            release_readiness_decision_id,
1306            citation_status: citation.context_status(),
1307            obligation_refs_status: obligation_refs.context_status(),
1308            citation,
1309            obligation_refs,
1310            score_only_gate,
1311            final_state,
1312        };
1313        review.validate()?;
1314        Ok(review)
1315    }
1316
1317    /// Rejects release-gate review cases with impossible state/context combinations.
1318    pub fn validate(&self) -> Result<(), String> {
1319        validate_review_artifact_context(
1320            &self.schema_version,
1321            RELEASE_GATE_CASE_V1_SCHEMA,
1322            &self.citation,
1323            self.citation_status,
1324            &self.obligation_refs,
1325            self.obligation_refs_status,
1326        )?;
1327        if self.score_only_gate && matches!(self.final_state, ReleaseGateFinalStateV1::Approved) {
1328            return Err("score-only release gates cannot be fully approved".into());
1329        }
1330        Ok(())
1331    }
1332}
1333
1334impl ContinuityReviewCaseV1 {
1335    #[allow(clippy::too_many_arguments)]
1336    /// Creates a continuity review case with computed constitutional context status.
1337    pub fn new(
1338        incident_case_id: IncidentCaseId,
1339        citation: V25CitationContext,
1340        obligation_refs: V25ControlObligationRefs,
1341        continuity_exception_id: Option<ContinuityExceptionId>,
1342        recovery_replay_slice_id: Option<RecoveryReplaySliceId>,
1343        post_hoc_review_due: bool,
1344        final_state: ContinuityReviewFinalStateV1,
1345    ) -> Result<Self, String> {
1346        let review = Self {
1347            schema_version: CONTINUITY_REVIEW_CASE_V1_SCHEMA.into(),
1348            continuity_review_case_id: ContinuityReviewCaseId::generate(),
1349            incident_case_id,
1350            citation_status: citation.context_status(),
1351            obligation_refs_status: obligation_refs.context_status(),
1352            citation,
1353            obligation_refs,
1354            continuity_exception_id,
1355            recovery_replay_slice_id,
1356            post_hoc_review_due,
1357            final_state,
1358        };
1359        review.validate()?;
1360        Ok(review)
1361    }
1362
1363    /// Rejects continuity review cases that claim closure while post-hoc review is still due.
1364    pub fn validate(&self) -> Result<(), String> {
1365        validate_review_artifact_context(
1366            &self.schema_version,
1367            CONTINUITY_REVIEW_CASE_V1_SCHEMA,
1368            &self.citation,
1369            self.citation_status,
1370            &self.obligation_refs,
1371            self.obligation_refs_status,
1372        )?;
1373        if self.post_hoc_review_due
1374            && matches!(self.final_state, ContinuityReviewFinalStateV1::Closed)
1375        {
1376            return Err(
1377                "continuity review cannot be closed while post-hoc review is still due".into(),
1378            );
1379        }
1380        Ok(())
1381    }
1382}
1383
1384impl BoundaryRepairRecord {
1385    /// Creates a boundary-repair record for one repaired artifact field.
1386    pub fn new(
1387        artifact_kind: BoundaryArtifactKind,
1388        artifact_schema_version: impl Into<String>,
1389        repair_kind: impl Into<String>,
1390        field_path: impl Into<String>,
1391        original_value: Option<Value>,
1392        repaired_value: Value,
1393        rationale: impl Into<String>,
1394    ) -> Self {
1395        Self {
1396            schema_version: BOUNDARY_REPAIR_RECORD_V1_SCHEMA.into(),
1397            repair_record_id: BoundaryRepairRecordId::generate(),
1398            artifact_kind,
1399            artifact_schema_version: artifact_schema_version.into(),
1400            repair_kind: repair_kind.into(),
1401            field_path: field_path.into(),
1402            original_value,
1403            repaired_value,
1404            rationale: rationale.into(),
1405        }
1406    }
1407}
1408
1409impl RegionArtifactTransport {
1410    #[allow(clippy::too_many_arguments)]
1411    /// Creates a transport record for an artifact crossing between compiled regions.
1412    pub fn new(
1413        artifact_kind: RegionArtifactKind,
1414        artifact_id: impl Into<String>,
1415        artifact_bytes: &[u8],
1416        from_region_id: RegionId,
1417        to_region_id: RegionId,
1418        from_surface: impl Into<String>,
1419        to_surface: impl Into<String>,
1420        replay_ref: impl Into<String>,
1421    ) -> Self {
1422        Self {
1423            schema_version: REGION_ARTIFACT_TRANSPORT_V1_SCHEMA.into(),
1424            transport_id: ArtifactTransportId::generate(),
1425            artifact_kind,
1426            artifact_id: artifact_id.into(),
1427            artifact_digest: ContentDigest::compute(artifact_bytes),
1428            from_region_id,
1429            to_region_id,
1430            from_surface: from_surface.into(),
1431            to_surface: to_surface.into(),
1432            replay_ref: replay_ref.into(),
1433        }
1434    }
1435}
1436
1437impl SyndromeRouteRecord {
1438    /// Creates the default syndrome route record for a compiled repair path.
1439    pub fn new(
1440        syndrome_id: SyndromeId,
1441        region_id: RegionId,
1442        blast_radius_node_ids: Vec<String>,
1443        witness_refs: Vec<String>,
1444    ) -> Self {
1445        Self {
1446            schema_version: SYNDROME_ROUTE_RECORD_V1_SCHEMA.into(),
1447            route_id: RepairRouteId::generate(),
1448            syndrome_id,
1449            region_id,
1450            candidate_id: RepairCandidateId::generate(),
1451            repair_surface: "compiled_repair_graph".into(),
1452            blast_radius_node_ids,
1453            witness_refs,
1454            routed_before_global_invalidation: true,
1455        }
1456    }
1457}
1458
1459impl ControlReceipt {
1460    /// Creates a verification execution receipt with explicit missing constitutional context status.
1461    pub fn new_case_execution(
1462        case: &VerificationCase,
1463        plan: &CheckPlan,
1464        attempt: &VerificationAttempt,
1465        promotable: bool,
1466        details: Value,
1467    ) -> Self {
1468        let citation = V25CitationContext::missing();
1469        let citation_status = citation.context_status();
1470        let obligation_refs = V25ControlObligationRefs::missing();
1471        let obligation_refs_status = obligation_refs.context_status();
1472        let receipt = Self {
1473            schema_version: CONTROL_RECEIPT_V1_SCHEMA.into(),
1474            receipt_id: ControlReceiptId::generate(),
1475            action_kind: ControlActionKind::VerificationExecution,
1476            case_id: Some(case.case_id.clone()),
1477            plan_id: Some(plan.plan_id.clone()),
1478            trace_ctx: case.trace_ctx.clone(),
1479            attempt_id: attempt.attempt_id.clone(),
1480            trial_id: attempt.trial_id.clone(),
1481            actor: "forge-pilot".into(),
1482            planner_stage: Some("verification_execution".into()),
1483            started_at: attempt.started_at.clone(),
1484            finished_at: attempt.finished_at.clone(),
1485            citation,
1486            obligation_refs,
1487            citation_status,
1488            obligation_refs_status,
1489            deadline: None,
1490            workload_class: Some("orchestration".into()),
1491            budget_context: None,
1492            parent_receipt_id: None,
1493            family_receipt_id: None,
1494            replay_parent_receipt_id: None,
1495            retry_owner: Some(ControlRetryOwnerV1::ForgeOrchestration),
1496            non_authoritative: true,
1497            advisory_only: attempt.advisory_only,
1498            degraded: attempt.degraded,
1499            promotable: promotable
1500                && citation_status.is_complete()
1501                && obligation_refs_status.is_complete(),
1502            source_receipt_kind: "forge_pilot.loop_iteration".into(),
1503            source_receipt_id: None,
1504            details,
1505        };
1506        debug_assert!(receipt.validate().is_ok());
1507        receipt
1508    }
1509
1510    /// Rejects receipts whose constitutional status payload or promotability claims are incoherent.
1511    pub fn validate(&self) -> Result<(), String> {
1512        if self.schema_version != CONTROL_RECEIPT_V1_SCHEMA {
1513            return Err(format!(
1514                "expected schema_version `{CONTROL_RECEIPT_V1_SCHEMA}`, found `{}`",
1515                self.schema_version
1516            ));
1517        }
1518        let actual_citation_status = self.citation.context_status();
1519        if self.citation_status != actual_citation_status {
1520            return Err(format!(
1521                "citation_status {:?} does not match citation payload {:?}",
1522                self.citation_status, actual_citation_status
1523            ));
1524        }
1525        let actual_obligation_status = self.obligation_refs.context_status();
1526        if self.obligation_refs_status != actual_obligation_status {
1527            return Err(format!(
1528                "obligation_refs_status {:?} does not match obligation payload {:?}",
1529                self.obligation_refs_status, actual_obligation_status
1530            ));
1531        }
1532        if self.promotable
1533            && (!self.citation_status.is_complete() || !self.obligation_refs_status.is_complete())
1534        {
1535            return Err(
1536                "promotable control receipt requires complete citation and obligation refs".into(),
1537            );
1538        }
1539        Ok(())
1540    }
1541}
1542
1543impl From<&ToolReceipt> for ControlReceipt {
1544    fn from(value: &ToolReceipt) -> Self {
1545        let citation = V25CitationContext::missing();
1546        let citation_status = citation.context_status();
1547        let obligation_refs = V25ControlObligationRefs::missing();
1548        let obligation_refs_status = obligation_refs.context_status();
1549        Self {
1550            schema_version: CONTROL_RECEIPT_V1_SCHEMA.into(),
1551            receipt_id: ControlReceiptId::generate(),
1552            action_kind: ControlActionKind::ToolExecution,
1553            case_id: None,
1554            plan_id: None,
1555            trace_ctx: value.trace_ctx.clone(),
1556            attempt_id: value.attempt_id.clone(),
1557            trial_id: Some(value.trial_id.clone()),
1558            actor: value.tool_name.clone(),
1559            planner_stage: Some(format!("{:?}", value.planner_stage)),
1560            started_at: value.started_at.clone(),
1561            finished_at: value.finished_at.clone(),
1562            citation,
1563            obligation_refs,
1564            citation_status,
1565            obligation_refs_status,
1566            deadline: value.deadline.clone(),
1567            workload_class: value.workload_class.clone(),
1568            budget_context: value
1569                .budget_context
1570                .as_ref()
1571                .map(|budget| ReceiptBudgetContext {
1572                    budget_kind: budget.budget_kind.clone(),
1573                    max_steps: budget.max_steps,
1574                    time_budget_ms: budget.time_budget_ms,
1575                    cost_budget_units: budget.cost_budget_units,
1576                }),
1577            parent_receipt_id: value
1578                .parent_receipt_id
1579                .as_ref()
1580                .map(|id| ControlReceiptId::new(id.clone())),
1581            family_receipt_id: value
1582                .family_receipt_id
1583                .as_ref()
1584                .map(|id| ControlReceiptId::new(id.clone())),
1585            replay_parent_receipt_id: value
1586                .replay_parent_receipt_id
1587                .as_ref()
1588                .map(|id| ControlReceiptId::new(id.clone())),
1589            retry_owner: Some(ControlRetryOwnerV1::from(&value.retry_owner)),
1590            non_authoritative: true,
1591            advisory_only: false,
1592            degraded: false,
1593            promotable: false,
1594            source_receipt_kind: "llm_tool_runtime.tool_receipt".into(),
1595            source_receipt_id: Some(value.receipt_id.clone()),
1596            details: json!({
1597                "tool_name": value.tool_name,
1598                "tool_version": value.tool_version,
1599                "backend_kind": value.backend_kind.as_str(),
1600                "approval_state": value.approval_state.as_str(),
1601            }),
1602        }
1603    }
1604}
1605
1606impl From<&ForgeToolReceiptV2> for ControlReceipt {
1607    fn from(value: &ForgeToolReceiptV2) -> Self {
1608        let citation = V25CitationContext::missing();
1609        let citation_status = citation.context_status();
1610        let obligation_refs = V25ControlObligationRefs::missing();
1611        let obligation_refs_status = obligation_refs.context_status();
1612        Self {
1613            schema_version: CONTROL_RECEIPT_V1_SCHEMA.into(),
1614            receipt_id: ControlReceiptId::generate(),
1615            action_kind: ControlActionKind::ToolExecution,
1616            case_id: None,
1617            plan_id: None,
1618            trace_ctx: value.trace_ctx.clone(),
1619            attempt_id: value.attempt_id.clone(),
1620            trial_id: Some(value.trial_id.clone()),
1621            actor: value.tool_name.clone(),
1622            planner_stage: Some(value.planner_stage.clone()),
1623            started_at: value.started_at.clone(),
1624            finished_at: value.finished_at.clone(),
1625            citation,
1626            obligation_refs,
1627            citation_status,
1628            obligation_refs_status,
1629            deadline: value.deadline.clone(),
1630            workload_class: value.workload_class.clone(),
1631            budget_context: value
1632                .budget_context
1633                .as_ref()
1634                .map(|budget| ReceiptBudgetContext {
1635                    budget_kind: budget.budget_kind.clone(),
1636                    max_steps: budget.max_steps,
1637                    time_budget_ms: budget.time_budget_ms,
1638                    cost_budget_units: budget.cost_budget_units,
1639                }),
1640            parent_receipt_id: value
1641                .parent_receipt_id
1642                .as_ref()
1643                .map(|id| ControlReceiptId::new(id.clone())),
1644            family_receipt_id: value
1645                .family_receipt_id
1646                .as_ref()
1647                .map(|id| ControlReceiptId::new(id.clone())),
1648            replay_parent_receipt_id: value
1649                .replay_parent_receipt_id
1650                .as_ref()
1651                .map(|id| ControlReceiptId::new(id.clone())),
1652            retry_owner: Some(match value.retry_owner.as_str() {
1653                "llm_pipeline" => ControlRetryOwnerV1::LlmPipeline,
1654                "agent_graph" => ControlRetryOwnerV1::AgentGraph,
1655                "job_queue" => ControlRetryOwnerV1::JobQueue,
1656                "ai_batch_queue" => ControlRetryOwnerV1::AiBatchQueue,
1657                "forge_orchestration" => ControlRetryOwnerV1::ForgeOrchestration,
1658                _ => ControlRetryOwnerV1::External,
1659            }),
1660            non_authoritative: true,
1661            advisory_only: false,
1662            degraded: false,
1663            promotable: false,
1664            source_receipt_kind: "semantic_memory_forge.forge_tool_receipt_v2".into(),
1665            source_receipt_id: Some(value.receipt_id.clone()),
1666            details: json!({
1667                "tool_name": value.tool_name,
1668                "tool_version": value.tool_version,
1669                "backend_kind": value.backend_kind,
1670                "approval_state": value.approval_state,
1671            }),
1672        }
1673    }
1674}
1675
1676/// An event in the verification case ledger.
1677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1678#[serde(tag = "event_type", rename_all = "snake_case")]
1679#[allow(clippy::large_enum_variant)]
1680pub enum LedgerEvent {
1681    CaseOpened {
1682        case: VerificationCase,
1683    },
1684    PlanAdopted {
1685        plan: CheckPlan,
1686    },
1687    AttemptRecorded {
1688        attempt: VerificationAttempt,
1689    },
1690    ReceiptAppended {
1691        receipt: Box<ControlReceipt>,
1692    },
1693    CaseClosed {
1694        case_id: VerificationCaseId,
1695        disposition: TerminalDisposition,
1696    },
1697}
1698
1699/// A sequenced, timestamped entry in the verification case ledger.
1700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1701pub struct LedgerEntry {
1702    pub schema_version: String,
1703    pub entry_id: LedgerEntryId,
1704    pub case_id: VerificationCaseId,
1705    pub sequence_no: u64,
1706    pub recorded_at: String,
1707    pub event: LedgerEvent,
1708}
1709
1710impl LedgerEntry {
1711    /// Appends a timestamped ledger event for a single verification case.
1712    pub fn new(case_id: VerificationCaseId, sequence_no: u64, event: LedgerEvent) -> Self {
1713        Self {
1714            schema_version: LEDGER_ENTRY_V1_SCHEMA.into(),
1715            entry_id: LedgerEntryId::generate(),
1716            case_id,
1717            sequence_no,
1718            recorded_at: chrono::Utc::now().to_rfc3339(),
1719            event,
1720        }
1721    }
1722}
1723
1724/// Aggregate state reconstructed by replaying a verification case ledger.
1725#[derive(Debug, Clone, PartialEq, Eq)]
1726pub struct ReplayedCaseState {
1727    pub case: Option<VerificationCase>,
1728    pub plan_count: usize,
1729    pub attempt_count: usize,
1730    pub receipt_count: usize,
1731    pub terminal_disposition: Option<TerminalDisposition>,
1732}
1733
1734/// Maps a check method onto the scheduler workload class used for budgeting and routing.
1735pub fn infer_workload_class(method: CheckMethod) -> VerificationWorkloadClass {
1736    match method {
1737        CheckMethod::PairedPatch => VerificationWorkloadClass::Patch,
1738        CheckMethod::AdvisoryOnly => VerificationWorkloadClass::Advisory,
1739        _ => VerificationWorkloadClass::Oracle,
1740    }
1741}
1742
1743/// Combines policy, calibration, degradation, budget, and method outcome into promotion eligibility.
1744pub fn interpret_promotion_eligibility(
1745    policy_allows_promotion: bool,
1746    calibration_forces_advisory_only: bool,
1747    degraded: bool,
1748    budget_exhausted: bool,
1749    method_succeeded: bool,
1750) -> bool {
1751    policy_allows_promotion
1752        && !calibration_forces_advisory_only
1753        && !degraded
1754        && !budget_exhausted
1755        && method_succeeded
1756}
1757
1758/// Returns true when a refuted artifact must be rolled back from an already promoted state.
1759pub fn interpret_rollback_requirement(refuted: bool, currently_promoted: bool) -> bool {
1760    refuted && currently_promoted
1761}
1762
1763/// Builds the scheduler decision record that preserves budget and degradation lineage.
1764pub fn schedule_check_plan(
1765    case: &VerificationCase,
1766    plan: &CheckPlan,
1767    budget_lineage: BudgetLineage,
1768    degradation_markers: Vec<DegradationMarker>,
1769    queue_hops: Vec<QueueHop>,
1770) -> SchedulerDecision {
1771    let exactness_budget = Some(build_exactness_budget(
1772        plan,
1773        &budget_lineage,
1774        &degradation_markers,
1775    ));
1776    let proof_blocked = !plan.proof_obligations_remaining.is_empty()
1777        || matches!(
1778            plan.evidence_admissibility,
1779            EvidenceAdmissibilityV1::Inadmissible | EvidenceAdmissibilityV1::Unknown
1780        );
1781
1782    SchedulerDecision {
1783        schema_version: SCHEDULER_DECISION_V1_SCHEMA.into(),
1784        case_id: case.case_id.clone(),
1785        plan_id: plan.plan_id.clone(),
1786        workload_class: infer_workload_class(plan.method),
1787        cheapest_adequate: plan.cheapest_adequate,
1788        exactness_budget,
1789        promotion_blocked: budget_lineage.exhausted
1790            || degradation_markers
1791                .iter()
1792                .any(|marker| marker.blocks_promotion)
1793            || proof_blocked,
1794        queue_hops,
1795        budget_lineage,
1796        degradation_markers,
1797    }
1798}
1799
1800fn build_exactness_budget(
1801    plan: &CheckPlan,
1802    budget_lineage: &BudgetLineage,
1803    degradation_markers: &[DegradationMarker],
1804) -> ExactnessBudgetV1 {
1805    let degradation = if degradation_markers.is_empty() && !plan.degraded {
1806        Vec::new()
1807    } else {
1808        let mut mapped = degradation_markers
1809            .iter()
1810            .map(|marker| match marker.kind.as_str() {
1811                "thin_export" => DegradationKindV1::ThinExport,
1812                "missing_proof" => DegradationKindV1::MissingProof,
1813                "missing_replay" => DegradationKindV1::MissingReplay,
1814                "budget_exhausted" => DegradationKindV1::BudgetExceeded,
1815                _ => DegradationKindV1::AdvisoryOnly,
1816            })
1817            .collect::<Vec<_>>();
1818        if plan.degraded && !mapped.contains(&DegradationKindV1::ExactnessDowngraded) {
1819            mapped.push(DegradationKindV1::ExactnessDowngraded);
1820        }
1821        mapped
1822    };
1823    let budget_units = budget_lineage
1824        .max_cost_budget_units
1825        .unwrap_or_else(|| budget_lineage.max_time_budget_ms.unwrap_or_default());
1826    let remaining_units = budget_lineage
1827        .remaining_cost_budget_units
1828        .or(budget_lineage.remaining_time_budget_ms)
1829        .unwrap_or_default();
1830    let units_consumed = budget_units.saturating_sub(remaining_units);
1831    ExactnessBudgetV1 {
1832        schema_version: semantic_memory_forge::EXACTNESS_BUDGET_V1_SCHEMA.into(),
1833        exactness_budget_id: stack_ids::ExactnessBudgetId::generate(),
1834        semantics_profile_id: stack_ids::SemanticsProfileId::new("semantics-profile:control-plane"),
1835        subject: plan.plan_id.to_string(),
1836        requested_exactness: plan.target_exactness.clone(),
1837        achieved_exactness: if plan.degraded {
1838            ExactnessLevelV1::Conservative
1839        } else {
1840            plan.target_exactness.clone()
1841        },
1842        budget_units,
1843        units_consumed,
1844        degradation,
1845        escalation_rules: vec![ExactnessEscalationRuleV1 {
1846            when_degraded: DegradationKindV1::ExactnessDowngraded,
1847            escalate_to: plan.target_exactness.clone(),
1848            block_action: Some("promotion".into()),
1849        }],
1850        failure_artifact_refs: plan
1851            .proof_obligations_remaining
1852            .iter()
1853            .map(|obligation| format!("proof-obligation:{obligation}"))
1854            .collect(),
1855    }
1856}
1857
1858/// Replays a case ledger in sequence order and reconstructs the terminal aggregate state.
1859pub fn replay_case(entries: &[LedgerEntry]) -> Result<ReplayedCaseState, String> {
1860    let mut ordered = entries.to_vec();
1861    ordered.sort_by_key(|entry| entry.sequence_no);
1862
1863    let mut case = None;
1864    let mut plan_count = 0;
1865    let mut attempt_count = 0;
1866    let mut receipt_count = 0;
1867    let mut terminal_disposition = None;
1868    let mut expected_case_id = None::<VerificationCaseId>;
1869
1870    for entry in ordered {
1871        if let Some(expected) = &expected_case_id {
1872            if entry.case_id != *expected {
1873                return Err("ledger replay encountered mixed case ids".into());
1874            }
1875        } else {
1876            expected_case_id = Some(entry.case_id.clone());
1877        }
1878
1879        match entry.event {
1880            LedgerEvent::CaseOpened { case: opened_case } => case = Some(opened_case),
1881            LedgerEvent::PlanAdopted { .. } => plan_count += 1,
1882            LedgerEvent::AttemptRecorded { .. } => attempt_count += 1,
1883            LedgerEvent::ReceiptAppended { .. } => receipt_count += 1,
1884            LedgerEvent::CaseClosed { disposition, .. } => {
1885                terminal_disposition = Some(disposition.clone());
1886                if let Some(open_case) = case.take() {
1887                    case = Some(open_case.close(disposition));
1888                }
1889            }
1890        }
1891    }
1892
1893    Ok(ReplayedCaseState {
1894        case,
1895        plan_count,
1896        attempt_count,
1897        receipt_count,
1898        terminal_disposition,
1899    })
1900}
1901
1902/// Converts retry ownership into the stable label used on control-plane artifacts.
1903pub fn retry_owner_label(retry_owner: &ToolRetryOwner) -> &'static str {
1904    match retry_owner {
1905        ToolRetryOwner::LlmPipeline => "llm_pipeline",
1906        ToolRetryOwner::AgentGraph => "agent_graph",
1907        ToolRetryOwner::JobQueue => "job_queue",
1908        ToolRetryOwner::AiBatchQueue => "ai_batch_queue",
1909        ToolRetryOwner::ForgeOrchestration => "forge_orchestration",
1910        ToolRetryOwner::External => "external",
1911    }
1912}
1913
1914#[cfg(test)]
1915#[path = "lib_tests.rs"]
1916mod tests;