Skip to main content

icydb_core/db/integrity/
job.rs

1//! Module: db::integrity::job
2//! Responsibility: invariant-bearing Deep job, checkpoint, and receipt vocabulary.
3//! Does not own: stable storage, physical traversal, authorization, or time.
4//! Boundary: Deep controller <-> current-form progress record codec.
5
6use crate::db::{
7    codec::hex::decode_hex_nibble,
8    encode_hex_lower,
9    integrity::{
10        DatabaseIncarnationId, IntegrityAuthorityDiagnostic, IntegrityEntityIdentity,
11        IntegrityFinding, IntegrityFindingKind, IntegrityPhase, IntegrityProofVector,
12        IntegrityResourceDiagnostic, IntegrityVerifierFamily, MAX_INTEGRITY_PATH_BYTES,
13        PhysicalUnitCheckpoint,
14    },
15    journal::JournalInspectionCheckpoint,
16    schema::MAX_ACCEPTED_TARGET_PATH_COMPONENTS,
17};
18use crate::error::{ConstraintValuePath, ConstraintValuePathComponent, InternalError};
19
20use icydb_diagnostic_code::{DiagnosticDetail, RuntimeBoundaryCode};
21use serde::Deserialize;
22
23pub(in crate::db) const MAX_INTEGRITY_OWNER_BYTES: usize = 256;
24pub(in crate::db) const MAX_INTEGRITY_SUBMISSION_KEY_BYTES: usize = 256;
25pub(super) const MAX_INTEGRITY_RECEIPT_FINDINGS: usize = 64;
26pub(in crate::db) const MAX_INTEGRITY_IN_PROGRESS_PAGES: u64 = u64::MAX - 1;
27
28/// Opaque lookup identity for one retained Deep inspection job.
29
30#[derive(
31    serde::Serialize, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd,
32)]
33#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
34pub struct IntegrityJobId([u8; 32]);
35
36impl IntegrityJobId {
37    /// Admit one nonzero current-form lookup identity.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`IntegrityJobError::CorruptProgressRecord`] when `bytes` is
42    /// the reserved all-zero identity.
43    pub fn try_from_bytes(bytes: [u8; 32]) -> Result<Self, IntegrityJobError> {
44        if bytes == [0; 32] {
45            return Err(IntegrityJobError::CorruptProgressRecord);
46        }
47        Ok(Self(bytes))
48    }
49
50    /// Decode one exact lowercase or uppercase hexadecimal job identity.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`IntegrityJobError::InvalidJobId`] unless `value` contains
55    /// exactly 64 hexadecimal characters and decodes to a nonzero identity.
56    pub fn try_from_hex(value: &str) -> Result<Self, IntegrityJobError> {
57        if value.len() != 64 {
58            return Err(IntegrityJobError::InvalidJobId);
59        }
60
61        let mut bytes = [0_u8; 32];
62        for (index, pair) in value.as_bytes().as_chunks::<2>().0.iter().enumerate() {
63            let high = decode_hex_nibble(pair[0]).ok_or(IntegrityJobError::InvalidJobId)?;
64            let low = decode_hex_nibble(pair[1]).ok_or(IntegrityJobError::InvalidJobId)?;
65            bytes[index] = (high << 4) | low;
66        }
67        if bytes == [0; 32] {
68            return Err(IntegrityJobError::InvalidJobId);
69        }
70
71        Ok(Self(bytes))
72    }
73
74    /// Return the canonical lookup bytes.
75    #[must_use]
76    pub const fn to_bytes(self) -> [u8; 32] {
77        self.0
78    }
79
80    /// Render the canonical lowercase hexadecimal SQL/shell identity.
81    #[must_use]
82    pub fn to_hex(self) -> String {
83        encode_hex_lower(&self.0)
84    }
85
86    /// Revalidate a possibly deserialized job identity before lookup.
87    pub(in crate::db) fn validate(self) -> Result<(), IntegrityJobError> {
88        if self.0 == [0; 32] {
89            return Err(IntegrityJobError::InvalidJobId);
90        }
91        Ok(())
92    }
93}
94
95/// Bounded authorization identity persisted with one job.
96
97#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
98pub struct IntegrityJobOwner(String);
99
100impl IntegrityJobOwner {
101    /// Admit one nonempty bounded owner identity.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`IntegrityJobError::InvalidOwner`] when the identity is empty
106    /// or exceeds the protocol bound.
107    pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
108        let value = value.into();
109        if value.is_empty() || value.len() > MAX_INTEGRITY_OWNER_BYTES {
110            return Err(IntegrityJobError::InvalidOwner);
111        }
112        Ok(Self(value))
113    }
114
115    /// Borrow the canonical owner identity.
116    #[must_use]
117    pub const fn as_str(&self) -> &str {
118        self.0.as_str()
119    }
120
121    /// Revalidate a possibly deserialized owner before authorization.
122    pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
123        if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_OWNER_BYTES {
124            return Err(IntegrityJobError::InvalidOwner);
125        }
126        Ok(())
127    }
128}
129
130/// Bounded client idempotency identity for Deep start.
131
132#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
133pub struct IntegritySubmissionKey(String);
134
135impl IntegritySubmissionKey {
136    /// Admit one nonempty bounded submission key.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`IntegrityJobError::InvalidSubmissionKey`] when the key is
141    /// empty or exceeds the protocol bound.
142    pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
143        let value = value.into();
144        if value.is_empty() || value.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
145            return Err(IntegrityJobError::InvalidSubmissionKey);
146        }
147        Ok(Self(value))
148    }
149
150    /// Borrow the canonical submission key.
151    #[must_use]
152    pub const fn as_str(&self) -> &str {
153        self.0.as_str()
154    }
155
156    /// Revalidate a possibly deserialized key before job identity derivation.
157    pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
158        if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
159            return Err(IntegrityJobError::InvalidSubmissionKey);
160        }
161        Ok(())
162    }
163}
164
165/// Exact private continuation for the current Deep phase.
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub(in crate::db) enum IntegrityCheckpoint {
168    /// Bounded accepted metadata and control closure.
169    QuickMetadata,
170    /// Canonical source-row interval.
171    Rows(PhysicalUnitCheckpoint),
172    /// One active forward-index domain in accepted plan order.
173    Index {
174        ordinal: u32,
175        checkpoint: PhysicalUnitCheckpoint,
176    },
177    /// One active source-owned reverse domain in accepted plan order.
178    ReverseRelation {
179        ordinal: u32,
180        checkpoint: PhysicalUnitCheckpoint,
181    },
182    /// One participating journal tail in canonical store order.
183    Journal {
184        store_ordinal: u32,
185        checkpoint: JournalInspectionCheckpoint,
186    },
187    /// No physical traversal remains; only final proof equality may complete.
188    FinalProof,
189}
190
191impl IntegrityCheckpoint {
192    /// Return the canonical phase implied by this checkpoint.
193    #[must_use]
194    pub(in crate::db) const fn phase(&self) -> IntegrityPhase {
195        match self {
196            Self::QuickMetadata => IntegrityPhase::QuickMetadata,
197            Self::Rows(_) => IntegrityPhase::Rows,
198            Self::Index { .. } => IntegrityPhase::IndexEntries,
199            Self::ReverseRelation { .. } => IntegrityPhase::ReverseRelations,
200            Self::Journal { .. } => IntegrityPhase::JournalTails,
201            Self::FinalProof => IntegrityPhase::FinalProofVectorCheck,
202        }
203    }
204}
205
206/// Frozen intent that supersedes advancement after the outstanding page is acknowledged.
207
208#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
209#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
210pub enum IntegrityPendingTerminal {
211    /// The inactivity lease elapsed.
212    Expired,
213    /// The authorized owner requested abort.
214    Aborted,
215}
216
217/// Stable terminal meaning of a completed Deep job.
218
219#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
220#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
221pub enum IntegrityTerminalOutcome {
222    /// Every phase exhausted cleanly under one unchanged proof.
223    DeepCompleteClean,
224    /// Every phase exhausted with one or more definite findings.
225    DeepCompleteWithFindings,
226    /// One proof component changed before completion.
227    Invalidated,
228    /// Accepted authority could not be inspected.
229    Uninspectable(IntegrityAuthorityDiagnostic),
230    /// One frozen bounded resource was insufficient.
231    ResourceLimited(IntegrityResourceDiagnostic),
232    /// The inactivity lease expired.
233    Expired,
234    /// The authorized owner aborted the job.
235    Aborted,
236}
237
238impl IntegrityTerminalOutcome {
239    /// Classify a failed inspection without confusing exhaustion with invalid
240    /// authority. The exact typed boundary, not the broad Unsupported class,
241    /// distinguishes existing relation and construction budget failures.
242    pub(in crate::db::integrity) fn from_internal(error: &InternalError) -> Self {
243        if matches!(
244            error.diagnostic().detail(),
245            Some(DiagnosticDetail::RuntimeBoundary {
246                boundary: RuntimeBoundaryCode::ExecutionBudgetExceeded
247                    | RuntimeBoundaryCode::PageUnitTooLarge,
248            })
249        ) {
250            return Self::ResourceLimited(IntegrityResourceDiagnostic {
251                diagnostic_code: error.diagnostic_code().error_code().raw(),
252            });
253        }
254
255        Self::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(error))
256    }
257}
258
259/// Durable job lifecycle state.
260#[derive(Clone, Debug, Eq, PartialEq)]
261pub(in crate::db) enum IntegrityJobState {
262    /// Physical advancement remains permitted.
263    InProgress,
264    /// Advancement is frozen while the last page remains unacknowledged.
265    TerminalPending(IntegrityPendingTerminal),
266    /// One final receipt is retained for replay and acknowledgement.
267    Terminal {
268        outcome: IntegrityTerminalOutcome,
269        receipt_acknowledged: bool,
270    },
271}
272
273/// Semantic status carried by one bounded Deep page.
274
275#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
276#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
277pub enum DeepIntegrityPageStatus {
278    /// More physical work or the final proof check remains.
279    InProgress,
280    /// This receipt records the stable terminal result.
281    Terminal(IntegrityTerminalOutcome),
282}
283
284/// One bounded replayable Deep result page.
285
286#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
287#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
288pub struct DeepIntegrityPage {
289    pub(super) job_id: IntegrityJobId,
290    pub(super) page_sequence: u64,
291    pub(super) phase: IntegrityPhase,
292    pub(super) status: DeepIntegrityPageStatus,
293    pub(super) pages_completed: u64,
294    pub(super) findings_seen: u64,
295    pub(super) findings: Vec<IntegrityFinding>,
296    pub(super) blocked_verifier_families: Vec<IntegrityVerifierFamily>,
297}
298
299impl DeepIntegrityPage {
300    /// Return the opaque job lookup identity.
301    #[must_use]
302    pub const fn job_id(&self) -> IntegrityJobId {
303        self.job_id
304    }
305
306    /// Return the monotonically increasing receipt sequence.
307    #[must_use]
308    pub const fn page_sequence(&self) -> u64 {
309        self.page_sequence
310    }
311
312    /// Return the phase represented by this receipt.
313    #[must_use]
314    pub const fn phase(&self) -> IntegrityPhase {
315        self.phase
316    }
317
318    /// Borrow the current or terminal status.
319    #[must_use]
320    pub const fn status(&self) -> &DeepIntegrityPageStatus {
321        &self.status
322    }
323
324    /// Return cumulative successfully persisted page count.
325    #[must_use]
326    pub const fn pages_completed(&self) -> u64 {
327        self.pages_completed
328    }
329
330    /// Return cumulative definite findings.
331    #[must_use]
332    pub const fn findings_seen(&self) -> u64 {
333        self.findings_seen
334    }
335
336    /// Borrow findings produced only by this page.
337    #[must_use]
338    pub const fn findings(&self) -> &[IntegrityFinding] {
339        self.findings.as_slice()
340    }
341
342    /// Borrow the cumulative canonical blocked-family set.
343    #[must_use]
344    pub const fn blocked_verifier_families(&self) -> &[IntegrityVerifierFamily] {
345        self.blocked_verifier_families.as_slice()
346    }
347}
348
349/// Abort receipt status.
350
351#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
352#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
353pub enum IntegrityAbortStatus {
354    /// Abort is frozen but cannot replace the outstanding page yet.
355    TerminationPending(IntegrityPendingTerminal),
356    /// The terminal abort result is replayable.
357    Terminal(IntegrityTerminalOutcome),
358}
359
360/// One bounded abort/expiry receipt.
361
362#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
363#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
364pub struct IntegrityAbortReceipt {
365    pub(super) job_id: IntegrityJobId,
366    pub(super) page_sequence: u64,
367    pub(super) status: IntegrityAbortStatus,
368}
369
370impl IntegrityAbortReceipt {
371    /// Return the opaque job identity.
372    #[must_use]
373    pub const fn job_id(&self) -> IntegrityJobId {
374        self.job_id
375    }
376
377    /// Return the outstanding or terminal receipt sequence.
378    #[must_use]
379    pub const fn page_sequence(&self) -> u64 {
380        self.page_sequence
381    }
382
383    /// Borrow the pending or terminal abort status.
384    #[must_use]
385    pub const fn status(&self) -> &IntegrityAbortStatus {
386        &self.status
387    }
388}
389
390/// Persisted receipt body.
391
392#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
393#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
394pub enum IntegrityJobReceipt {
395    /// Normal start, advancement, or completion page.
396    Page(DeepIntegrityPage),
397    /// Abort/expiry terminal or pending acknowledgement.
398    Abort(IntegrityAbortReceipt),
399}
400
401impl IntegrityJobReceipt {
402    /// Return the job identity carried by this receipt.
403    #[must_use]
404    pub const fn job_id(&self) -> IntegrityJobId {
405        match self {
406            Self::Page(page) => page.job_id,
407            Self::Abort(receipt) => receipt.job_id,
408        }
409    }
410
411    /// Return the sequence carried by this receipt.
412    #[must_use]
413    pub const fn page_sequence(&self) -> u64 {
414        match self {
415            Self::Page(page) => page.page_sequence,
416            Self::Abort(receipt) => receipt.page_sequence,
417        }
418    }
419}
420
421/// Request identity that produced the cached receipt.
422#[derive(Clone, Copy, Debug, Eq, PartialEq)]
423pub(in crate::db) enum IntegrityReceiptReplayKey {
424    /// Initial Deep start.
425    Start,
426    /// Continue request acknowledging the named prior sequence.
427    Continue { acknowledged_sequence: u64 },
428}
429
430/// One cached bounded receipt and its exact replay request.
431#[derive(Clone, Debug, Eq, PartialEq)]
432pub(in crate::db) struct IntegrityReceiptEnvelope {
433    pub(super) replay_key: IntegrityReceiptReplayKey,
434    pub(super) receipt: IntegrityJobReceipt,
435}
436
437/// Current invariant-bearing durable Deep record.
438#[derive(Clone, Debug, Eq, PartialEq)]
439pub(in crate::db) struct IntegrityJob {
440    pub(super) id: IntegrityJobId,
441    pub(super) database_incarnation_id: DatabaseIncarnationId,
442    pub(super) owner: IntegrityJobOwner,
443    pub(super) submission_key: IntegritySubmissionKey,
444    pub(super) entity: IntegrityEntityIdentity,
445    pub(super) accepted_schema_version: u32,
446    pub(super) accepted_schema_fingerprint: [u8; 16],
447    pub(super) inspection_plan_fingerprint: [u8; 32],
448    pub(super) checkpoint: IntegrityCheckpoint,
449    pub(super) captured_proof_vector: IntegrityProofVector,
450    pub(super) state: IntegrityJobState,
451    pub(super) lease_deadline_nanos: u64,
452    pub(super) findings_seen: u64,
453    pub(super) pages_completed: u64,
454    pub(super) blocked_verifier_families: Vec<IntegrityVerifierFamily>,
455    pub(super) last_receipt: IntegrityReceiptEnvelope,
456}
457
458impl IntegrityJob {
459    /// Validate all persisted cross-field invariants before use.
460    pub(super) fn validate(&self) -> Result<(), IntegrityJobError> {
461        if self.id != self.last_receipt.receipt.job_id()
462            || self.database_incarnation_id != self.captured_proof_vector.database_incarnation_id()
463            || self.accepted_schema_version != self.captured_proof_vector.accepted_schema_version()
464            || self.accepted_schema_fingerprint
465                != self.captured_proof_vector.accepted_schema_fingerprint()
466            || self.inspection_plan_fingerprint
467                != self.captured_proof_vector.inspection_plan_fingerprint()
468            || self.entity.entity_path().is_empty()
469            || self.entity.entity_path().len() > MAX_INTEGRITY_PATH_BYTES
470            || self.entity.store_path().is_empty()
471            || self.entity.store_path().len() > MAX_INTEGRITY_PATH_BYTES
472            || self.entity.entity_tag() == 0
473            || self.owner.as_str().is_empty()
474            || self.owner.as_str().len() > MAX_INTEGRITY_OWNER_BYTES
475            || self.submission_key.as_str().is_empty()
476            || self.submission_key.as_str().len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES
477            || self.accepted_schema_version == 0
478            || self.lease_deadline_nanos == 0
479            || matches!(
480                self.state,
481                IntegrityJobState::InProgress | IntegrityJobState::TerminalPending(_)
482            ) && self.pages_completed > MAX_INTEGRITY_IN_PROGRESS_PAGES
483            || !strictly_sorted_unique(&self.blocked_verifier_families)
484            || self.captured_proof_vector.validate().is_err()
485            || !self.checkpoint_is_well_formed()
486        {
487            return Err(IntegrityJobError::CorruptProgressRecord);
488        }
489
490        let receipt_matches_state = match (&self.state, &self.last_receipt.receipt) {
491            (
492                IntegrityJobState::InProgress | IntegrityJobState::TerminalPending(_),
493                IntegrityJobReceipt::Page(page),
494            ) => {
495                page.status == DeepIntegrityPageStatus::InProgress
496                    && page.phase == self.checkpoint.phase()
497                    && self.page_matches_counters(page)
498            }
499            (IntegrityJobState::Terminal { outcome, .. }, IntegrityJobReceipt::Page(page)) => {
500                page.status == DeepIntegrityPageStatus::Terminal(outcome.clone())
501                    && page.phase == self.checkpoint.phase()
502                    && !matches!(
503                        outcome,
504                        IntegrityTerminalOutcome::Expired | IntegrityTerminalOutcome::Aborted
505                    )
506                    && self.page_matches_counters(page)
507            }
508            (IntegrityJobState::Terminal { outcome, .. }, IntegrityJobReceipt::Abort(receipt)) => {
509                receipt.status == IntegrityAbortStatus::Terminal(outcome.clone())
510                    && matches!(
511                        outcome,
512                        IntegrityTerminalOutcome::Expired | IntegrityTerminalOutcome::Aborted
513                    )
514            }
515            _ => false,
516        };
517        if !receipt_matches_state
518            || self.last_receipt.receipt.page_sequence() != self.pages_completed
519            || !self.replay_key_matches_receipt()
520            || !self.terminal_outcome_matches_counts()
521        {
522            return Err(IntegrityJobError::CorruptProgressRecord);
523        }
524
525        Ok(())
526    }
527
528    fn page_matches_counters(&self, page: &DeepIntegrityPage) -> bool {
529        page.pages_completed == self.pages_completed
530            && page.findings_seen == self.findings_seen
531            && page.findings.len() <= MAX_INTEGRITY_RECEIPT_FINDINGS
532            && u64::try_from(page.findings.len()).is_ok_and(|count| count <= self.findings_seen)
533            && page.findings.iter().all(|finding| {
534                finding.value_path().is_none_or(|path| {
535                    finding.kind() == IntegrityFindingKind::ConstraintViolation
536                        && finding.constraint_id().is_some()
537                        && finding.constraint_name().is_some()
538                        && constraint_value_path_is_well_formed(path)
539                })
540            })
541            && page.blocked_verifier_families == self.blocked_verifier_families
542    }
543
544    fn replay_key_matches_receipt(&self) -> bool {
545        match self.last_receipt.replay_key {
546            IntegrityReceiptReplayKey::Start => {
547                self.pages_completed == 0
548                    && self.last_receipt.receipt.page_sequence() == 0
549                    && matches!(
550                        &self.last_receipt.receipt,
551                        IntegrityJobReceipt::Page(DeepIntegrityPage {
552                            status: DeepIntegrityPageStatus::InProgress,
553                            ..
554                        })
555                    )
556            }
557            IntegrityReceiptReplayKey::Continue {
558                acknowledged_sequence,
559            } => acknowledged_sequence
560                .checked_add(1)
561                .is_some_and(|sequence| sequence == self.last_receipt.receipt.page_sequence()),
562        }
563    }
564
565    const fn terminal_outcome_matches_counts(&self) -> bool {
566        match &self.state {
567            IntegrityJobState::Terminal {
568                outcome: IntegrityTerminalOutcome::DeepCompleteClean,
569                ..
570            } => self.findings_seen == 0 && self.blocked_verifier_families.is_empty(),
571            IntegrityJobState::Terminal {
572                outcome: IntegrityTerminalOutcome::DeepCompleteWithFindings,
573                ..
574            } => self.findings_seen > 0 || !self.blocked_verifier_families.is_empty(),
575            _ => true,
576        }
577    }
578
579    fn checkpoint_is_well_formed(&self) -> bool {
580        match &self.checkpoint {
581            IntegrityCheckpoint::Rows(checkpoint) => row_checkpoint_is_well_formed(checkpoint),
582            IntegrityCheckpoint::Index {
583                ordinal,
584                checkpoint,
585            } => {
586                usize::try_from(*ordinal).is_ok_and(|ordinal| {
587                    ordinal < self.captured_proof_vector.index_generation_count()
588                }) && index_checkpoint_is_well_formed(checkpoint)
589            }
590            IntegrityCheckpoint::ReverseRelation {
591                ordinal,
592                checkpoint,
593            } => {
594                usize::try_from(*ordinal).is_ok_and(|ordinal| {
595                    ordinal < self.captured_proof_vector.relation_generation_count()
596                }) && reverse_checkpoint_is_well_formed(checkpoint)
597            }
598            IntegrityCheckpoint::Journal {
599                store_ordinal,
600                checkpoint,
601            } => usize::try_from(*store_ordinal)
602                .ok()
603                .and_then(|ordinal| self.captured_proof_vector.stores().get(ordinal))
604                .is_some_and(|proof| {
605                    let (fold_sequence, next_append_sequence) = proof.journal_interval();
606                    journal_checkpoint_is_well_formed(
607                        checkpoint,
608                        fold_sequence,
609                        next_append_sequence,
610                    )
611                }),
612            IntegrityCheckpoint::QuickMetadata | IntegrityCheckpoint::FinalProof => true,
613        }
614    }
615}
616
617fn constraint_value_path_is_well_formed(path: &ConstraintValuePath) -> bool {
618    let Some((first, remaining)) = path.components().split_first() else {
619        return false;
620    };
621    path.components().len() <= MAX_ACCEPTED_TARGET_PATH_COMPONENTS
622        && matches!(
623            first,
624            ConstraintValuePathComponent::RootField { field_id } if *field_id != 0
625        )
626        && remaining.iter().all(|component| match component {
627            ConstraintValuePathComponent::RootField { .. } => false,
628            ConstraintValuePathComponent::RecordMember {
629                composite_type_id,
630                member_id,
631            } => *composite_type_id != 0 && *member_id != 0,
632            ConstraintValuePathComponent::TupleElement {
633                composite_type_id, ..
634            }
635            | ConstraintValuePathComponent::Newtype { composite_type_id } => {
636                *composite_type_id != 0
637            }
638            ConstraintValuePathComponent::EnumVariant {
639                enum_type_id,
640                variant_id,
641            } => *enum_type_id != 0 && *variant_id != 0,
642            ConstraintValuePathComponent::ListElement { .. }
643            | ConstraintValuePathComponent::SetElement { .. }
644            | ConstraintValuePathComponent::MapEntryKey { .. }
645            | ConstraintValuePathComponent::MapEntryValue { .. } => true,
646        })
647}
648
649fn row_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
650    checkpoint.raw_data_key().is_ok()
651        && !matches!(
652            checkpoint,
653            PhysicalUnitCheckpoint::Within {
654                verifier_family: IntegrityVerifierFamily::IndexEntry
655                    | IntegrityVerifierFamily::UniqueIndex
656                    | IntegrityVerifierFamily::ReverseRelationEntry
657                    | IntegrityVerifierFamily::JournalEnvelope
658                    | IntegrityVerifierFamily::JournalBatchIdentity,
659                ..
660            }
661        )
662}
663
664fn index_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
665    checkpoint.raw_index_key().is_ok()
666        && !matches!(
667            checkpoint,
668            PhysicalUnitCheckpoint::Within {
669                verifier_family: IntegrityVerifierFamily::DataKey
670                    | IntegrityVerifierFamily::RowEnvelope
671                    | IntegrityVerifierFamily::FieldValue
672                    | IntegrityVerifierFamily::PrimaryKey
673                    | IntegrityVerifierFamily::IdentityState
674                    | IntegrityVerifierFamily::ValidatedConstraints
675                    | IntegrityVerifierFamily::ForwardIndex
676                    | IntegrityVerifierFamily::Relation
677                    | IntegrityVerifierFamily::ReverseRelationEntry
678                    | IntegrityVerifierFamily::JournalEnvelope
679                    | IntegrityVerifierFamily::JournalBatchIdentity,
680                ..
681            }
682        )
683}
684
685fn reverse_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
686    checkpoint.raw_index_key().is_ok()
687        && !matches!(
688            checkpoint,
689            PhysicalUnitCheckpoint::Within {
690                verifier_family: IntegrityVerifierFamily::DataKey
691                    | IntegrityVerifierFamily::RowEnvelope
692                    | IntegrityVerifierFamily::FieldValue
693                    | IntegrityVerifierFamily::PrimaryKey
694                    | IntegrityVerifierFamily::IdentityState
695                    | IntegrityVerifierFamily::ValidatedConstraints
696                    | IntegrityVerifierFamily::ForwardIndex
697                    | IntegrityVerifierFamily::Relation
698                    | IntegrityVerifierFamily::IndexEntry
699                    | IntegrityVerifierFamily::UniqueIndex
700                    | IntegrityVerifierFamily::JournalEnvelope
701                    | IntegrityVerifierFamily::JournalBatchIdentity,
702                ..
703            }
704        )
705}
706
707const fn journal_checkpoint_is_well_formed(
708    checkpoint: &JournalInspectionCheckpoint,
709    fold_sequence: u64,
710    next_append_sequence: u64,
711) -> bool {
712    match checkpoint {
713        JournalInspectionCheckpoint::BeforeFirst => true,
714        JournalInspectionCheckpoint::BeforeBatch { sequence } => {
715            *sequence > fold_sequence && *sequence < next_append_sequence
716        }
717        JournalInspectionCheckpoint::CheckingBatchIdentity {
718            sequence,
719            next_prior_sequence,
720            ..
721        } => {
722            *sequence > fold_sequence
723                && *sequence < next_append_sequence
724                && *next_prior_sequence > fold_sequence
725                && *next_prior_sequence < *sequence
726        }
727        JournalInspectionCheckpoint::AfterBatch { sequence } => {
728            *sequence >= fold_sequence && *sequence < next_append_sequence
729        }
730    }
731}
732
733fn strictly_sorted_unique(values: &[IntegrityVerifierFamily]) -> bool {
734    values.windows(2).all(|pair| pair[0] < pair[1])
735}
736
737/// Typed Deep protocol or progress-record failure.
738
739#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
740#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
741pub enum IntegrityJobError {
742    /// The bounded progress store cannot admit another job.
743    CapacityExceeded,
744
745    /// The progress-store header is malformed.
746    CorruptProgressHeader,
747
748    /// The retained job record violates its current-form contract.
749    CorruptProgressRecord,
750
751    /// A checked protocol counter cannot advance.
752    CounterExhausted,
753
754    /// The accepted entity selector no longer matches runtime authority.
755    EntityIdentityMismatch,
756
757    /// The progress store uses an unsupported persisted format.
758    IncompatibleProgressFormat,
759
760    /// An internal Deep controller operation failed.
761    Internal,
762
763    /// The accepted entity selector is malformed.
764    InvalidEntityIdentity,
765
766    /// A caller-authored job identity is malformed or reserved.
767    InvalidJobId,
768
769    /// The authorization owner is empty or exceeds its bound.
770    InvalidOwner,
771
772    /// The idempotency key is empty or exceeds its bound.
773    InvalidSubmissionKey,
774
775    /// The job belongs to another database incarnation.
776    JobIncarnationMismatch,
777
778    /// No retained job has the supplied identity.
779    JobNotFound,
780
781    /// The supplied authorization owner does not own the job.
782    JobOwnerMismatch,
783
784    /// The acknowledgement does not name the outstanding receipt.
785    StaleAcknowledgement,
786
787    /// The Deep-start proof changed before a job could be published.
788    StartInvalidated,
789
790    /// A retried start names a job that already advanced.
791    SubmissionAlreadyAdvanced,
792
793    /// An owner/key pair was reused for a different target.
794    SubmissionConflict,
795}
796
797/// Deep protocol failures keep persisted-protocol and engine causes distinct.
798
799#[derive(Debug)]
800pub enum IntegrityDeepError {
801    /// Accepted authority or physical execution failed.
802    Internal(crate::error::InternalError),
803
804    /// Stable job/progress protocol rejected the request.
805    Job(IntegrityJobError),
806
807    /// Deep start could identify but not safely load accepted authority.
808    Uninspectable(IntegrityAuthorityDiagnostic),
809}
810
811impl IntegrityDeepError {
812    /// Keep failed plan construction distinct from invalid accepted authority.
813    pub(in crate::db) fn from_plan_load(error: InternalError) -> Self {
814        match IntegrityTerminalOutcome::from_internal(&error) {
815            IntegrityTerminalOutcome::Uninspectable(diagnostic) => Self::Uninspectable(diagnostic),
816            // The shared classifier owns resource recognition. Preserve its
817            // original operational error, including all budget facts.
818            _ => Self::Internal(error),
819        }
820    }
821}
822
823impl From<IntegrityJobError> for IntegrityDeepError {
824    fn from(error: IntegrityJobError) -> Self {
825        Self::Job(error)
826    }
827}
828
829impl From<crate::error::InternalError> for IntegrityDeepError {
830    fn from(error: crate::error::InternalError) -> Self {
831        Self::Internal(error)
832    }
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838    use crate::error::{ErrorClass, ErrorOrigin};
839    use icydb_diagnostic_code::DiagnosticExecutionBudgetResource as Resource;
840
841    #[test]
842    fn inspection_resource_failures_keep_their_typed_terminal_and_wire_code() {
843        use crate::db::{
844            resource::budget::MaintenanceConstructionBudget,
845            resource::construction::ConstructionBudget,
846        };
847        let work = MaintenanceConstructionBudget::with_limit_for_tests(Resource::TemporaryBytes, 0);
848        for error in [
849            work.charge(Resource::TemporaryBytes, 1).unwrap_err(),
850            InternalError::relation_budget_exceeded(Resource::NestedValueSteps, 10, 11),
851            InternalError::relation_budget_exceeded(Resource::TemporaryBytes, 10, 11),
852            InternalError::page_unit_too_large(Resource::TemporaryBytes, 10, 11),
853        ] {
854            let expected = error.diagnostic();
855            let outcome = IntegrityTerminalOutcome::from_internal(&error);
856            let IntegrityTerminalOutcome::ResourceLimited(diagnostic) = &outcome else {
857                panic!("resource exhaustion must not become an authority failure");
858            };
859            assert_eq!(
860                diagnostic.diagnostic_code(),
861                error.diagnostic_code().error_code().raw(),
862            );
863            let bytes = icydb_schema::wire::encode_one(&outcome).expect("terminal should encode");
864            let decoded: IntegrityTerminalOutcome =
865                icydb_schema::wire::decode_one(&bytes).expect("terminal should decode");
866            assert_eq!(decoded, outcome);
867            let IntegrityDeepError::Internal(error) = IntegrityDeepError::from_plan_load(error)
868            else {
869                panic!("plan-load exhaustion must retain its operational error");
870            };
871            assert_eq!(error.diagnostic(), expected);
872        }
873    }
874
875    #[test]
876    fn inspection_authority_failures_are_not_classified_as_exhaustion() {
877        for class in [
878            ErrorClass::Corruption,
879            ErrorClass::IncompatiblePersistedFormat,
880            ErrorClass::InvariantViolation,
881            ErrorClass::Unsupported,
882            ErrorClass::NotFound,
883            ErrorClass::Conflict,
884            ErrorClass::Internal,
885        ] {
886            let error = InternalError::classified(class, ErrorOrigin::Store);
887            assert_eq!(
888                IntegrityTerminalOutcome::from_internal(&error),
889                IntegrityTerminalOutcome::Uninspectable(
890                    IntegrityAuthorityDiagnostic::from_internal(&error),
891                ),
892            );
893            let expected = IntegrityAuthorityDiagnostic::from_internal(&error);
894            let IntegrityDeepError::Uninspectable(diagnostic) =
895                IntegrityDeepError::from_plan_load(error)
896            else {
897                panic!("invalid authority keeps its established plan-load classification");
898            };
899            assert_eq!(diagnostic, expected);
900        }
901    }
902
903    #[test]
904    fn public_job_inputs_revalidate_after_wire_decode() {
905        let owner_bytes = icydb_schema::wire::encode_one(IntegrityJobOwner(String::new()))
906            .expect("owner should encode");
907        let owner: IntegrityJobOwner =
908            icydb_schema::wire::decode_one(&owner_bytes).expect("owner should decode");
909        assert_eq!(owner.validate(), Err(IntegrityJobError::InvalidOwner));
910
911        let submission_bytes =
912            icydb_schema::wire::encode_one(IntegritySubmissionKey(String::new()))
913                .expect("submission key should encode");
914        let submission: IntegritySubmissionKey = icydb_schema::wire::decode_one(&submission_bytes)
915            .expect("submission key should decode");
916        assert_eq!(
917            submission.validate(),
918            Err(IntegrityJobError::InvalidSubmissionKey),
919        );
920
921        let job_id_bytes =
922            icydb_schema::wire::encode_one(IntegrityJobId([0; 32])).expect("job id should encode");
923        let job_id: IntegrityJobId =
924            icydb_schema::wire::decode_one(&job_id_bytes).expect("job id should decode");
925        assert_eq!(job_id.validate(), Err(IntegrityJobError::InvalidJobId),);
926    }
927
928    #[test]
929    fn persisted_constraint_value_paths_require_current_accepted_id_shape() {
930        let valid = ConstraintValuePath::new(vec![
931            ConstraintValuePathComponent::RootField { field_id: 1 },
932            ConstraintValuePathComponent::RecordMember {
933                composite_type_id: 2,
934                member_id: 3,
935            },
936            ConstraintValuePathComponent::ListElement { index: 0 },
937        ]);
938        assert!(constraint_value_path_is_well_formed(&valid));
939
940        for malformed in [
941            ConstraintValuePath::new(Vec::new()),
942            ConstraintValuePath::new(vec![ConstraintValuePathComponent::RootField {
943                field_id: 0,
944            }]),
945            ConstraintValuePath::new(vec![
946                ConstraintValuePathComponent::RootField { field_id: 1 },
947                ConstraintValuePathComponent::RootField { field_id: 2 },
948            ]),
949            ConstraintValuePath::new(vec![
950                ConstraintValuePathComponent::RootField { field_id: 1 },
951                ConstraintValuePathComponent::Newtype {
952                    composite_type_id: 0,
953                },
954            ]),
955        ] {
956            assert!(!constraint_value_path_is_well_formed(&malformed));
957        }
958    }
959
960    #[test]
961    fn public_job_id_hex_round_trip_is_exact_and_fail_closed() {
962        let mut bytes = [0_u8; 32];
963        bytes[0] = 0x01;
964        bytes[31] = 0xfe;
965        let job_id = IntegrityJobId::try_from_bytes(bytes).expect("job id should admit");
966        let encoded = job_id.to_hex();
967
968        assert_eq!(encoded.len(), 64);
969        assert_eq!(IntegrityJobId::try_from_hex(encoded.as_str()), Ok(job_id),);
970        assert_eq!(
971            IntegrityJobId::try_from_hex(encoded.to_uppercase().as_str()),
972            Ok(job_id),
973        );
974        for malformed in [
975            "",
976            "01",
977            "0000000000000000000000000000000000000000000000000000000000000000",
978            "g001000000000000000000000000000000000000000000000000000000000000",
979        ] {
980            assert_eq!(
981                IntegrityJobId::try_from_hex(malformed),
982                Err(IntegrityJobError::InvalidJobId),
983            );
984        }
985    }
986
987    #[test]
988    fn persisted_checkpoint_families_stay_phase_owned() {
989        let journal_in_row = PhysicalUnitCheckpoint::Within {
990            physical_key: vec![1],
991            verifier_family: IntegrityVerifierFamily::JournalEnvelope,
992            ordinal: 0,
993        };
994        let row_in_index = PhysicalUnitCheckpoint::Within {
995            physical_key: vec![1],
996            verifier_family: IntegrityVerifierFamily::FieldValue,
997            ordinal: 0,
998        };
999        let reverse_in_reverse = PhysicalUnitCheckpoint::Within {
1000            physical_key: vec![1],
1001            verifier_family: IntegrityVerifierFamily::ReverseRelationEntry,
1002            ordinal: 0,
1003        };
1004
1005        assert!(!row_checkpoint_is_well_formed(&journal_in_row));
1006        assert!(!index_checkpoint_is_well_formed(&row_in_index));
1007        assert!(reverse_checkpoint_is_well_formed(&reverse_in_reverse));
1008    }
1009
1010    #[test]
1011    fn persisted_journal_checkpoint_cannot_skip_the_captured_tail_interval() {
1012        assert!(journal_checkpoint_is_well_formed(
1013            &JournalInspectionCheckpoint::BeforeFirst,
1014            4,
1015            8,
1016        ));
1017        assert!(journal_checkpoint_is_well_formed(
1018            &JournalInspectionCheckpoint::BeforeBatch { sequence: 7 },
1019            4,
1020            8,
1021        ));
1022        assert!(journal_checkpoint_is_well_formed(
1023            &JournalInspectionCheckpoint::AfterBatch { sequence: 4 },
1024            4,
1025            8,
1026        ));
1027        assert!(!journal_checkpoint_is_well_formed(
1028            &JournalInspectionCheckpoint::BeforeBatch { sequence: 4 },
1029            4,
1030            8,
1031        ));
1032        assert!(!journal_checkpoint_is_well_formed(
1033            &JournalInspectionCheckpoint::AfterBatch { sequence: 8 },
1034            4,
1035            8,
1036        ));
1037        assert!(!journal_checkpoint_is_well_formed(
1038            &JournalInspectionCheckpoint::CheckingBatchIdentity {
1039                sequence: 7,
1040                batch_id: [1; 16],
1041                next_prior_sequence: 4,
1042            },
1043            4,
1044            8,
1045        ));
1046    }
1047}