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