1#![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#[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#[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 pub fn is_complete(self) -> bool {
99 matches!(self, Self::Complete)
100 }
101}
102
103impl V25CitationContext {
104 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 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#[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 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 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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#[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#[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 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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[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#[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 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#[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
1734pub 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
1743pub 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
1758pub fn interpret_rollback_requirement(refuted: bool, currently_promoted: bool) -> bool {
1760 refuted && currently_promoted
1761}
1762
1763pub 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 °radation_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
1858pub 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
1902pub 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;