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